Cross-Account Access for a Contractor
A company has a workload account (prod) holding an S3 bucket with customer PII and an RDS instance. An external contracting firm needs read access to a specific prefix (reports/) in that bucket for 60 days. The contractors use their own AWS accounts and cannot be issued IAM users in the company's org. The security team requires: (a) no long-lived access keys, (b) the ability to revoke access immediately, (c) an audit trail of who read what, and (d) that contractors cannot read anything outside reports/. The bucket currently has Block Public Access fully enabled and default encryption with an SSE-KMS customer-managed key.
1. Design the access mechanism and name the specific controls that satisfy each of the four requirements.
Read the worked answer
Use cross-account IAM role assumption, not bucket ACLs and not access keys.
- (a) No long-lived keys: Create an IAM role in the prod account with a trust policy that allows the contractors' account(s) to assume it, optionally gated by an
ExternalIdand a condition onaws:PrincipalOrgIDif the firm is in a known org. Contractors callsts:AssumeRoleand receive temporary credentials. No keys are ever issued by the company. - (b) Immediate revocation: Revocation is deleting/editing the role's trust policy or attaching a deny - effective on the next
AssumeRole, and existing sessions can be cut by revoking the role or usingaws:TokenIssueTimeconditions. This is why a role beats a bucket policy granting a foreign principal directly: you can kill the trust in one place. - (c) Audit trail: CloudTrail data events on the bucket (S3 object-level logging) plus CloudTrail management events for
AssumeRole. Enable data events specifically for this bucket; they are not on by default and cost per event, which is acceptable for a 60-day scope. - (d) Prefix restriction: Scope the role's IAM policy to
arn:aws:s3:::bucket/reports/*fors3:GetObject, ands3:ListBucketwith ans3:prefixcondition. Because the bucket is in the same account as the role, this single identity policy is sufficient - no bucket policy change is needed. Do not weaken Block Public Access.
One more gate: the objects are SSE-KMS encrypted, so the role also needs kms:Decrypt on the key, and the key policy must allow the role to use it. Forgetting this is the most common failure - S3 access succeeds at the API layer and fails at decrypt.
2. Six weeks in, the contractor reports AccessDenied on some objects under `reports/` but not others. What is the most likely cause and how do you confirm it?
Read the worked answer
The most likely cause is the KMS key policy / grant, not S3. If the role can list and get some objects but not others, the differentiator is usually which key encrypted them. Common scenarios: some objects were written before a key rotation or by a different principal using a different key, or the key policy grants kms:Decrypt only to a narrower set of principals than the S3 policy does.
Confirm by triangulating:
- Check CloudTrail for the failing requests - look for
AccessDeniedonkms:Decryptversus ons3:GetObject. The event source tells you which layer denied. - Compare the
SSEKMSKeyIdof a working object and a failing object (head-object). If they differ, the key policy is the cause. - If they're the same key, check for an explicit
Denyin the bucket policy or an SCP on the role's account that excludes the prefix or the key.
Fix by adding the role as a principal with kms:Decrypt in the relevant key policy (or a grant), then re-test. The lesson: with SSE-KMS, authorization is the intersection of the S3 policy and the KMS policy - both must allow.