Yesterday's IAM evaluation algorithm described single-account permission flow. But real companies don't operate one account—they segment: Operations, Development, Data Analytics, Security Audit, Billing. Each account then distributes users by department and role. A company with 100 AWS accounts is common (Amazon itself has tens of thousands), and the question becomes: "How do we prevent a developer with AdministratorAccess from obtaining permissions close to company IAM root?" Today's answer: SCP, Permission Boundary, and IAM Identity Center.
All three solve the same goal (delegation safety net) but at different layers. When operators miss that layer distinction, they flounder through "why isn't permission working?" or "why can't I block this?"
Whether user U in account A can do action X is calculated by:
effective_permission(U, X) =
SCP_on_account(A)
∩ permission_boundary(U)
∩ identity_policy(U)
∩ (resource_policy(X) OR identity_policy(U))
∩ session_policy(if assumed)
- any_explicit_deny
Each ∩ (intersection) means "only what's Allow-ed here AND also Allow-ed in the next step passes." SCP denies = identity policy irrelevant. Boundary denies = identity policy irrelevant.
The operator value of this structure: safety net for delegation. Security team writes SCP + Boundary guardrails; developers delegate create Roles freely within them.
💡 Related Theory: This model combines capability-based security (authority token) and ABAC. Saltzer & Schroeder's "The Protection of Information in Computer Systems" (1975, CACM) defined Least Privilege; this implements it at distributed scale. Effective permission via policy intersection is a lattice-based access control variant (Denning, 1976). Since Bell-LaPadula (1973) formalized mandatory access control for military security, the "intersection of multiple policies" paradigm became the standard for distributed system security.
SCP (Service Control Policy) is organization-level guardrail. Regardless of IAM permissions, it makes "this action is impossible in this account." Operators commonly use:
{
"Effect": "Deny",
"NotAction": ["iam:*", "support:*", "route53:*", "cloudfront:*",
"organizations:*", "sts:*", "waf:*", "globalaccelerator:*"],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["ap-northeast-2", "us-east-1"]
}
}
}This SCP blocks all regions except ap-northeast-2 and us-east-1 (excluding IAM, support, route53, cloudfront, organizations, sts, waf). Stops "EC2 bitcoin mining in a region the operator doesn't watch" scenarios. Post-account breach, attackers often "spin c6i.32xlarge × 100 in an obscure region." Region-lock SCP blocks that entirely.
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
}Forces no daily work from root. Management account root is exempt from SCP (exception), so this applies to member accounts only. Since 2024, AWS lets management account centralize root access (aws iam centralize-root-access).
{
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail",
"cloudtrail:PutEventSelectors"
],
"Resource": "*"
}Prevents intruders from covering tracks. GuardDuty flags Stealth:IAMUser/CloudTrailLoggingDisabled, but SCP blocks, not detects.
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}Forces IMDSv2 on new EC2. Stronger than Config rule—blocks creation itself. Adopted universally post-Capital One.
{
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}Blocks S3 PutObject without SSE header. Since 2023, S3 auto-applies SSE-S3 default; add StringNotEquals: aws:kms for KMS-only enforcement.
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"ForAnyValue:StringNotLike": {
"ec2:InstanceType": ["t3.*", "t4g.*", "m5.large", "m5.xlarge"]
}
}
}Applied to dev sandbox OU to block expensive instance accidents. SCP is policy-based, so applies consistently to IaC too.
⚠️ Pitfall: SCP never grants Allow. Even
Allow: s3:*in SCP is just a whitelist—real permission granting is IAM Policy. Another: SCP doesn't apply to service-linked roles or AWS service principals' internal API calls.
🔍 Deeper Dive: Removing
FullAWSAccessfrom SCP means account can't do anything. SCP attached to OU applies to all descendant accounts. Pattern: organize OUs by environment (prod, dev, sandbox) → apply appropriate SCP per OU. Sandbox gets "EC2 t3 medium max," Prod gets "region lock + IAM change restriction." Max 5 SCP per account, 5120 character limit per policy. Operators distribute and compose multiple SCPs across OU hierarchy.
📚 Case Study: 2023 SaaS company—GitHub Actions OIDC role too permissive, PR code injection stole IAM admin. But prod OU's SCP denied
iam:CreateUser+iam:CreateAccessKey, so attacker couldn't create permanent credentials. After 24 hours, STS token expired and auto-locked. SCP was last-resort breach containment.
If SCP is account-wide ceiling, Permission Boundary is per-user/role ceiling. Most common: "delegate Role creation to developers" while enforcing boundary guardrails.
{
"Effect": "Allow",
"Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy"],
"Resource": "arn:aws:iam::*:role/Dev-*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary":
"arn:aws:iam::123456789012:policy/DeveloperBoundary"
}
}
}Developer-created Roles must start Dev- and attach DeveloperBoundary. Effective permission = boundary-limited only.
If operators create all Roles = bottleneck. If developers get just iam:CreateRole = risky. Boundary = balanced. Security team writes one solid boundary; developers free-build inside it.
Applies to IaC too (Terraform/CDK auto-create Roles). Terraform's aws_iam_role gets forced permissions_boundary attribute, company-managed.
📚 Case Study: 2022 fintech—developer created Lambda Role with AdministratorAccess, SSRF breach exposed RDS. Post-incident: boundary pattern adopted.
iam:CreateRoleallowed, boundary removes RDS/KMS write, operations require ops team Role assumption. Lambda effective permission bounded; breach contained.
⚠️ Pitfall: Boundary sets Allow ceiling, not Deny. If boundary only has
Allow: s3:*, identity policy's other permissions are filtered—S3 only passes. Boundary only applies identity-side; cross-account resource policy Allow can't be blocked by boundary.
Creating IAM Users for every employee is anti-pattern:
Solution: IAM Identity Center (ex-AWS SSO). Single user source (external IdP or built-in directory), one login for all AWS accounts.
[Employee]
│
├─ Okta / Azure AD / Google Workspace (SAML 2.0 or SCIM)
│ │
│ ▼
│ IAM Identity Center (one login)
│ │
├──────┼──────┬──────┬──────┐
▼ ▼ ▼ ▼ ▼
Prod Dev Audit Logs Billing
Account Account Account Account Account
│
Each account auto-grants permissions via Permission Set (auto-generated IAM Role)
Permission Set = "permissions a user has in one account." Essence: auto-created IAM Role (AWSReservedSSO_<PSName>_<hash>) + user assumption mapping.
PermissionSet: AdministratorAccess
- Managed Policy: AdministratorAccess
- Session Duration: 4 hours
- User: user@company.com → automatically available all accounts
- Customer Managed Policy: optional add
- Permissions Boundary: optionalOperators manage which Permission Set each user has in which account. User logs console → sees accessible account list → clicks → gets STS temp creds → console opens. CLI: aws configure sso + aws sso login for browser auth.
🔍 Deeper Dive: Identity Center credentials are also STS temp. Session 1-12 hours per Permission Set. No permanent access key. CLI also uses
aws sso login(browser auth) for temp creds.~/.aws/sso/cache/token valid during SSO session (~90 days max), but new STS temp creds issued each use. This is the standard for all automation.
📚 Case Study: Company—200 employees, 100 accounts. Employee had avg 8 access keys. Rotation/offboarding misses = dozens of monthly alerts. Identity Center: keys nearly gone, offboard employee = IdP disable = instant all-account lock. IdP group permissions (e.g., "data-engineers: PowerUser all dev, ReadOnly prod") = zero permission change burden.
For scale, IAM supports ABAC (Attribute-Based Access Control): grant permissions via tags, not user/role IDs.
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Department": "${aws:PrincipalTag/Department}"
}
}
}One policy: "employees start/stop EC2s tagged with their department." No policy explosion for 100 people × 1000 EC2. Identity Center maps IdP attributes (e.g., AD department) to PrincipalTag; department change in IdP = AWS permission auto-change.
💡 Related Theory: ABAC standardized in NIST SP 800-162. RBAC's N×M (role × permission) matrix explodes; ABAC solves via dynamic attribute eval. IAM implements all ABAC elements (subject, resource, environment attributes) via Condition. Google Zanzibar (2019) solved same via ReBAC (Relationship-Based—graph of group membership). AWS = tag ABAC, Google = relation graph, Azure = Resource Hierarchy. Same problem, different answers.
AWS services (ECS, Auto Scaling, ELB, GuardDuty) auto-generate dedicated Roles for service-to-service calls. These are Service-Linked Roles (SLR).
Traits:
AWSServiceRoleFor*⚠️ Pitfall: Deleting SLR triggers dependency checks; in-use = rejected. Exam: "ECS cluster auto-generated Role—modify it?" Answer: "Can't—AWS manages." SCP can't block SLR operations (service principal bypass).
Standard denial-trace pattern:
SELECT eventTime, eventName, errorCode, errorMessage,
userIdentity.type, userIdentity.arn,
requestParameters.bucketName
FROM cloudtrail_logs
WHERE errorCode = 'AccessDenied'
AND eventTime > '2025-05-25T00:00:00Z'
ORDER BY eventTime DESC
LIMIT 100;Four things to check:
🔍 Deeper Dive: "explicit deny in identity-based policy" = identity policy denied. "explicit deny in resource-based policy" = resource policy. "implicit deny" = nowhere allowed. "explicit deny in service control policy" = SCP. This one line tells you where to look. Standardized since 2022.
Today's diagram: IAM permissions built in multi-layer guardrails. SCP (account-level), Boundary (user/role-level), Identity Policy (actual grant), Resource Policy (target-side allow). Any layer's Deny wins. Identity Center adds auth/SSO atop this, solving N-account user management explosion.
Tomorrow: AWS Organizations and multi-account governance making all this possible. How to manage 100 accounts consistently.
Click a choice to reveal the answer and explanation.
Question 1
SCP has `Allow: s3:*`, IAM Policy has `Deny: s3:DeleteObject`, Bucket Policy has `Allow: s3:DeleteObject`. Result?
Question 2
Operator wants developers to create Roles but enforce Role must attach `DevBoundary` policy. Answer?
Question 3
200 employees, 50 accounts. Minimize user management burden?
Question 4
Force all accounts: only ap-northeast-2 and us-east-1 allowed. Best tool?
Question 5
Auto-created `AWSServiceRoleForECS` from new cluster—modify its permissions?
Question 6
Identity Center user gets STS token via CLI. Which command?