skip to content

Service control policies in AWS Organizations can be run as a deny list or as an allow list. Compare the two strategies, and say what the allow-list approach costs you operationally.

level: seniorimportance: should knowfreq 48%

answer

  1. keep FullAWSAccess, or detach it
  2. exclusions versus enumeration
  3. every new service becomes a ticket
  4. conditions express the real rule
  5. size and count quotas bite allow lists

basics

~20 s

A deny list keeps the default FullAWSAccess policy and adds targeted Deny statements for a few forbidden things. An allow list detaches FullAWSAccess and enumerates every permitted service — tighter, but it blocks any service nobody remembered to list.

solid answer

~60 s

Deny-list mode leaves `FullAWSAccess` attached and layers narrow `Effect: Deny` statements on top: no regions outside the approved ones, no disabling CloudTrail or GuardDuty, no touching the roles that the landing zone manages. Allow-list mode detaches `FullAWSAccess` and replaces it with an explicit list of permitted services, so anything unlisted is outside the ceiling. Allow lists sound stricter and are, but they turn every new service adoption into a governance ticket, they are hard to keep correct because features span services and action prefixes are not always the name you would guess, and they collide with the SCP size and count quotas — a document listing dozens of prefixes is not small. Deny lists are what most organizations actually run, because a Deny statement with a condition expresses the real rule ("not outside eu-west-1", "not by anyone except the break-glass role") far more precisely than an enumeration ever will. The exception worth making is a tightly scoped sandbox or a regulated OU, where the allow list is small, stable and the friction is the point.

code

json · 23 lines
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "sts:*",
        "organizations:*",
        "route53:*",
        "cloudfront:*",
        "support:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["eu-west-1", "eu-central-1"]
        }
      }
    }
  ]
}

go deeper

for a junior

Know that AWS attaches a default policy allowing everything, and that SCP work is normally about subtracting from that rather than listing what is permitted.

for a middle

Explain the arithmetic — union at a node, intersection down the tree — and why detaching FullAWSAccess is the step that makes an allow list take effect at all.

for a senior

Weigh the operating cost honestly: allow lists convert every new service adoption into a management-account change, collide with SCP quotas, and hide intent that a conditional Deny states directly. Have a safe rollout sequence ready.

for a principal

Decide where each strategy belongs across the OU tree, and own the second-order effect: governance that is slow and opaque gets routed around, so the guardrail set has to stay small enough that teams still respect it.

## The two strategies Every root and OU starts with the AWS-managed **`FullAWSAccess`** SCP attached, which allows `*` on `*`. What you do with that policy determines which strategy you are running. **Deny list** — leave `FullAWSAccess` in place, and attach additional policies containing only `Effect: Deny` statements. The ceiling is "everything, except these named things". **Allow list** — detach `FullAWSAccess` from the node and attach a policy whose Allow statements enumerate exactly which service prefixes are permitted. The ceiling becomes "only these things". They are not exclusive: an organization commonly runs deny-list SCPs at the root for universal guardrails and an allow list on one restrictive OU. ## How the arithmetic works, and why it matters here At a single node, the Allow statements of all attached SCPs are **unioned**. Across levels, each level's result is **intersected** as you descend. Two consequences fall out: 1. An allow-list policy attached *next to* `FullAWSAccess` does nothing at all, because the union is still everything. Detaching `FullAWSAccess` is the step that makes an allow list real. 2. A parent's allow list can never be widened by a child. If the root permits only three services, no OU beneath it can add a fourth. Allow lists therefore have to be designed top-down, and an allow list at the root is a very large commitment. Deny statements need none of this care: a Deny anywhere on the path removes the action, full stop. ## What an allow list actually costs **Every new service is a change request.** A team wants to try Step Functions; the call fails with an SCP denial; someone with management-account access has to edit and redeploy the policy. In a fast-moving org this is a steady tax and it trains teams to route around governance. **Enumeration is not the same as intention.** Features cut across service prefixes in ways that are not obvious — using one feature can require permissions in a supporting service, and IAM action prefixes are not always the name you would guess from the product. An allow list that looks complete usually is not, and the failures show up as puzzling partial breakage rather than a clean denial. **Quotas are real.** SCP documents have a size limit measured in a few kilobytes, and only a small number of SCPs can attach to any one root, OU or account (as of 2025, five policies per entity and 5,120 characters per document). A serious allow list burns most of that budget in one policy, leaving no room for the targeted guardrails you also want. **It hides the rule.** "We allow these 40 prefixes" does not tell a future reader what the policy is *for*. `Deny anything outside eu-west-1` does. ## Why deny lists express real rules better The rules organizations actually have are conditional, not enumerative. A Deny statement carries a `Condition`, which is where the intent lives: ```json { "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "StringNotEquals": { "aws:PrincipalArn": "arn:aws:iam::*:role/OrgBreakGlass" } } } ``` That shape — deny broadly, carve out one principal — has no allow-list equivalent. Common deny-list guardrails include: pinning workloads to approved regions with `aws:RequestedRegion`; forbidding the disabling of CloudTrail, GuardDuty or Config; protecting the roles and resources your landing zone manages; blocking member-account root-user activity; and preventing an account from leaving the organization with `organizations:LeaveOrganization`. Note the `NotAction` trick when writing a region lock: several services are global and their API calls are made against a single region, so denying everything outside your approved regions without exempting IAM, STS, Organizations, Route 53, CloudFront and Support will break the account in ways that are painful to unpick. ## Where an allow list earns its place - **A sandbox OU** where you genuinely want a short list of cheap, low-risk services and nothing else. - **A regulated or data-residency OU** where an auditor wants the permitted surface written down rather than inferred from a list of exclusions. - **A dedicated single-purpose account** — a logging archive account, say — whose job is small and stable. In each case the list is short, changes rarely, and the friction is the point. ## How to roll either one out safely SCPs have no dry-run mode of their own, so the safe sequence is: attach to a single throwaway account first, then to a sandbox OU, then widen. Watch CloudTrail for denials attributable to the SCP before promoting, and let it soak long enough to catch weekly and monthly jobs. And write the policy so a reader can tell why it exists — `Sid` values are one of the few pieces of documentation that travel with the policy.

  • Why does a region-lock SCP usually need a NotAction block?
    Because several AWS services are global and their API calls are made against a single endpoint region rather than your workload regions. Denying everything whose `aws:RequestedRegion` is not on your list would take IAM, STS, Organizations, Route 53, CloudFront and Support with it, which can lock you out of managing the account at all. Exempting those prefixes with `NotAction` keeps the global control plane usable.
  • How would you test a new SCP before applying it organization-wide?
    There is no dry-run for SCPs, so stage it: attach to one disposable account, then a sandbox OU, then widen. Watch CloudTrail for denials carrying the explicit-deny-in-a-service-control-policy wording, and give the change a soak period long enough to catch weekly and monthly jobs, which are exactly the workloads nobody remembers until they fail.
  • You need one break-glass role to be exempt from an organization-wide Deny. How do you write that?
    Add a `Condition` to the Deny using `aws:PrincipalArn` with a `StringNotEquals` or `ArnNotLike` operator naming the role, so the Deny applies to everyone except that principal. Keep the exemption to a single, named, heavily audited role — every exemption is a hole in the ceiling, and a wildcard-shaped one silently exempts far more than intended.

saying these in an interview costs you the question

  • Says an allow-list SCP works with FullAWSAccess still attached
  • Thinks a child OU can widen a parent's allow list
  • Region-locks everything without exempting global services
  • Believes SCP documents have no size or count limits
  • Treats allow lists as strictly better because they are stricter

context