Cross-account trust and the confused deputy

Lesson 2 of 4 in Temporary Credentials.

Cross-account access is one of the densest areas of the exam, and almost all of it reduces to one rule plus one attack.

The both-sides rule. When a principal in account A reaches a resource in account B, AWS performs two evaluations — one in each account — and allows the request only if both return Allow. Account A is the trusted account, account B the trusting account. Concretely:

  • The resource in account B must have a resource-based policy (or role trust policy) naming the principal in account A.
  • The principal in account A must have an identity-based policy allowing the action on that resource's ARN.

Neither account can grant on the other's behalf, and a trust policy naming arn:aws:iam::111122223333:root does not remove the identity-policy requirement — it delegates the decision to account A's administrators, who still have to make it. This symmetry is why cross-account AccessDenied is diagnosed by checking both sides rather than hunting for one culprit.

Cross-account request: both evaluations must allow

  1. Principal in account A calls account B
  2. Evaluate in account A

    Identity-based policy on the principal, plus anything that limits it: permissions boundary, session policy, and the SCPs applying to account A.

  3. Evaluate in account B

    The resource-based policy on the target resource, plus anything that limits it: RCPs applying to account B, and — for KMS — the mandatory key policy.

  4. Denied: account A does not allow it
  5. Denied: account B does not allow it
  6. Allowed — both evaluations returned Allow

There are two ways to get a principal in account A onto a resource in account B, and choosing between them is a frequent exam decision.

Option 1 — a role in account B. Account A's principal assumes a role in B and operates as a principal of B. Use this when the work spans many resources or services, when you want the actions to appear in B's CloudTrail as B's own role, or when the target service has no resource-based policy at all. Plenty of services fall in that last category — EC2, RDS, Systems Manager, CloudWatch metrics and alarms, Auto Scaling — and for them a role is the only route. The supported set grows over time (DynamoDB gained table and index resource policies in 2024), so the durable skill is checking whether the service in the stem has one rather than memorising a frozen list.

Option 2 — a resource-based policy in account B. Account A's principal keeps its own identity and the resource policy admits it directly. This is available only for services that support resource policies: S3 buckets and access points, KMS keys, SQS queues, SNS topics, Lambda functions, Secrets Manager secrets, ECR repositories, EventBridge buses, CloudWatch Logs destinations, EFS file systems, Backup vaults, API Gateway REST APIs, and IAM role trust policies themselves. It is simpler — no assume-role step, no credential juggling — and it keeps the caller's own identity in the logs of both accounts.

The exam tell is the service. "Let the analytics account read this bucket" can be a bucket policy. "Let the analytics account run Athena queries, read the bucket, and write results" wants a role.

The confused deputy is the attack that lives in the gaps of the both-sides rule. A confused deputy is a privileged intermediary — a SaaS vendor's account, or an AWS service acting on your behalf — tricked into applying its legitimate authority for someone else's benefit. Two shapes, two defences.

Shape 1: a third-party account. You give a monitoring vendor a role in your account; its trust policy names the vendor's account. Another of the vendor's customers now configures your role ARN in their vendor console. The vendor, holding perfectly valid credentials and following its own code, assumes your role for them. Nothing was breached; the deputy was confused.

The defence is sts:ExternalId: a value the vendor generates per customer and must pass on every assume-role call, matched by a condition in your trust policy. Because the vendor issues it, one customer cannot guess another's, and because it is in your trust policy, a call without it fails.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::999988887777:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "a7f3c0d1-vendor-issued-per-customer" }
      }
    }
  ]
}

Shape 2: an AWS service principal. A service assumes a role in your account to do work — and the request context tells you which of its resources triggered it. Pin that with aws:SourceArn and aws:SourceAccount, so the service may act only when the triggering resource is yours:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "sns.amazonaws.com" },
      "Action": "sqs:SendMessage",
      "Resource": "arn:aws:sqs:eu-west-2:111122223333:alerts",
      "Condition": {
        "StringEquals": { "aws:SourceAccount": "111122223333" },
        "ArnLike": { "aws:SourceArn": "arn:aws:sns:eu-west-2:111122223333:topic/*" }
      }
    }
  ]
}

And at organization scale, aws:PrincipalOrgID replaces a list of account ids with "anyone in my organization" — a condition that keeps working as accounts come and go, and the starting point for a data perimeter.

Which confused-deputy guard belongs to which situation
SituationCondition keyWho supplies the valueWhere it goes

A third-party SaaS account assumes a role in your account

sts:ExternalId

The vendor, uniquely per customer

A StringEquals condition in your role’s trust policy

An AWS service principal assumes a role or writes to your resource on behalf of one of your resources

aws:SourceArn + aws:SourceAccount

You — the ARN of the triggering trail, bucket, topic, or trust anchor

The trust policy (for service roles) or the resource policy (for direct service writes)

Access should be limited to principals inside your organization

aws:PrincipalOrgID, or aws:PrincipalOrgPaths for an OU subtree

Your organization id or OU path

Resource policies, trust policies, and RCPs

A specific role, not a whole account, should be trusted

The Principal element itself, or aws:PrincipalArn

The exact role ARN

Name the role ARN as the principal rather than the account root

Key terms: Confused deputy, sts:ExternalId, aws:SourceAccount / aws:SourceArn, aws:PrincipalOrgID, Resource-based policy

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.