AWS Organizations SCPs: Hard Caps on What Coding-Agent Accounts Can Call

0 views

A tenant’s coding-agent role was supposed to be “S3 + Bedrock + one Lambda.” Someone widened it during an incident; the agent later called iam:CreateAccessKey and walked out with long-lived credentials. IAM identity policies alone are not a blast-radius ceiling when humans (or agents) can attach AdministratorAccess. AWS Organizations Service Control Policies (SCPs) are the unfair advantage: OU-level deny guards that apply even when the role is over-permissioned — agent accounts simply cannot call dangerous APIs. Pair with Verified Permissions, IAM condition keys, and Budgets — this post is the account blast-radius ceiling.

⚡ TL;DR: Put each tenant agent runtime in an account (or tightly scoped OU). Attach SCPs that deny iam:* mutations, organizations:LeaveOrganization, broad kms:ScheduleKeyDeletion, and region escapes. SCPs do not grant access — they only clip the max permissions. Related: Verified Permissions Cedar, IAM condition keys, Lambda recursive loop protection.

Why identity policies are not enough

Defense in depth for coding agents:

Layer Answers Failure mode if alone
Cedar / Verified Permissions May this tool run for this user? Tool runner still has fat IAM
IAM role + condition keys May this principal call this API on tagged resources? Human attaches Admin
SCP on agent OU May anyone in this account call this API class? Nothing — ceiling holds
Budgets / AppConfig Spend + feature kill Too late for IAM theft

SCPs are max permission filters. They never grant. If SCP denies iam:CreateUser, no role in that account can CreateUser — including AdministratorAccess.

json
// ❌ Relying only on a "tight" agent role — next week someone attaches Admin
{
  "Effect": "Allow",
  "Action": ["s3:*", "bedrock:*", "iam:*"],
  "Resource": "*"
}

OU layout for per-tenant agent accounts

Recommended skeleton:

  • Root
  • Security / Shared (no agent workloads)
  • AgentRuntimes OU ← SCP attached here
    • agent-tenant-acme
    • agent-tenant-globex
  • Sandbox OU (different SCP, more deny)

Provision accounts with Control Tower / AFT / Account Factory. Agents never get organizations:* in identity policies; SCP still denies Leave/Remove.

typescript
// ✅ Conceptual: attach SCP to AgentRuntimes OU (Organizations SDK)
import {
  OrganizationsClient,
  AttachPolicyCommand,
} from "@aws-sdk/client-organizations";

const org = new OrganizationsClient({});
await org.send(
  new AttachPolicyCommand({
    PolicyId: "p-agentguard",
    TargetId: "ou-xxxx-agentruntimes",
  })
);

Deny dangerous actions even if roles are wide

Start with a deny list SCP for agent OUs (full-AWS-access SCP still required elsewhere in the hierarchy so allows can exist).

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyIAMMutationsInAgentAccounts",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateAccessKey",
        "iam:CreateLoginProfile",
        "iam:AttachUserPolicy",
        "iam:AttachRolePolicy",
        "iam:PutRolePolicy",
        "iam:UpdateAssumeRolePolicy",
        "iam:PassRole"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/OrgBreakGlass*",
            "arn:aws:iam::*:role/StackSet*"
          ]
        }
      }
    },
    {
      "Sid": "DenyLeaveOrg",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyDangerousAccountMoves",
      "Effect": "Deny",
      "Action": [
        "organizations:DeregisterDelegatedAdministrator",
        "organizations:DisableAWSServiceAccess"
      ],
      "Resource": "*"
    }
  ]
}
json
// ✅ Extra: deny regions you do not operate
{
  "Sid": "DenyNonHomeRegions",
  "Effect": "Deny",
  "NotAction": ["sts:*", "cloudformation:*"],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": {
      "aws:RequestedRegion": ["us-east-1", "eu-west-1"]
    }
  }
}
python
# ❌ SCP that allows iam:* "for convenience" in agent OU — deletes the ceiling
# Never put Allow iam:* on agent runtime OUs via SCP (SCPs rarely Allow anyway;
# the bug is usually forgetting Deny + attaching Admin at identity layer)

