Cross-Account S3 Access Still Fails. Just Open It Up?
Scenario
A partner company needs to read files from one of your S3 buckets from a role in their own AWS account. You did the obvious thing: added a bucket policy allowing their role. They confirmed their IAM permissions look right on their side. And yet every GetObject still comes back AccessDenied. The detail that matters: the bucket's objects are encrypted with a customer-managed KMS key in your account — and that key's policy hasn't been touched. After a few rounds of failed attempts, patience runs out and someone proposes the nuclear option.
The Quick Fix on the Table
Turn off Block Public Access, make the bucket world-readable, and open the KMS key policy to all principals. If nothing can be denied, nothing will be denied — the partner finally gets the data and the ticket closes.
The quick fix is on the table and the room is waiting for your call. Would you sign off on it? Take a position and justify it — out loud or on paper — before revealing the analysis.
Why the Quick Fix Fails
- It publishes the data to the planet to serve one partner. A public bucket is readable by anyone, and automated scanners find newly public buckets within hours. The partner needed the files; so does everyone else now. This is the mechanism behind a large share of headline data breaches.
- A KMS key open to all principals stops being encryption. The key policy is the sole root of trust for a customer-managed key. Granting
kms:Decryptto"AWS": "*"means every AWS account on earth can decrypt anything under that key — and this key almost certainly protects more than this one bucket. - It fails compliance instantly and loudly. Public data plus an open CMK torches encryption-at-rest guarantees, contractual data-handling commitments, and whatever framework you're audited against. The partner integration would become the finding that dominates the next audit.
- It's a misdiagnosis dressed up as decisiveness. Cross-account access to encrypted objects requires three layers to agree: the caller's IAM policy, the bucket policy, and the KMS key policy. Two are done; the third was never touched. The error message is pointing straight at the missing link, and the "fix" bulldozes all three layers instead of adding one statement.
- It never gets undone. "We'll lock it back down after they've synced" competes with every other priority forever and loses. Openings created under frustration have a way of appearing, years later, in the incident report.
The interviewer nods: “Fine, the quick fix is off the table. So what exactly would you do — step by step?” Sketch your plan before revealing the approach.
The Right Approach
- Confirm the diagnosis first. Check CloudTrail in your account for the failing request: an
AccessDeniedon thekms:Decryptevent (rather than the S3GetObjectitself) confirms the key policy is the blocker. The IAM Policy Simulator or a testaws s3api get-objectfrom an assumed partner-role session pins it down within minutes. - Add the partner's role to the KMS key policy — narrowly. A statement granting exactly
kms:Decrypt(andkms:DescribeKeyfor some SDK flows) toarn:aws:iam::<partner-account>:role/<their-role>. Not the whole partner account root unless that's a deliberate choice; never a wildcard principal. - Tighten the bucket policy while you're in there. Scope it to the partner role's ARN, restrict to
s3:GetObject(pluss3:ListBucketif needed) on the specific prefix they should see — not the whole bucket if they only need one path. - Have the partner mirror it in their IAM. Cross-account KMS needs allows on both sides: their role's IAM policy must itself include
kms:Decrypton your key's ARN alongside the S3 permissions. This is the second most commonly forgotten link after the key policy. - Test the whole chain end to end. The partner assumes their role and runs
aws s3 cpagainst a known object; verify the success in your CloudTrail with their role in the caller identity. Then verify a request from a different principal still fails — proving you opened a door, not a wall. - Keep Block Public Access on and alarmed. BPA at both the bucket and account level stays enabled — it doesn't interfere with principal-scoped cross-account access. Add an alert (Config rule or IAM Access Analyzer) on any future policy change that widens the bucket or the key, so the nuclear option can't be deployed quietly next time someone's frustrated.
- Document the pattern. Write the three-layer checklist — caller IAM, bucket policy, key policy — into the runbook. The next cross-account request should take twenty minutes, not two days of guessing followed by a dangerous shortcut.
Final pushback: “Your plan costs more time and money than the quick fix. Convince me.” How do you defend your position under pressure?
How to Defend It
- "The AccessDenied isn't AWS being stubborn — it's telling us exactly which layer we forgot. The objects are KMS-encrypted, and the key policy was never updated. That's one JSON statement, not a redesign."
- "Opening the bucket and key to fix a partner integration is treating the third missing 'allow' by deleting all the 'denies' — including the ones protecting us from the entire internet."
- "A KMS key that allows all principals to decrypt isn't encryption anymore, it's a formality — and that key likely protects more data than just this bucket."
- "Newly public buckets get found by scanners in hours, not months. If we open this 'temporarily,' we should assume the data is copied before we get around to closing it."
- "The narrow fix is genuinely faster: one statement on the key, one on the bucket, one on their role, one end-to-end test. We can have the partner reading files today without ever loosening our posture."