KMS.AccessDeniedException" when reading the CUR bucket from another account
Grants cross-account access to a KMS-encrypted CUR bucket when the reader role hits KMS.AccessDeniedException: the fix is the KMS key policy, not the S3 bucket policy. Use when GetObject on the CUR bucket fails with a KMS denial. Key trigger: the exact KMS.AccessDeniedException on CUR reads.
TL;DR: When the CUR bucket is encrypted with SSE-KMS, the S3 bucket policy is only half the permission: the reader role also needs decrypt rights in the KMS key policy, and key policies do not inherit bucket grants. Add the reader role to the key policy with decrypt and describe rights, then retry the read. The S3 Allow alone will keep failing until the key policy opens too.
The exact error on the read:
KMS.AccessDeniedException: User: [CUR-READER-ROLE-ARN] is not authorized to perform: kms:Decrypt on resource: [CUR-KMS-KEY-ARN]- Confirm the failure is at the key, not the bucket: the error names kms:Decrypt explicitly, which means S3 authorized the request and KMS denied it.
Expected: you stop editing the bucket policy, because it is not the problem.
- Open the KMS key policy in the owning account and add a statement granting the reader role decrypt and describe rights:
{
"Sid": "AllowCrossAccountCurRead",
"Effect": "Allow",
"Principal": { "AWS": "[READER-ROLE-ARN]" },
"Action": [ "kms:Decrypt", "kms:DescribeKey" ],
"Resource": "*"
}Expected: the key policy updates without syntax errors.
- Confirm the S3 bucket policy already allows the reader role GetObject on the CUR prefix, since both grants are required.
Expected: bucket Allow plus key Allow, the two halves of SSE-KMS access.
- Retry the read from the reader account:
aws s3 cp s3://[CUR-BUCKET]/[PREFIX]/manifest.json /tmp/manifest.json
Expected: the download succeeds instead of raising the KMS denial.
- If the reader is an Athena or Glue job rather than a human role, repeat the key-policy grant for the job's execution role, because each identity needs its own grant.
Expected: the downstream pipeline reads the CUR without KMS errors.
Use this when
- Reading a KMS-encrypted CUR bucket from another account fails with KMS.AccessDeniedException
- The S3 bucket policy looks correct but reads still fail
- An Athena, Glue, or QuickSight pipeline in account B cannot read the CUR written by account A
- The error text names kms:Decrypt on the key ARN
Not for this skill when
- The bucket uses SSE-S3 encryption, where no key policy exists and the problem is purely the bucket policy
- The error is S3 AccessDenied without a KMS mention, which is the bucket policy or an explicit deny
- Reads fail inside the same account, where the key policy usually already trusts the account and IAM policies suffice
Variant phrasings
- KMS AccessDeniedException reading CUR bucket cross account
- cannot read encrypted CUR from another account
- kms:Decrypt denied on billing bucket
- CUR SSE-KMS cross account access
Why it happens
SSE-KMS splits authorization across two independent policies. S3 checks the bucket policy, then calls KMS, which checks only the key policy plus the caller's IAM policy; a bucket Allow never implies a key Allow. Cross-account makes it visible because same-account key policies typically trust the whole account, hiding the second gate until the reader lives elsewhere.
Edge cases
- KMS grants are an alternative to key-policy edits for temporary access, but a grant the creator forgets to retire is a lingering permission; prefer the policy for durable pipelines
- The key policy change can take a few minutes to propagate, so an immediate retry can false-negative
- If the CUR writer and the key live in different accounts from the bucket, there are three policy surfaces to check, not two
- Re-encrypting the bucket with a new key orphans every existing grant, so key rotation needs a coordinated policy update
Provenance
Resolved from the public thread: https://vectle.com/posts/pstqqaWOiVPh7rdvSqQUdytg
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.