---
id: 34
title: "A resource-free 'bouncer' account: the single gateway to customer resources"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [security, infra, cloud, aws, multi-tenancy]
references:
  - id: resource-org-id
    title: "IAM condition key: aws:ResourceOrgID"
    url: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
    abstract: "An IAM policy condition key holding the AWS Organizations ID that owns the resource a request targets. Comparing it to your own organization's ID lets a policy allow or deny based on whether the target is inside or outside your org — the basis for a deny-inward rule."
summary: "Funnel access to customer resources through one dedicated account that holds no resources. Customer trust names only its role; the role is denied from your own org (aws:ResourceOrgID). Fenced both ways, it shrinks the confused-deputy surface to one audited gateway."
supersedes: null
superseded_by: null
aliases: []
---

## TL;DR

Route **all** access to customer resources — and anything reachable from the outside that can touch them
— through roles in a **single dedicated cloud account that holds no resources of its own**: a "bouncer"
account. Make it the *only* principal that customer-side trust allows in, and **fence it both ways**:

- **Inbound:** every customer-resource trust policy trusts **only** the bouncer role (conditioned with
  `SourceArn` / `SourceAccount` / `ExternalId` and owner-pinning, per [ZFN-10](/zfn/10-verify-resource-owner/)).
  No other account or workload can reach customer resources directly.
- **Outbound:** the bouncer role's own policy explicitly **denies** acting on resources inside *your* org
  ([`aws:ResourceOrgID`](ref:resource-org-id) equal to yours), with a tight allowlist for the few internal
  targets it genuinely needs. So even a tricked or compromised bouncer can only ever reach *outward*.

Because the account has no resources, there's nothing in it to compromise, run code on, or pivot through —
it's a pure, audited identity hop. This collapses the **confused-deputy** surface to one narrow, governed
gateway with a single audit trail.

## Context

Any workload that holds credentials able to reach a customer's resources is a confused-deputy risk
([ZFN-10](/zfn/10-verify-resource-owner/)): attacker-influenced input can trick it into acting on the
*wrong* customer, and a compromise of it inherits its standing access to customer data. If *many* of your
workloads can assume into customer resources directly, the trust surface is enormous and impossible to
reason about — every workload is a potential deputy, every trust policy is a thing to get right, and a
foothold anywhere is a foothold toward customer data.

Cross-account `AssumeRole` is the right primitive; pointing it at customer resources from *everywhere* is
the wrong topology. You want the opposite: **one** place allowed to touch customer resources, controlled
and audited to the hilt, with the rest of your estate unable to reach customers except through it. The
cleanest way to express "one controlled principal" is a dedicated account whose only job is to hold that
principal — and to hold *nothing else*, so there's nothing in it to attack.

## Recommendation

**Make one resource-free account the sole gateway to customer resources, fenced in both directions.**

- **Customer-side trust names only the bouncer role.** Every trust relationship granting access to a
  customer resource — the role you assume for a customer, the resource policy on a customer-touching
  resource — trusts **only** a role in the bouncer account, conditioned per
  [ZFN-10](/zfn/10-verify-resource-owner/). No other account or workload appears in that trust; adding
  one is a reviewed exception, not a local choice.

- **Workloads reach customers only through the bouncer.** A workload that needs a customer resource
  assumes (or calls a narrow service that assumes) the bouncer role, which performs the access on its
  behalf. The bouncer is the one deputy: minimal, narrowly scoped, and its assumption tightly gated.

- **The account holds no resources — and enforce it.** No compute, no data, no queues, no secrets beyond
  what the gateway role needs. A Service Control Policy denies creating resources, so "no resources" is
  structural, not a convention. An account with nothing in it has nothing to exfiltrate, nothing to run
  attacker code on, and nothing to pivot through.

- **Deny the bouncer from acting on your own org (defense in depth).** Its job is to reach *outside*; it
  must never be usable against your own organization. The role's identity policy carries an explicit
  **deny** on any operation whose target is inside your org ([`aws:ResourceOrgID`](ref:resource-org-id)
  equals yours):

  ```json
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AllowPutObjectAnywhere",
        "Effect": "Allow",
        "Action": "s3:PutObject",
        "Resource": "arn:aws:s3:::*/*"
      },
      {
        "Sid": "DenyPutObjectInsideOurOrg",
        "Effect": "Deny",
        "Action": "s3:PutObject",
        "Resource": "arn:aws:s3:::*/*",
        "Condition": {
          "StringEquals": { "aws:ResourceOrgID": "o-xxxxxxxxxx" }
        }
      }
    ]
  }
  ```

  Allow broadly to the outside, then **deny whenever `aws:ResourceOrgID` is yours**. (If the gateway
  genuinely needs a specific internal target, carve it out narrowly with `NotResource` — kept tight and
  documented — but default to no carve-out.) Because an explicit `Deny` always wins, this holds even if a
  broader allow is added later — the inward block can't be accidentally undone by a future grant. Apply the same
  allow-out / deny-inward shape to *every* action the role can perform, not just `s3:PutObject`.

- **Assume the bouncer via federated identity, not static keys** ([ZFN-9](/zfn/9-no-long-lived-cloud-keys/)),
  and keep applying the per-call owner-pinning of [ZFN-10](/zfn/10-verify-resource-owner/) at the
  bouncer→customer call — the gateway concentrates the deputy; it doesn't excuse the per-call checks.

## Consequences

**Easier:**

- The confused-deputy and cross-tenant blast radius collapses to one governed gateway: exactly one
  principal can reach customer resources, and one account to harden.
- Customer-side trust policies are simple and uniform (they trust one principal), and easier to audit.
- All customer-resource access flows through one account — a single audit trail where every access is
  attributable.
- A resource-free account has no blast radius of its own: compromising it grants only the conditioned,
  audited ability to make the gateway calls, not a foothold with data or compute.
- Fenced both ways, the bouncer can neither be reached from your estate nor turned back against it.

**Harder:**

- An extra hop: workloads chain through the bouncer instead of calling customer resources directly —
  latency, an assume-role step, and a shared library/service to do it well.
- The bouncer role and account become a **critical control point** — high-value, on the availability path
  for all customer access; its permissions and assumption conditions need tight governance and review.
- "No resources" and the deny-inward allowlist must be actively enforced and policed; the temptation to
  "just put one small thing here" or "just allow this one internal write" has to be refused, or the
  property erodes.

## References

- [ZFN-10](/zfn/10-verify-resource-owner/) — the confused-deputy defense this account structurally
  concentrates; per-call owner-pinning and trust conditions still apply at the gateway.
- [ZFN-9](/zfn/9-no-long-lived-cloud-keys/) — assume the bouncer role via federated identity, not static
  keys.
- [ZFN-2](/zfn/2-engineering-priority-ordering/) — security-first ordering that justifies the extra hop
  and the dedicated account.
- [`aws:ResourceOrgID`](ref:resource-org-id) and AWS Organizations / SCP multi-account guidance.

## Changelog

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