Guide · AWS Organizations · Service Control Policies
AWS Organizations SCPs from MCP Server — CreatePolicy, AttachPolicy, evaluation logic
Service Control Policies (SCPs) define the maximum permissions available to accounts in your AWS organization. From an MCP server you create SCPs with CreatePolicyCommand and attach them to roots, OUs, or individual accounts with AttachPolicyCommand. The critical concept is SCPs do not grant permissions — they only restrict them: effective permissions for an IAM principal in a member account = IAM permissions ∩ all applicable SCPs. If an SCP doesn't explicitly allow an action, the action is denied even if the IAM policy allows it. SCPs never apply to the management account itself.
TL;DR
Create an SCP with CreatePolicyCommand{Content: JSON, Name, Description, Type: 'SERVICE_CONTROL_POLICY'}. Attach it with AttachPolicyCommand{PolicyId, TargetId} where TargetId is a root ID (r-), OU ID (ou-), or account ID (12-digit). The FullAWSAccess policy (p-FullAWSAccess) is attached to the root by default — do not detach it unless you're applying an allowlist strategy. Up to 5 SCPs per target, up to 1000 SCPs per org. SCP content max 5120 characters.
SCP evaluation logic
Understanding SCP evaluation prevents accidental lockouts. The effective permission for any action is the intersection of:
- The IAM identity-based policy (must Allow the action)
- All SCPs on the account's path from root to account (all must Allow or none must Deny)
| IAM policy | SCP result | Effective permission |
|---|---|---|
| Allow | Allow (FullAWSAccess) | Allowed |
| Allow | Deny (explicit Deny statement) | Denied |
| Allow | Not present in any SCP Allow | Denied (implicit) |
| Deny | Allow | Denied (IAM deny wins) |
| Not present | Allow | Denied (no IAM grant) |
The FullAWSAccess managed SCP (p-FullAWSAccess) allows all actions on all resources and is the default on every root and new OU. With only FullAWSAccess attached, IAM policies in member accounts function normally. The two SCP strategies are:
- Denylist (default): Keep
FullAWSAccessattached, then add Deny SCPs for specific actions. Easier to manage; individual Deny SCPs are additive. - Allowlist: Detach
FullAWSAccessand attach a minimal Allow policy. More restrictive but harder to maintain — missing any service breaks it.
Creating and attaching SCPs
import {
OrganizationsClient,
CreatePolicyCommand,
AttachPolicyCommand,
DetachPolicyCommand,
DeletePolicyCommand,
ListPoliciesCommand,
DescribePolicyCommand,
ListPoliciesForTargetCommand,
ListTargetsForPolicyCommand,
UpdatePolicyCommand,
} from '@aws-sdk/client-organizations';
const org = new OrganizationsClient({ region: 'us-east-1' });
export async function createSCP(params: {
name: string;
description: string;
content: object; // the SCP JSON document
}): Promise<string> { // returns PolicyId
const response = await org.send(new CreatePolicyCommand({
Name: params.name,
Description: params.description,
Type: 'SERVICE_CONTROL_POLICY',
Content: JSON.stringify(params.content), // must be a JSON string, max 5120 chars
}));
return response.Policy!.PolicySummary!.Id!; // e.g. 'p-examplepolicyid111'
}
export async function attachSCP(policyId: string, targetId: string): Promise<void> {
await org.send(new AttachPolicyCommand({ PolicyId: policyId, TargetId: targetId }));
}
export async function detachSCP(policyId: string, targetId: string): Promise<void> {
await org.send(new DetachPolicyCommand({ PolicyId: policyId, TargetId: targetId }));
}
// Delete only works if the policy has no attached targets
export async function deleteSCP(policyId: string): Promise<void> {
await org.send(new DeletePolicyCommand({ PolicyId: policyId }));
}
export async function updateSCPContent(policyId: string, content: object): Promise<void> {
await org.send(new UpdatePolicyCommand({
PolicyId: policyId,
Content: JSON.stringify(content),
}));
}
// List all SCPs in the org
export async function listAllSCPs() {
const policies = [];
let nextToken: string | undefined;
do {
const response = await org.send(new ListPoliciesCommand({
Filter: 'SERVICE_CONTROL_POLICY',
NextToken: nextToken,
}));
policies.push(...(response.Policies ?? []));
nextToken = response.NextToken;
} while (nextToken);
return policies;
}
// List SCPs attached to a specific target (root/OU/account)
export async function listSCPsForTarget(targetId: string) {
const policies = [];
let nextToken: string | undefined;
do {
const response = await org.send(new ListPoliciesForTargetCommand({
TargetId: targetId,
Filter: 'SERVICE_CONTROL_POLICY',
NextToken: nextToken,
}));
policies.push(...(response.Policies ?? []));
nextToken = response.NextToken;
} while (nextToken);
return policies;
}
// List all targets a policy is attached to
export async function listTargetsForSCP(policyId: string) {
const targets = [];
let nextToken: string | undefined;
do {
const response = await org.send(new ListTargetsForPolicyCommand({
PolicyId: policyId,
NextToken: nextToken,
}));
targets.push(...(response.Targets ?? []));
nextToken = response.NextToken;
} while (nextToken);
return targets; // each has TargetId, Name, Arn, Type (ROOT/OU/ACCOUNT)
}
Common SCP patterns
These are the most frequently deployed SCP guardrails. Each is a denylist SCP — safe to attach alongside FullAWSAccess.
Deny actions outside approved regions
const denyNonApprovedRegions = {
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyOutsideApprovedRegions',
Effect: 'Deny',
NotAction: [
// Global services that always use us-east-1 — must be excluded
'iam:*',
'organizations:*',
'route53:*',
'budgets:*',
'waf::*',
'cloudfront:*',
'sts:*',
'support:*',
'trustedadvisor:*',
],
Resource: '*',
Condition: {
StringNotEquals: {
'aws:RequestedRegion': ['us-east-1', 'us-west-2', 'eu-west-1'],
},
},
},
],
};
The NotAction list is essential — global services (IAM, Organizations, Route 53, CloudFront, etc.) always make API calls to us-east-1 regardless of your selected region. Without the NotAction exclusions, IAM role creation and DNS operations will fail.
Require MFA for sensitive actions
const requireMFAForSensitiveActions = {
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyHighRiskActionsWithoutMFA',
Effect: 'Deny',
Action: [
'iam:CreateAccessKey',
'iam:DeleteAccessKey',
'iam:UpdateAccessKey',
'iam:CreateUser',
'iam:DeleteUser',
'iam:AttachRolePolicy',
'iam:PutRolePolicy',
's3:DeleteBucket',
'ec2:TerminateInstances',
],
Resource: '*',
Condition: {
BoolIfExists: { 'aws:MultiFactorAuthPresent': 'false' },
},
},
],
};
Prevent leaving the organization
const preventLeaveOrg = {
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyLeaveOrganization',
Effect: 'Deny',
Action: 'organizations:LeaveOrganization',
Resource: '*',
},
],
};
Protect security tooling
const protectSecurityTooling = {
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyDisablingSecurityServices',
Effect: 'Deny',
Action: [
'guardduty:DeleteDetector',
'guardduty:DisassociateFromMasterAccount',
'securityhub:DisableSecurityHub',
'config:StopConfigurationRecorder',
'config:DeleteConfigurationRecorder',
'config:DeleteDeliveryChannel',
'cloudtrail:DeleteTrail',
'cloudtrail:StopLogging',
'macie2:DisableMacie',
'access-analyzer:DeleteAnalyzer',
],
Resource: '*',
},
],
};
Enabling SCP type (if not already enabled)
SCP type must be enabled on the root before you can attach any SCP other than FullAWSAccess. In most orgs it's already enabled, but if you're setting up a new org programmatically, enable it first.
import {
EnablePolicyTypeCommand,
DescribePolicyCommand,
} from '@aws-sdk/client-organizations';
export async function enableSCPType(rootId: string): Promise<void> {
try {
await org.send(new EnablePolicyTypeCommand({
RootId: rootId,
PolicyType: 'SERVICE_CONTROL_POLICY',
}));
} catch (err: any) {
// PolicyTypeAlreadyEnabledException is expected if SCPs were already on
if (err.name !== 'PolicyTypeAlreadyEnabledException') throw err;
}
}
Org feature set must be ALL (not CONSOLIDATED_BILLING) for SCP type to be enabled. Use DescribeOrganizationCommand to check Organization.FeatureSet before attempting to enable.
Error reference
| Error | Cause | Fix |
|---|---|---|
PolicyNotAttachedException | DetachPolicyCommand on a policy not attached to the target | Check ListPoliciesForTargetCommand first |
PolicyInUseException | DeletePolicyCommand when policy is still attached to targets | Call DetachPolicyCommand on all targets first |
DuplicatePolicyAttachmentException | AttachPolicyCommand when policy is already on that target | Check ListPoliciesForTargetCommand before attaching |
PolicyTypeNotEnabledException | Trying to attach SCPs before enabling SCP type on root | Call EnablePolicyTypeCommand first |
MalformedPolicyDocumentException | SCP JSON is invalid (syntax error or invalid IAM grammar) | Validate JSON; check for NotAction/NotResource usage correctness |
PolicySizeExceededException | SCP content exceeds 5120 characters | Split into multiple SCPs; use wildcards to consolidate action lists |
Related pages
- AWS Organizations overview — ListAccounts, CreateAccount, MoveAccount — manage the account structure
- AWS Organizations tag policies — enforcement and GetEffectivePolicy — standardize tagging across the org
- AWS Organizations delegated administrators — SecurityHub, Config, GuardDuty — delegate org-wide service administration
- AWS Security Hub for MCP servers
- AWS IAM for MCP servers