---
id: 55
title: "On AWS, the account is the unit of isolation"
kind: note
status: current
date: 2026-08-12
authors:
  - "Theo Zourzouvillys"
tags: [security, infra, cloud, aws]
references:
  - id: multiaccount
    title: "Organizing Your AWS Environment Using Multiple Accounts (AWS whitepaper)"
    url: https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/organizing-your-aws-environment.html
    abstract: "AWS's own guidance on multi-account architecture: accounts as the boundary for security, billing, and service quotas, organised into an Organization with organizational units, and the recommended baseline accounts (management, log archive, security tooling) that every environment should start with."
  - id: scps
    title: "Service control policies (AWS Organizations)"
    url: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html
    abstract: "Organization-level policies that set the maximum available permissions for every principal in an account, including its root user. The mechanism that turns an account from a default-open container into one with organization-enforced guardrails no local admin can lift."
  - id: rcps
    title: "Resource control policies (AWS Organizations)"
    url: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_rcps.html
    abstract: "The resource-side twin of SCPs: organization-level policies that cap what any principal — including external ones — can do with resources in your accounts. The primitive that makes 'only our organization can touch this bucket' a fleet-wide invariant rather than a per-bucket policy."
summary: "IAM inside one account is access control; the account boundary is isolation. Quotas, billing, credential scope, and blast radius are all account-shaped, and cross-account access fails closed. One workload per account, guardrailed by SCPs and RCPs — plumbing friction is the point."
supersedes: null
superseded_by: null
aliases: []
crossrefs:
  ZFN-42: "The conviction this note builds on: all-in on AWS. Going all-in is what makes it worth learning the one isolation primitive AWS itself treats as hard."
  ZFN-34: "The bouncer-account pattern is this note applied to third-party access: a whole account whose only job is to be the fenced place where customer trust terminates."
  ZFN-9: "Federated, short-lived credentials are what make many accounts liveable — role assumption is the same motion whether you have three accounts or three hundred."
  ZFN-10: "Once everything is cross-account, every call needs the expected owner pinned — account isolation and confused-deputy defence are two halves of one design."
---

## TL;DR

**Within a single AWS account, IAM is access control. Between accounts, it's isolation. Those are
different things, and you need both.** A policy bug inside an account fails *open* — some
principal can reach some resource you never intended, found later by an auditor or an attacker. A
missing grant across an account boundary fails *closed* — the access simply doesn't exist until
both sides say so.

So partition by account, aggressively:

- **One workload per account** — a service (or small cohesive family of them) × environment.
  Production and non-production **never** share an account, for anything.
- **Baseline accounts** with one job each: management (touched rarely, by almost nobody), log
  archive (write-only from everywhere else), security tooling, and a
  [bouncer for external trust](/zfn/34-resource-free-bouncer-account/).
- **Organization guardrails** that member accounts can't lift: [SCPs](ref:scps) capping what
  principals may do, [RCPs](ref:rcps) capping what may be done to resources, deny-by-default on
  regions and services you don't use.
- **An account factory**, so a new account is a vending-machine operation with the baseline
  stamped in — not a quarterly ceremony.

If creating an account feels too heavyweight to do per workload, fix the factory, not the
architecture.

## Context

Everything that actually limits damage on AWS is account-shaped:

- **Credential scope.** A leaked credential, an SSRF'd instance role
  ([ZFN-11](/zfn/11-outbound-http-egress-proxy/)), a compromised CI runner — the theft is bounded
  by what that account can reach. In a shared account, "what it can reach" is whatever IAM
  gymnastics were supposed to prevent, and IAM at scale accumulates the same sediment as any
  other policy language: wildcard ARNs from a deadline, a `*` in a resource field nobody
  revisited, tag-based conditions half-applied. In a small single-workload account you can allow
  broadly and still sleep, because the *account* is the fence.
- **Service quotas.** Quotas are per-account per-region. A load test in a shared account eats the
  API rate limits, ENI counts, and instance capacity that production needed — self-inflicted
  denial of service with no attacker involved. Separate accounts make the noisy neighbour
  structurally impossible ([ZFN-18](/zfn/18-enforce-quotas-at-ingress/) is the same instinct,
  applied to your own tenants).
- **Blast radius of change.** Terraform/OpenTofu state, deletion of "unused" resources, a
  misdirected cleanup script — the worst case is the account, not the company.
- **Billing and attribution.** Cost by account is exact and free; cost by tag is a forensic
  reconstruction that's never quite right.
- **Forensics and containment.** When an account is compromised, you can freeze *it* — snapshot,
  detach, quarantine with an SCP — without touching production's twin. Try that when both live in
  one account.

AWS says this themselves, in the [multi-account whitepaper](ref:multiaccount): the account is the
boundary they build to. Fighting it — one big account, isolation by IAM condition keys and prayer
— means hand-building, in the world's most detail-hostile policy language, a property the
platform would have given you for free.