PassRole deserves special care: agents that can iam:PassRole into Lambda/CodeBuild can escalate. Deny PassRole broadly, then allowlist specific roles via condition iam:PassedToService + resource ARNs if you must.

Complement Verified Permissions and IAM condition keys

  • Cedar: “user alice may invoke tool apply_patch on repo X”
  • IAM conditions: “role may s3:PutObject only if s3:ExistingObjectTag/tenant=acme” (condition keys post)
  • SCP: “this account may not call iam:CreateAccessKey at all”
typescript
// ✅ Runtime still authorizes tools finely
import { VerifiedPermissionsClient, IsAuthorizedCommand } from "@aws-sdk/client-verifiedpermissions";

const vp = new VerifiedPermissionsClient({});
const decision = await vp.send(
  new IsAuthorizedCommand({
    policyStoreId: process.env.POLICY_STORE!,
    principal: { entityType: "User", entityId: userId },
    action: { actionType: "Action", actionId: "invokeTool" },
    resource: { entityType: "Tool", entityId: "apply_patch" },
  })
);
if (decision.decision !== "ALLOW") throw new Error("denied");
// SCP silently ensures even a buggy allow cannot CreateAccessKey

Recursive agent invokes and privilege loops still need Lambda recursive loop protection — SCPs do not stop application-level loops.

Rollout without locking yourself out

  1. Create SCP in detachable draft; validate JSON
  2. Attach to a pilot OU with one non-prod agent account
  3. Run agent integration suite: Bedrock, S3, Lambda invoke, CodeBuild start
  4. Watch CloudTrail for implicit denies (implicitDeny / Org SCP)
  5. Expand OU; keep break-glass roles exempt via ArnNotLike
  6. Document that management account / stacksets need exclusion paths
bash
# ✅ Test effective permissions from agent role
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::TENANT:role/AgentToolRole \
  --action-names iam:CreateAccessKey bedrock:InvokeModel s3:PutObject

# Expect CreateAccessKey denied by SCP even if identity allows

Pair spend ceilings with Budgets + Cost Anomaly — SCPs stop IAM escapes; Budgets stop token/GPU escapes.

What SCPs do not replace

SCPs will not:

  • Stop an agent from deleting objects in buckets it is allowed to access
  • Validate tool arguments (paths, SQL, shell) — that is application + Cedar
  • Cap Bedrock token spend — use Budgets / Cost Anomaly and AppConfig
  • Fix confused-deputy across accounts without resource policies

They will stop the common “agent escaped IAM” stories: creating users, planting access keys, attaching Admin to itself, leaving the organization to escape centralized CloudTrail, or spinning resources in unauthorized regions.

bash
# ✅ Continuous control: EventBridge / Config rule on IAM API calls from agent accounts
# Any iam:CreateAccessKey from AgentRuntimes OU → PagerDuty (should be zero)
json
{
  "Sid": "DenyS3PublicACL",
  "Effect": "Deny",
  "Action": ["s3:PutObjectAcl", "s3:PutBucketAcl"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "s3:x-amz-acl": ["public-read", "public-read-write"]
    }
  }
}

Document the break-glass path offline. If your only recovery role is also denied by SCP without an exemption, you will learn about SCP semantics at the worst possible time. Test restore of a mis-attached SCP quarterly.

Checklist

  • [ ] Tenant agent workloads live under a dedicated OU with deny-list SCPs
  • [ ] Deny IAM mutations, LeaveOrganization, and dangerous org APIs
  • [ ] Exempt only break-glass / StackSet roles via tight ARN conditions
  • [ ] Keep Cedar + IAM condition keys for fine-grained tool/resource auth
  • [ ] Pilot on one account; simulate CreateAccessKey before broad attach
  • [ ] Monitor CloudTrail for SCP denies; alert on any IAM API attempts from agent roles

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.