Service Control Policies
Service Control Policies (SCPs) define the maximum permissions available to identities in member accounts. The Data Landing Zone composes SCPs from three additive tiers and ships a library of vetted preset statements you can drop in.
Composition model
SCPs in DLZ are composed from three tiers. All tiers are additive only — a tier cannot weaken or remove statements from a tier above it.
| Tier | Applies to | Configured via |
|---|---|---|
| Org baseline | Every workload account | scpBaselineStatements (or denyServiceList) |
| Per-account-type | All accounts of one type | scpStatementsByAccountType.{development | production} |
| Per-account | One specific account | DLzAccount.scpStatements |
The mandatory-tags SCP is always appended after the org baseline and cannot be opted out of.
SCP preset library
DLZ ships a library of preset statements covering common deny patterns. Each preset returns an
iam.PolicyStatement you can compose into any tier. Presets exempt the Control Tower execution role automatically
where required.
| Preset | Purpose |
|---|---|
ScpDenyActionsOutsideRegions | Deny regional API calls outside an allow-list (global services exempt). |
ScpDenyServiceActions | Deny a caller-supplied list of service:action strings. |
ScpDenyDisablingSecurityServices | Block disabling GuardDuty, Macie, Security Hub, Config, CloudTrail, etc. |
ScpDenyLeavingOrganization | Block organizations:LeaveOrganization. |
ScpDenyRootUserActions | Block actions performed by the account root user. |
ScpDenyRootCredentialsManagementInMemberAccounts | Block management of root credentials in member accounts. |
ScpDenyIamWithoutPermissionsBoundary | Require a permissions boundary on all IAM principals. |
ScpDenyS3PublicAccessBypass | Block bypassing S3 Block Public Access settings. |
ScpDenyS3ObjectLockAndRetention | Block tampering with S3 Object Lock and retention. |
ScpDenyBackupVaultLock | Block tampering with AWS Backup Vault Lock. |
ScpDenyGlacierVaultLock | Block tampering with S3 Glacier Vault Lock. |
ScpDenyMarketplaceSubscriptions | Block AWS Marketplace subscription changes. |
ScpDenyDomainRegistrations | Block Route53 domain registrations and transfers. |
ScpDenyDedicatedInfraAndSubscriptions | Block dedicated hosts, capacity reservations, and similar commitments. |
ScpDenyReservedCapacityPurchases | Block purchasing reserved capacity (EC2 RIs, etc.). |
ScpDenySavingsPlanPurchases | Block purchasing Savings Plans. |
ScpDenyBedrockProvisionedThroughput | Block creating Bedrock Provisioned Throughput. |
ScpDenyCfnStacksWithoutStandardTags | Require mandatory tags on CloudFormation stacks (always appended). |
Defining a baseline
Use scpBaselineStatements for full control over the org baseline. Compose it from presets and your own statements.
import { App } from 'aws-cdk-lib';import { DataLandingZone, ScpDenyActionsOutsideRegions, ScpDenyDisablingSecurityServices, ScpDenyLeavingOrganization, ScpDenyRootUserActions, ScpDenyServiceActions,} from 'aws-data-landing-zone';
const app = new App();const dlz = new DataLandingZone(app, { scpBaselineStatements: [ ScpDenyActionsOutsideRegions.statement(['eu-west-1', 'eu-central-1']), ScpDenyDisablingSecurityServices.statement(), ScpDenyLeavingOrganization.statement(), ScpDenyRootUserActions.statement(), ScpDenyServiceActions.statement(['ecs:*', 'workspaces:*']), ], ...});import aws_cdk as cdkimport aws_data_landing_zone as dlz
app = cdk.App()dlz.DataLandingZone(app, scp_baseline_statements=[ dlz.ScpDenyActionsOutsideRegions.statement(["eu-west-1", "eu-central-1"]), dlz.ScpDenyDisablingSecurityServices.statement(), dlz.ScpDenyLeavingOrganization.statement(), dlz.ScpDenyRootUserActions.statement(), dlz.ScpDenyServiceActions.statement(["ecs:*", "workspaces:*"]), ], ...)Layering by account type
Apply additional restrictions to all accounts of one type via scpStatementsByAccountType. For example, prevent
production accounts from purchasing long-term commitments while leaving development accounts unrestricted:
import { DataLandingZone, ScpDenyReservedCapacityPurchases, ScpDenySavingsPlanPurchases,} from 'aws-data-landing-zone';
const dlz = new DataLandingZone(app, { scpStatementsByAccountType: { production: [ ScpDenyReservedCapacityPurchases.statement(), ScpDenySavingsPlanPurchases.statement(), ], }, ...});dlz.DataLandingZone(app, scp_statements_by_account_type={ "production": [ dlz.ScpDenyReservedCapacityPurchases.statement(), dlz.ScpDenySavingsPlanPurchases.statement(), ], }, ...)Per-account extras
Use scpStatements on a DLzAccount for one-off restrictions that only apply to a single account.
import * as iam from 'aws-cdk-lib/aws-iam';
const dlz = new DataLandingZone(app, { organization: { ous: { workloads: { accounts: [ { name: 'data-prod', type: DlzAccountType.PRODUCTION, scpStatements: [ new iam.PolicyStatement({ sid: 'DenyDynamoDBDelete', effect: iam.Effect.DENY, actions: ['dynamodb:DeleteTable'], resources: ['*'], }), ], ... }, ], }, ... }, ... }, ...});from aws_cdk import aws_iam as iam
dlz.DataLandingZone(app, organization=dlz.DLzOrganization( ous=dlz.OrgOus( workloads=dlz.OrgOuWorkloads( accounts=[ dlz.DLzAccount( name="data-prod", type=dlz.DlzAccountType.PRODUCTION, scp_statements=[ iam.PolicyStatement( sid="DenyDynamoDBDelete", effect=iam.Effect.DENY, actions=["dynamodb:DeleteTable"], resources=["*"], ), ], ... ), ], ), ... ), ... ), ...)Service Deny List (legacy)
Before the preset library was introduced, the denyServiceList property accepted a flat list of service:action
strings to deny in the org baseline. It is still supported for the simple case but is deprecated in favor of
scpBaselineStatements with ScpDenyServiceActions.
denyServiceList and scpBaselineStatements cannot be used together — DLZ throws at synthesis time if both are set.
import { App } from 'aws-cdk-lib';import { DataLandingZone, Defaults } from 'aws-data-landing-zone';
const app = new App();const dlz = new DataLandingZone(app, { denyServiceList: [ ...Defaults.denyServiceList(), 'ecs:*', ], ...});import aws_cdk as cdkimport aws_data_landing_zone as dlz
app = cdk.App()dlz.DataLandingZone(app, deny_service_list=[ *dlz.Defaults.deny_service_list(), "ecs:*" ], ...)The Defaults.denyServiceList() helper returns the conservative starter list. See the source
here.
Validation
DLZ validates each account’s resolved SCP at synthesis time and fails with a clear error if:
- the resolved policy is empty (AWS rejects empty policies)
- more than 5 SCPs would be attached to the account
- the policy body exceeds 5120 bytes
This catches misconfiguration before any CloudFormation deployment runs.