> [!aside]
>
> The tell that a shared account has crossed the line is the IAM policy with a comment in it.
> The moment a policy needs prose to explain which of three co-tenant services it's really
> scoping, you're maintaining a hand-rolled hypervisor out of JSON. Accounts are the managed
> version of that hypervisor.

## Recommendation

**Make the account the default answer to "how do we separate these?" — and make accounts cheap
enough that the default is painless.**

- **One workload × one environment = one account.** A "workload" is a service or a small family
  that deploys and fails together. When in doubt, split — the cost of a boundary you didn't need
  is some cross-account plumbing; the cost of a boundary you needed and don't have is a shared
  fate you can't untangle.

- **Never share prod with anything.** Not staging ("it's basically prod"), not tooling, not the
  one convenient Lambda. Every prod incident story that starts in a non-prod system walked
  through a shared account or a shared credential.

- **Stamp the baseline from a factory.** New accounts arrive with the org's guardrails, the
  log-archive trail already flowing, break-glass access configured, federation wired
  ([ZFN-9](/zfn/9-no-long-lived-cloud-keys/)), and no root credentials in circulation. If this
  takes more than minutes, invest there first — factory friction is why teams share accounts.

- **Guardrails live above the account.** [SCPs](ref:scps) for "no principal here may leave these
  regions / touch these services / disable the audit trail"; [RCPs](ref:rcps) for "no resource
  here may be touched from outside the organization" — which closes, fleet-wide, the
  confused-deputy paths that per-resource policies close one bucket at a time
  ([ZFN-10](/zfn/10-verify-resource-owner/), [ZFN-34](/zfn/34-resource-free-bouncer-account/)).
  The point of enforcing at the organization is that a compromised account admin can't lift the
  rules from inside.

- **Cross-account access is explicit, and that's the feature.** Every legitimate flow between
  workloads becomes a named role with a named trust policy — an inventory of your real internal
  trust graph that you can read, review, and revoke. Inside one account that graph exists too;
  it's just invisible.

- **The extreme tier: account-per-tenant.** For customers whose regulatory or contractual
  isolation needs outgrow row-level tenancy
  ([ZFN-15](/zfn/15-partition-customer-data-by-tenant/)), the account is the honest version of
  "dedicated environment" — and if your factory is real, it's an attainable SKU rather than a
  bespoke project.

## Consequences

**Easier:**

- **IAM policies get simpler and safer simultaneously.** Broad grants within a small account beat
  surgical grants within a huge one — less policy code, fewer places to be wrong.
- **Compromise containment is a boundary you already drew.** Freeze the account, keep the fleet.
- **Cost, quotas, and ownership become facts** (the account says so) rather than reconstructions
  (the tags suggest so).
- **Deleting things becomes possible.** Decommissioning a workload is closing an account — a
  clean, total operation — instead of an archaeology project in a shared one.

**Harder:**

- **Everything is cross-account now.** Role hops, resource shares, peering/PrivateLink where
  networks must meet, endpoint policies. The plumbing is real work, and it's on every path that
  used to be a same-account convenience.
- **You must invest in the factory and the org layer** — account vending, SCP/RCP hygiene,
  centralised identity — before the pattern pays. Half-adopted, you get sprawl without
  guardrails, which is worse than one well-run shared account.
- **Some limits are organizational, not per-account** — org-level API rates, policy size and
  attachment caps — and hundreds of accounts will find them.
- **Local development gets a hop harder**: engineers hold federated access to many accounts, and
  tooling has to make "which account am I in?" impossible to get wrong
  ([ZFN-40](/zfn/40-no-anonymous-system-actor/) applies to humans too).

**New obligations:**

- **The account inventory is now a security document.** Every account has an owner, a purpose,
  and a lifecycle; an unowned account is an unwatched credential scope.
- **The trust graph needs review.** Cross-account roles are legible — so read them, periodically,
  and delete the stale ones.

## References

- [ZFN-42](/zfn/42-my-one-cloud-is-aws/) — the all-in-on-AWS conviction that makes deep use of
  the account primitive rational rather than lock-in anxiety.
- [ZFN-34](/zfn/34-resource-free-bouncer-account/) — a whole account whose only job is to
  terminate external trust: this note's pattern at its sharpest.
- [ZFN-9](/zfn/9-no-long-lived-cloud-keys/) — federation and short-lived credentials, which make
  a many-account world navigable without a key under every doormat.
- [ZFN-10](/zfn/10-verify-resource-owner/) — pinning the expected owner on cross-account calls;
  mandatory once account boundaries are your architecture.
- [ZFN-15](/zfn/15-partition-customer-data-by-tenant/) — the same partition-first instinct one
  level down, inside the database.
- [AWS multi-account whitepaper](ref:multiaccount), [SCPs](ref:scps), [RCPs](ref:rcps) — the
  platform's own statement that the account is the boundary, and the two policy layers that
  enforce it from above.

## Changelog

- **2026-08-12**: First published as a Field Note.
