The landing zone ships five 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 denyS3BucketDeletionSCP: ServiceControlPolicy = {
policyName: 'DenyS3BucketDeletion',
description: 'Prevents deletion of general purpose S3 buckets',
policyType: PolicyType.SERVICE_CONTROL_POLICY,
content: {
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyS3BucketDeletion',
Effect: 'Deny',
Action: 's3:DeleteBucket',
Resource: 'arn:aws:s3:::*',
},
],
},
};
This example denies deletion of general purpose S3 buckets. It does not prevent deleting objects.
The content field is the raw IAM policy document. AWS Organizations enforces a 10,240-character size limit per SCP, raised from 5,120 in May 2026. Synthesis rejects policies that exceed the size limit. 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:
// Add this entry to the existing serviceControlPolicies array
// on ProductionOU, preserving its existing policies, accounts, and name:
denyS3BucketDeletionSCP,
To apply it across the entire organization, add the same entry to the existing root.serviceControlPolicies array instead. Keep the existing policies and the full organizationalUnits map. These examples show only the array entry to add, not a replacement organization structure.
Before attaching at the root, count the SCPs already there. The root starts with 3 shipped custom SCPs plus FullAWSAccess, leaving room for 6 additional SCPs within the ceiling of 10 total. Adding this policy leaves 5 slots if no other policies are attached. Each root, OU, and account has its own attachment quota; inherited SCPs do not count against it. The AWS-managed FullAWSAccess policy counts toward the quota where attached.
4. Preview the change
pnpm run organization:diff
The diff should show a new AWS::Organizations::Policy resource with the intended target IDs.
5. Deploy
pnpm run organization:deploy
AWS Organizations creates the SCP and attaches it at the specified levels. Verify the policy on a test account after deployment before applying it more broadly.
6. Verify
Confirm the policy exists in AWS Organizations:
aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
--query 'Policies[?Name==`DenyS3BucketDeletion`]'
Test in a non-production account before attaching to the root or to production OUs. For this example, use an empty disposable bucket in a test account and a role that otherwise has permission to delete it. Confirm deletion is denied while the SCP is attached, then remove the test policy and clean up the bucket.
What happens on deploy
LandingZoneOrganizationStack 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. A policy no longer referenced anywhere in the organization structure is removed from the deployment.
The starter's custom SCPs use deny statements: they restrict what IAM already allows. They cannot grant permissions. Keep the AWS-managed FullAWSAccess policy attached when using this deny-list approach; your custom SCPs layer on top as additional restrictions.