ZodOps

Case study · 5 min · May 20, 2026

The Deny Still Held

A weekend AdministratorAccess grant outlived the migration. The SCP kept the role from leaving the org or turning off CloudTrail. It couldn't stop a bucket policy from trusting an account we didn't own. I treated the deny as the ceiling, then added resource control policies for the data path.

At a glance

Case study summary: environment, role, tools, and timeline
EnvironmentMulti-account AWS organization, financial services, about 8,000 employees
My roleEnterprise Security Architect, one of two engineers on the cloud program
ToolsAWS Organizations (SCPs and RCPs), CloudTrail, Config, GuardDuty
TimelineThree months from inventory to org-wide guardrails

The problem

The platform team reviewed IAM policies carefully and treated the organization-level deny as a footnote. Member accounts could still leave the organization. Security services could still be turned off in an account that had drifted. And a migration weekend left AdministratorAccess attached to a deployment role on Monday morning.

The organization needed a ceiling that still held when someone attached a broad IAM policy, without pretending that ceiling was the whole perimeter around the data.

What made it hard

  • SCPs only constrain principals in member accounts. They don't stop a bucket or KMS key policy from granting access to an account outside the organization.
  • SCPs don't apply to the management account. Identities there, including that account's root user, sit outside the ceiling. Workloads had been run there "just for bootstrap."
  • A useful guardrail still has to leave a path for the people who operate security tooling. A blanket deny on CloudTrail, Config, and GuardDuty changes would lock out the team responsible for running them.

What I did

Wrote the deny as a ceiling, not a room. The guardrail SCP kept security logging and detection on: no one in a member account could stop or delete CloudTrail, turn off Config, or disable GuardDuty. It also blocked leaving the organization. An explicit deny in an SCP overrides any allow in a member account, including that account's root user.

Denied the regions the company doesn't use. A region deny has to exempt global services such as IAM, Organizations, and CloudFront, which run through a single region, or it breaks them in every account.

Left a named exception for security operators. The SCP used an ArnNotLike condition so a SecurityAdmin role could still manage the services the deny was protecting. Everyone else hit the ceiling.

Paired SCPs with resource control policies on data accounts. After we found a migration bucket that trusted an account outside the organization, I added RCPs so resource policies on supported services couldn't grant access to principals outside the organization.

Moved bootstrap work out of the management account. SCPs and RCPs don't apply to the management account, so anything that looked like a workload came out of it.

What we found

  • The weekend admin grant was still attached. The SCP had done its job: the role couldn't stop CloudTrail or leave the org. But it could read any bucket in the account whose own policy didn't block it, because SCPs are guardrails, not a list of every sensitive read.
  • A bucket policy from the same migration trusted an external account. The SCP never saw that grant. Identity policies constrain principals you own. Resource policies can invite principals you don't.
  • The management account had more than break-glass in it. Two CI roles and a leftover sandbox had been created there because it was convenient.

Results

  • Leave-org, security-tooling, and region denials applied to every member account, with a documented SecurityAdmin exception.
  • RCPs landed on the accounts that hold customer data, so a bucket policy can no longer quietly grant access outside the organization.
  • The AdministratorAccess grant came off the deployment role, and the IAM review started treating the SCP as the first read, not the last.
  • Leadership had a one-page picture of the data perimeter: SCPs for identities, RCPs for resources, and a clear note that the management account is outside both.

What I'd do differently

Inventory resource policies in the same week as SCPs. We found the trusted-outside-account bucket after the identity guardrails were already in review. The data-perimeter model treats those as a set.

Test the SecurityAdmin exception in a non-prod account first. A deny that locks your own security team out of logging is worse than no deny. The exception is part of the design, not a hotfix.