AWS CDK Landing Zone

Create a Custom SCP

Author a new Service Control Policy, export it from the barrel, and attach it in organization-structure.ts.

The landing zone ships four ready-made guardrail SCPs covering the most critical controls. When you need an organization-specific restriction (blocking a particular service, locking down an OU's API surface, or enforcing a tagging policy), you author a custom SCP and attach it where needed.

Before writing a new SCP, check whether one of the shipped SCPs already covers your use case, or whether modifying an existing SCP file is cleaner than adding a new one.

Steps

1. Create the SCP file

Create a new TypeScript file in src/config/service-control-policies/. Name it after what it restricts (ec2-guardrails.ts, s3-guardrails.ts, etc.). One file per concern keeps things easy to reason about.

Define and export a const with the ServiceControlPolicy type:

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

The content field is the raw IAM policy document. AWS Organizations enforces a 10,240-character size limit per SCP (after whitespace is removed), raised from 5,120 in May 2026. If you need complex restrictions, split them across multiple SCPs rather than packing everything into one.

policyName must be unique across your organization. Use a descriptive name that makes the policy's purpose obvious in the Organizations console.

2. Export from the barrel

Open src/config/service-control-policies/index.ts and add an export for your new file:

export * from './region-guardrails';
export * from './security-guardrails';
export * from './suspended-account-guardrails';
export * from './s3-guardrails'; // add this line

This makes the SCP constant available throughout the project via the ./service-control-policies import path.

3. Attach the SCP in organization-structure.ts

Open src/config/organization-structure.ts, import your new SCP, and add it to the serviceControlPolicies array at the root, OU, or account level where you want it enforced:

import {
  criticalSecurityGuardrailsSCP,
  denyAllOutsidePrimaryAndSecondaryRegionsSCP,
  denyS3PublicAccessSCP, // import your new SCP
  lockdownSuspendedAccountsSCP,
  protectSecurityHubConfigurationSCP,
} from './service-control-policies';

// Attach to a specific OU:
ProductionOU: {
  name: `${orgName}-workload-prod-ou`,
  serviceControlPolicies: [denyS3PublicAccessSCP],
  accounts: { ... },
},

Or attach it at the root to apply it across the entire organization:

root: {
  serviceControlPolicies: [
    criticalSecurityGuardrailsSCP,
    denyAllOutsidePrimaryAndSecondaryRegionsSCP,
    denyS3PublicAccessSCP,
  ],
  organizationalUnits: { ... },
},

Before attaching at the root, count the SCPs already there. The root already has 2 shipped SCPs by default. Adding more is fine, but you have a ceiling of 10 total. Accounts inside OUs that already carry SCPs can only absorb additional account-level SCPs up to the remaining headroom.

4. Preview the change

pnpm run management:diff

The diff should show a new AWS::Organizations::Policy resource and updated AWS::Organizations::PolicyAttachment resources for every target the SCP is attached to.

5. Deploy

pnpm run management:deploy

AWS Organizations creates the SCP and attaches it at the specified levels. The policy takes effect immediately after attachment. There is no propagation delay.

6. Verify

Confirm the policy exists in AWS Organizations:

aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
  --query 'Policies[?Name==`DenyS3PublicAccess`]'

Test in a non-production account before attaching to the root or to production OUs. Use the IAM Policy Simulator or a dry-run API call to confirm the SCP blocks only the actions you intend.

What happens on deploy

OrganizationStack creates the SCP as an AWS::Organizations::Policy resource and attaches it at every level specified in organizationStructure. The OrganizationConstruct reconciles the full set of attachments on every deploy, so if you later remove an SCP from a target's array, it detaches the policy on the next deploy. It does not delete the policy itself until the ServiceControlPolicy constant is also removed from the code.

SCPs are deny-only: they restrict what IAM already allows. They cannot grant permissions. The AWS-managed FullAWSAccess policy is always attached at every level; your custom SCPs layer on top as additional restrictions.