AWS CDK Landing Zone

Service Control Policies

Shipped guardrail SCPs and how to author custom Service Control Policies for your AWS Organization.

src/config/service-control-policies/ contains the Service Control Policies that govern what actions are allowed across your AWS Organization. The landing zone ships five ready-made SCPs covering the most critical guardrails, all attached by default. You can use them as-is, modify their content, or add your own.

SCPs are attached in organization-structure.ts at the root, OU, or account level. AWS Organizations enforces a maximum of 10 SCPs per attachment point.

Shipped SCPs

SCPDefault attachmentWhat it enforces
criticalSecurityGuardrailsSCPRootDenies leaving the organization, creating IAM users and access keys, disabling EBS encryption by default, and modifying the account password policy
denyAllOutsidePrimaryAndSecondaryRegionsSCPRootRestricts all AWS API calls to your configured primary and secondary regions; exempts global services (IAM, Route 53, CloudFront, etc.) and landing-zone roles
protectTaggedCloudFormationStacksSCPRootDenies deleting CloudFormation stacks tagged with your organization name (organization: <organizationName>), protecting landing-zone managed stacks from accidental deletion
protectSecurityHubConfigurationSCPSecurity accountPrevents modification of Security Hub configuration in the delegated administrator account: disabling standards, deleting members, changing org configuration
lockdownSuspendedAccountsSCPSuspended OUDenies all AWS actions in accounts that have been moved to the Suspended OU

Exempt principals

A guardrail that denies an action still has to let the landing zone, and you, operate. src/config/service-control-policies/administrator-principals.ts is the single place those exemptions are defined, and every shipped guardrail reads its allowlists from there rather than repeating ARNs.

It exports three lists:

FunctionUsed byCovers
securityServiceAdministratorPrincipalArns()Region guardrail, Security Hub protectionSSO administrators, the Security Hub service role, the landing zone's own roles, and your external admins
landingZoneAdministratorPrincipalArns()CloudFormation stack protectionSSO administrators plus the landing zone's own roles and your external admins
suspendedAccountExemptPrincipalArns()Suspended-account lockdownThe principals that must keep access while an account is suspended

The landing zone's own roles come from LandingZoneRoles.principalArnAllowList(), which covers the StackSet administration and execution roles, the organization account access role, the GitHubActionsServiceRole your pipeline assumes, and a role for each entry in externalAdmins. Setting externalAdmins in landing-zone-settings.ts is enough to exempt them everywhere.

Edit this file when you have a break-glass or automation role that must survive the guardrails. Because the guardrails share these lists, adding a principal once applies it to every SCP that uses that list.

The region guardrail also exempts global services (IAM, Route 53, CloudFront, AWS Organizations, billing, KMS, S3, STS, support, and others with no regional API boundary). When you add secondaryRegions to landing-zone-settings.ts and redeploy, it includes those regions in the allow list automatically.

Authoring a custom SCP

Create a new file (or add to an existing one) in src/config/service-control-policies/:

export const denyS3PublicAccessSCP: ServiceControlPolicy = {
  content: {
    Version: '2012-10-17',
    Statement: [
      {
        Sid: 'DenyS3PublicAccess',
        Effect: 'Deny',
        Action: [
          's3:PutBucketPublicAccessBlock',
          's3:DeletePublicAccessBlock',
        ],
        Resource: '*',
        Condition: {
          StringEquals: {
            's3:publicAccessBlockConfiguration/BlockPublicAcls': 'false',
          },
        },
      },
    ],
  },
  description: 'Prevents disabling S3 Block Public Access at the bucket level',
  policyName: 'DenyS3PublicAccess',
  policyType: PolicyType.SERVICE_CONTROL_POLICY,
};

Export it from src/config/service-control-policies/index.ts:

export * from './your-new-guardrails';

Then attach it in organization-structure.ts at the appropriate level:

import { denyS3PublicAccessSCP } from './service-control-policies';

// In the organizationStructure object:
ProductionOU: {
  name: 'workload-prod-ou',
  serviceControlPolicies: [denyS3PublicAccessSCP],
  accounts: { ... },
},

Things to know

  • Maximum 10 directly attached SCPs per attachment point (root, OU, or account). AWS raised this limit from 5 to 10 in May 2026. SCPs inherited from a parent do not count against a child's own quota, so an account can carry 10 of its own SCPs regardless of what its OU has attached.
  • Quota violations fail at synth, not mid-deploy. OrganizationConstruct checks every attachment point and policy document when you synthesize: more than 10 SCPs on one node or a policy over 10,240 characters fails the synth, and you get a warning once a node reaches 8 attachments or a document approaches the size limit.
  • SCPs are deny-only: they can only restrict what IAM already allows; they cannot grant permissions.
  • The FullAWSAccess managed SCP is always attached by AWS Organizations at every level. Your custom SCPs layer on top of it as additional restrictions.
  • Test in a non-production OU first: attach a new SCP to a development account before applying it at the root or to production accounts.