skip to content

A resource-based policy in AWS grants an AWS service principal such as s3.amazonaws.com or events.amazonaws.com permission to act on your resource. Why is that grant too broad as written, and which condition keys narrow it?

level: seniorimportance: should knowfreq 40%

answer

  1. the principal is not account-specific
  2. every customer's calls look the same
  3. pin what the service is acting for
  4. one key for the resource, one for the owner
  5. bucket ARNs carry no account id

basics

~20 s

An AWS service principal is the same identity for every customer, so granting it alone lets the service act on your resource on anyone's behalf. Narrow the grant with aws:SourceArn for the specific calling resource and aws:SourceAccount for the account that owns it.

solid answer

~50 s

`"Principal": {"Service": "s3.amazonaws.com"}` does not identify *your* S3 — it identifies S3, globally, for every AWS customer. So a statement granting it alone says "whenever S3 anywhere calls me on someone's behalf, allow it", which is a confused-deputy shape: another customer configures their bucket to target your topic or your function, and the service happily makes the call under that same principal. The fix is to constrain what the service is acting *for*. `aws:SourceArn` pins the exact calling resource — the bucket ARN, the rule ARN, the alarm ARN — and supports `ArnLike` wildcards when you need a family. `aws:SourceAccount` pins the account that owns the calling resource, which also covers cases where the source ARN is not predictable at policy-writing time. Use both where the service populates both, and prefer `ArnLike`/`ArnEquals` over string operators so ARN structure is respected.

code

json · 14 lines
json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowOnlyMyBucketToPublish",
    "Effect": "Allow",
    "Principal": { "Service": "s3.amazonaws.com" },
    "Action": "SNS:Publish",
    "Resource": "arn:aws:sns:eu-west-1:123456789012:object-events",
    "Condition": {
      "ArnLike": { "aws:SourceArn": "arn:aws:s3:::reports-prod" },
      "StringEquals": { "aws:SourceAccount": "123456789012" }
    }
  }]
}

go deeper

for a junior

Know that when an AWS service calls another service for you it authenticates as a service principal, and that this principal is not unique to your account.

for a middle

Explain what aws:SourceArn and aws:SourceAccount each pin, and use ArnLike or ArnEquals rather than string operators when comparing ARNs.

for a senior

Show why the unqualified grant is exploitable by another customer, and why adding IfExists to make an error disappear reintroduces exactly the hole you closed.

for a principal

Own the standard: every service-principal grant in the estate carries a source condition, enforced by review or automated policy checks rather than left to whoever wires the integration.

## The principal you granted is not yours When an AWS service calls another AWS service on your behalf, it authenticates as a **service principal** — a fixed name such as `s3.amazonaws.com`, `events.amazonaws.com`, `sns.amazonaws.com`, `cloudtrail.amazonaws.com`. That name is a property of the service, not of your account. Every customer's S3 calls out as `s3.amazonaws.com`. So the seemingly reasonable statement ```json { "Effect": "Allow", "Principal": { "Service": "s3.amazonaws.com" }, "Action": "SNS:Publish", "Resource": "arn:aws:sns:eu-west-1:123456789012:events" } ``` does not mean "my buckets may publish here". It means "S3 may publish here, whatever it is acting for". If someone else learns your topic ARN and points their bucket's notification configuration at it, the call arrives under exactly the principal you allowed. The service is the deputy; it has been confused about whose authority it is exercising. The direction of harm depends on the pairing. Sometimes it is unwanted inbound traffic and cost — a stranger's events filling your queue or invoking your function. Sometimes it is worse: a service that reads from a resource in your account under a grant an outsider can trigger. ## The narrowing keys **`aws:SourceArn`** carries the ARN of the resource on whose behalf the service is calling: the bucket that generated the notification, the EventBridge rule that matched, the CloudWatch alarm that fired. Comparing it to an ARN you own makes the grant specific. Use `ArnEquals` for one exact resource, or `ArnLike` with a wildcard for a family — for example `arn:aws:events:eu-west-1:123456789012:rule/prod-*`. Because you are matching an ARN, prefer the ARN operators over `StringEquals`, which is not aware of ARN structure and cannot express partial matches safely. **`aws:SourceAccount`** carries the account ID that owns the calling resource. It is the coarser guard, and it is the one you can always write even when the source ARN is not known in advance — a common situation when the source is created after the policy, or when many sources exist. It is also the guard that still works when a service populates the account but not a usable ARN. **`aws:SourceOrgID`** narrows to an entire AWS Organization, which is useful when the legitimate callers are many accounts under one org rather than one account. Use `aws:SourceArn` and `aws:SourceAccount` together where the service sends both. The combination survives the case where an ARN alone would be ambiguous: S3 bucket ARNs contain no account ID, so a bucket ARN by itself does not prove which account owns the bucket — adding `aws:SourceAccount` closes exactly that gap. That is the reason AWS's own guidance for S3-to-service grants recommends both. ## Getting the operator and the absence case right Two details decide whether the guard actually works. First, a service that does not populate the key leaves it absent, and a plain comparison against an absent key is false — which in an `Allow` means the legitimate call is refused, and you will find out fast. That failure direction is the safe one, but it is why you should test the happy path after tightening a policy rather than assuming. Second, do not reach for `...IfExists` to make the error go away. `StringEqualsIfExists` on `aws:SourceAccount` re-opens the hole for any caller that does not send the key, which is precisely the population you were guarding against. If a key is genuinely not sent by the service, use the other key, not a forgiving operator. ## Where you actually write this The statement lives wherever the service-principal grant lives: an SNS topic policy, an SQS queue policy, a Lambda function's resource policy (written by `lambda:AddPermission`, whose CLI form takes `--source-arn` and `--source-account` for exactly this reason), a KMS key policy, a Secrets Manager resource policy. Many consoles add the condition for you when you wire the integration through the UI; policies written by hand, by an early script, or copied from a blog post are the ones that tend to be missing it. ## What to say in the interview Name the shape — the service principal is shared across all customers — and then name the two keys and what each pins. If you can add why an S3 bucket ARN alone is insufficient, and why `IfExists` is the wrong repair, you have demonstrated that you understand the mechanism rather than having memorised a snippet.

  • Why add aws:SourceAccount when you have already pinned aws:SourceArn?
    Because some ARNs do not identify an account. An S3 bucket ARN has an empty account field, so matching it alone proves only that a bucket with that name called — and bucket names are guessable strings in a global namespace. `aws:SourceAccount` adds the ownership check the ARN cannot carry, which is why AWS recommends both for S3-sourced grants.
  • You tighten a policy with aws:SourceArn and the legitimate integration starts failing. What do you check?
    Whether the service actually populates that key for this call, and whether the ARN you wrote matches exactly — region, account field, resource path and case. If the key is genuinely not sent, switch to `aws:SourceAccount` rather than adding `IfExists`, which would re-open the grant to every caller that omits the key.
  • How would you scope a resource policy to every account in your AWS Organization instead of one account?
    Condition on `aws:PrincipalOrgID` equal to your organization ID when the caller is an IAM principal, or `aws:SourceOrgID` when an AWS service is calling on behalf of a resource in the org. Both beat enumerating account IDs, which drifts the moment an account is added or closed.

saying these in an interview costs you the question

  • Thinks s3.amazonaws.com means only their own buckets
  • Uses StringEquals on an ARN instead of ArnLike
  • Adds IfExists to silence a failing source condition
  • Believes an unpublished ARN is protection enough
  • Assumes the console always adds the condition for you

context