Skip to content

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.

TierApplies toConfigured via
Org baselineEvery workload accountscpBaselineStatements (or denyServiceList)
Per-account-typeAll accounts of one typescpStatementsByAccountType.{development | production}
Per-accountOne specific accountDLzAccount.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.

PresetPurpose
ScpDenyActionsOutsideRegionsDeny regional API calls outside an allow-list (global services exempt).
ScpDenyServiceActionsDeny a caller-supplied list of service:action strings.
ScpDenyDisablingSecurityServicesBlock disabling GuardDuty, Macie, Security Hub, Config, CloudTrail, etc.
ScpDenyLeavingOrganizationBlock organizations:LeaveOrganization.
ScpDenyRootUserActionsBlock actions performed by the account root user.
ScpDenyRootCredentialsManagementInMemberAccountsBlock management of root credentials in member accounts.
ScpDenyIamWithoutPermissionsBoundaryRequire a permissions boundary on all IAM principals.
ScpDenyS3PublicAccessBypassBlock bypassing S3 Block Public Access settings.
ScpDenyS3ObjectLockAndRetentionBlock tampering with S3 Object Lock and retention.
ScpDenyBackupVaultLockBlock tampering with AWS Backup Vault Lock.
ScpDenyGlacierVaultLockBlock tampering with S3 Glacier Vault Lock.
ScpDenyMarketplaceSubscriptionsBlock AWS Marketplace subscription changes.
ScpDenyDomainRegistrationsBlock Route53 domain registrations and transfers.
ScpDenyDedicatedInfraAndSubscriptionsBlock dedicated hosts, capacity reservations, and similar commitments.
ScpDenyReservedCapacityPurchasesBlock purchasing reserved capacity (EC2 RIs, etc.).
ScpDenySavingsPlanPurchasesBlock purchasing Savings Plans.
ScpDenyBedrockProvisionedThroughputBlock creating Bedrock Provisioned Throughput.
ScpDenyCfnStacksWithoutStandardTagsRequire 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:*']),
],
...
});

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(),
],
},
...
});

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: ['*'],
}),
],
...
},
],
},
...
},
...
},
...
});

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:*',
],
...
});

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.

API References