skip to content

A role in account B is allowed s3:GetObject by both its own IAM policy and the bucket policy in account A, yet every download returns AccessDenied. The objects are encrypted with a customer managed AWS KMS key owned by account A. Why does the request still fail, and what has to change?

level: seniorimportance: should knowfreq 47%

answer

  1. two checks: the service and the key
  2. key policy is evaluated on its own
  3. cross-account needs both sides to allow
  4. look for a denied Decrypt in CloudTrail
  5. AWS managed keys cannot be shared

basics

~20 s

Reading KMS-encrypted data needs a second authorization — kms:Decrypt on the key — and a KMS key policy is evaluated independently of S3's policies. Account A's key policy does not name the role in account B, so the decrypt is denied and S3 surfaces AccessDenied.

solid answer

~50 s

There are two authorizations on that read, not one. S3 authorizes `s3:GetObject`, then calls KMS to decrypt the object's data key, and KMS authorizes `kms:Decrypt` against its own key policy. A KMS key policy is a resource policy that is evaluated on its own terms, so allowing the S3 action tells KMS nothing. Because this is cross-account, both sides must allow it: the key policy in account A has to name the role in account B (or account B's root, with account B's own IAM policy narrowing it), and the role in account B needs `kms:Decrypt` in its identity policy. The tell is in CloudTrail — you will see a denied `Decrypt` event on the key rather than a denied `GetObject`. Writes need the same treatment with `kms:GenerateDataKey`. And if the data were under the AWS managed key instead, this could not be fixed at all, because that key's policy is not editable.

code

json · 22 lines
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableAccountAdministration",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111111111111:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowPartnerRoleToDecryptViaS3",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::222222222222:role/report-reader" },
      "Action": ["kms:Decrypt", "kms:DescribeKey"],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" }
      }
    }
  ]
}

go deeper

for a junior

Recall that data encrypted with a KMS key needs permission on the key as well as on the service action, and that these are two separate policies. Knowing the name kms:Decrypt is most of what is expected here.

for a middle

Explain the request path: the service calls KMS on the caller's behalf, and the key policy is a resource policy evaluated independently. Be able to say which action is needed for reads versus writes.

for a senior

Debug it out loud — find the denied Decrypt in CloudTrail, apply the both-sides cross-account rule, and write a key policy statement scoped with a condition rather than a blanket account grant. Mention that AWS managed keys close the door entirely.

for a principal

Own the design consequence: which datasets get customer managed keys because sharing or independent revocation may be needed later, who administers key policies versus resource policies, and how you avoid a re-encryption project when a partner appears two years in.

## Two authorizations, not one When an object is encrypted under a KMS key, a read crosses two independent permission checks: 1. **The service action.** S3 decides whether the caller may perform `s3:GetObject` on that object, using the caller's identity policy and the bucket policy. 2. **The key action.** S3 then needs the object's data key in usable form, so it calls KMS on the caller's behalf. KMS decides whether that caller may perform `kms:Decrypt` on the key, using the *key policy* and the caller's identity policy. Either check can deny. The failure in the scenario is the second one, and it is confusing because the error S3 returns to the client is an ordinary `AccessDenied` — the same shape you get for a missing `s3:GetObject`. Engineers then re-read the bucket policy over and over, which is already correct. ## Why the key policy is not implied by the bucket policy A KMS key policy is the key's own resource policy, and unlike most resource policies it is *mandatory*: a key is never accessible through IAM alone unless its policy delegates to the account. That is what the default statement in a key policy does — it names the owning account's root principal with `kms:*`, which is what makes IAM policies in that account effective for the key. Nothing about that statement extends to another account, and nothing about S3 allowing an action makes KMS allow one. This separation is the feature, not an accident: it lets a security team hold an independent veto over a dataset that a data team administers. ## The cross-account both-sides rule Cross-account access to a KMS key requires an allow in the key's account *and* an allow in the caller's account. Neither alone is sufficient: ```json { "Sid": "AllowPartnerRoleToDecrypt", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:role/report-reader" }, "Action": ["kms:Decrypt", "kms:DescribeKey"], "Resource": "*", "Condition": { "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" } } } ``` That statement goes in the key policy in account A. `Resource: "*"` in a key policy means "this key" — it is not a wildcard over your account. The `kms:ViaService` condition is worth adding: it restricts the grant so the key can only be used through S3 in that Region, meaning the partner role cannot call `Decrypt` directly against arbitrary ciphertext it has obtained by other means. In account B, the role still needs `kms:Decrypt` on the key ARN in its identity policy. ## Diagnosing it quickly The fastest signal is CloudTrail on the key's side: a `Decrypt` event with an `AccessDenied` error code and the caller's role in `userIdentity`. If you see the `Decrypt` denial, stop reading the bucket policy. If you see no KMS event at all, the failure is earlier and the S3 layer is the problem after all. Newer AWS error messages often name the key ARN in the denial text, which shortens this considerably — but do not rely on the message, rely on the trail. ## Writes, and the AWS managed key trap The same structure applies to writing. A caller that puts an object into a KMS-encrypted bucket needs `kms:GenerateDataKey` on the key, because the data key for the new object has to be produced; multipart uploads typically need `kms:Decrypt` as well. A role with only `kms:Decrypt` can read but not write, which produces a puzzling half-working state. The hard stop is the AWS managed key. If the data were encrypted with the service's AWS managed key rather than a customer managed key, no cross-account fix exists: those keys have a key policy you cannot edit, so an external principal can never be authorized. This is the single most practical reason to choose a customer managed key up front for any dataset that might ever be shared — you cannot retrofit the decision without re-encrypting the data. ## The generalization worth stating The same pattern recurs everywhere KMS sits under a service: sharing an encrypted EBS snapshot with another account requires sharing the snapshot *and* granting on the key; a Lambda function reading a KMS-encrypted secret needs the key permission on its execution role; a replication or backup role that copies encrypted data needs permission on both the source and destination keys. Whenever a request touches encrypted data, ask which principal KMS sees and whether the key policy names it.

  • The same partner role can now read, but its uploads to the bucket fail. What is missing?
    Writing a KMS-encrypted object requires `kms:GenerateDataKey` on the key, because a fresh data key has to be produced for the new object; `kms:Decrypt` alone only covers reads. Multipart uploads generally need `kms:Decrypt` too. Add both actions in the key policy and in the caller's identity policy, keeping any `kms:ViaService` condition in place.
  • Why can't you solve this by encrypting the objects with the AWS managed KMS key for S3 instead?
    Because an AWS managed key's key policy is fixed and cannot be edited, so there is no way to name a principal from another account on it. Cross-account access to KMS-encrypted data requires a customer managed key. Retrofitting that means re-encrypting the data under a new key, which is why the choice matters at design time.
  • How would you keep the key policy from becoming a blanket cross-account allow?
    Name the specific role ARN rather than the partner account's root, limit the actions to `kms:Decrypt` and `kms:DescribeKey`, and add conditions — `kms:ViaService` to confine use to the intended service and Region, or `aws:PrincipalOrgID` when the caller is inside your own organization. Then confirm with CloudTrail that only the expected principal appears.

saying these in an interview costs you the question

  • Assumes the bucket policy grant implies permission on the KMS key
  • Thinks granting only in the caller's IAM policy is enough cross-account
  • Reads AccessDenied as always meaning the S3 action was missing
  • Believes AWS managed keys can be shared with another account
  • Grants kms:* to the partner account root to make it work

context