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.