Skip to main content
emnode / learn
Compliance

Lock down S3 bucket policies that hand sensitive actions to other AWS accounts

Security Hub S3.6 — a bucket policy that lets an outside account rewrite ACLs, encryption, or the policy itself is one statement away from data exfiltration; find and tighten them before someone else does.

12 min·10 sections·AWS

Last reviewed

Remediates AWS Security Hub: S3.6

Cross-account bucket policies: the basics

Why letting another account touch your bucket policy is the dangerous part

An S3 bucket policy is a resource policy attached directly to the bucket. Unlike an IAM policy, it can grant access to principals that live in completely different AWS accounts — arn:aws:iam::222233334444:root in a Principal block hands the named account a foothold in your bucket. That's a legitimate feature used for log aggregation, cross-account data sharing, and vendor integrations. The risk is in which actions you hand over.

Security Hub control S3.6 checks whether a general purpose bucket policy lets a principal in another AWS account perform any of five high-blast-radius actions: s3:DeleteBucketPolicy, s3:PutBucketAcl, s3:PutBucketPolicy, s3:PutEncryptionConfiguration, and s3:PutObjectAcl. The control fails if the policy Allows one or more of those actions for an external account. These are the actions that let an outsider rewrite the rules — delete your policy, flip an object's ACL to public, or change the encryption configuration out from under you.

It's flagged High severity because the actions on the list aren't read or write of data — they're control of the bucket's security posture. If an external account can call PutBucketPolicy, it can grant itself anything. If it can call PutObjectAcl, it can make individual objects world-readable. The finding is the gap between "we share this bucket with a partner" and "that partner can quietly reconfigure who else gets in."

In this lesson you'll learn how S3 bucket policies grant cross-account access, exactly which five administrative actions Security Hub S3.6 treats as off-limits for external principals, and why those actions matter far more than ordinary read/write. You'll see how to audit every bucket policy in an account, how to read a policy statement to spot an external Principal, and the safe remediation pattern — scope the principal, drop the dangerous actions, and add a confused-deputy guard — that keeps legitimate sharing working without handing over control of the bucket.

Fun fact

The wildcard principal that opened the door to the planet

A surprising number of real S3 exposures trace back not to a malicious actor but to a single overly broad statement: "Principal": "*" paired with an Allow on a PutObjectAcl or PutBucketPolicy action. The author meant "any principal in our partner account" and reached for a wildcard to save time. Because S3 evaluates "Principal": "*" as every AWS account on Earth, plus anonymous callers, that one shortcut converted a private data-sharing bucket into one any AWS user could reconfigure. S3.6 exists precisely because this class of mistake is so easy to make and so catastrophic when the granted action is administrative rather than a plain read.

Tightening a cross-account bucket policy in action

Dev is the platform engineer on call for the weekly security-finding review. Security Hub has raised an S3.6 finding on acme-partner-exports, a bucket the analytics team uses to hand monthly data files to an external partner. The policy Allows the partner account arn:aws:iam::222233334444:root on s3:GetObject, s3:PutObject — and s3:PutObjectAcl. That last one is why it failed: the partner can flip the ACL on any object they touch to public-read.

He pulls the live policy and reads it carefully. The partner genuinely needs GetObject to retrieve the exports, and the analytics pipeline writes the files, so the partner doesn't even need PutObject. The PutObjectAcl grant is pure leftover — copied from an old template where the partner used to upload. Nobody has called it in months, which he confirms from CloudTrail.

Dev rewrites the statement to grant the partner only s3:GetObject, scoped to the arn:aws:iam::222233334444:root principal and the specific export prefix, and adds an aws:SourceAccount condition as a confused-deputy guard. He applies it, re-runs the finding, and watches S3.6 flip to PASSED. The partner integration keeps working — they were only ever reading — and the bucket can no longer be reconfigured from outside. Total time: about eight minutes, most of it reading CloudTrail to confirm the action was unused.

First, pull the policy for a flagged bucket and look for an external account principal paired with one of the five sensitive actions.

$ aws s3api get-bucket-policy --bucket acme-partner-exports --query Policy --output text | jq '.Statement[]'
{
"Sid": "PartnerExportAccess",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::222233334444:root" },
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::acme-partner-exports/*"
}
# External account is granted s3:PutObjectAcl — this is why S3.6 fails.

An external Principal plus s3:PutObjectAcl lets the partner make objects public — exactly what S3.6 catches.

After rewriting the statement to grant only s3:GetObject (scoped + with a confused-deputy condition), confirm the control now passes.

$ aws securityhub get-findings --filters '{"GeneratorId":[{"Value":"security-control/S3.6","Comparison":"EQUALS"}],"ResourceId":[{"Value":"arn:aws:s3:::acme-partner-exports","Comparison":"EQUALS"}]}' --query 'Findings[0].Compliance.Status'
[
"PASSED"
]
# Partner can still read exports; it can no longer rewrite ACLs or the policy.

Least-privilege sharing — read-only access stays, administrative actions are gone, and the finding clears.

S3.6 under the hooddeep dive

S3.6 is backed by the AWS Config managed rule s3-bucket-blacklisted-actions-prohibited. The rule evaluates each general purpose bucket's policy and fails if the policy Allows any principal from a different AWS account to perform one of the actions in the blacklistedactionpatterns parameter. For this control that parameter is fixed and not customizable: s3:DeleteBucketPolicy, s3:PutBucketAcl, s3:PutBucketPolicy, s3:PutEncryptionConfiguration, and s3:PutObjectAcl. Any action not on that list is allowed to be granted cross-account without failing the rule — the control is deliberately narrow, targeting only the actions that let an outsider change the bucket's security posture.

The evaluation is change-triggered, so it re-runs whenever the bucket policy is modified, and "another AWS account" is determined by comparing the account in the Principal ARN to the bucket-owning account. A "Principal": "*" or a wildcard account ARN counts as external and will fail if it's allowed any blacklisted action. Note the control inspects the bucket policy specifically — it doesn't evaluate IAM identity policies in the other account, ACLs, or access points; those are covered by sibling controls (S3.12 for ACLs, S3.19 for access points).

The right remediation is least privilege: keep the external principal explicit (a specific account ARN or, better, a specific role ARN — never *), grant only the data actions the integration actually uses, and remove every administrative action from the cross-account statement. For partner integrations, add an aws:SourceAccount or aws:SourceArn condition to defend against the confused-deputy problem, where a third service is tricked into using its own permissions on your bucket.

# List every bucket, then dump each policy and flag the five S3.6 actions on external grants.
BLACKLIST='s3:DeleteBucketPolicy|s3:PutBucketAcl|s3:PutBucketPolicy|s3:PutEncryptionConfiguration|s3:PutObjectAcl'

for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  POLICY=$(aws s3api get-bucket-policy --bucket "$b" --query Policy --output text 2>/dev/null) || continue
  if echo "$POLICY" | grep -Eq "$BLACKLIST"; then
    echo "==> $b grants a blacklisted action; inspect the Principal:"
    echo "$POLICY" | jq '.Statement[] | select(.Effect=="Allow") | {Principal, Action}'
  fi
done

# For any hit, confirm the Principal is an external account (not your own / not a service principal),
# then rewrite the statement to drop the administrative actions and re-scope the principal.
aws s3api put-bucket-policy --bucket acme-partner-exports --policy file://tightened-policy.json

What is the impact of cross-account access to sensitive bucket actions?

The direct impact is loss of control over your own data store. If an external account can call s3:PutBucketPolicy, it can append a statement granting itself — or anyone — full access, and your original guardrails evaporate. s3:DeleteBucketPolicy lets it strip the policy entirely, falling back to whatever default access the account and ACLs permit. s3:PutObjectAcl and s3:PutBucketAcl let it set objects or the bucket to public-read. s3:PutEncryptionConfiguration lets it change or weaken encryption. Every one of these is a path from "shared" to "compromised" without the data owner doing anything wrong.

The second-order impact is data exfiltration by an insider or an attacker who has compromised the external account. AWS itself frames this control around least privilege: if a bucket policy allows access from external accounts, a malicious or careless party on the other side can exfiltrate data or open it to the world. Because the granted actions are administrative, the blast radius isn't one file — it's the whole bucket's access model, and the change can be made quietly and persist until the next audit catches it.

There's a compliance and audit impact too. S3.6 maps to NIST 800-53 controls around least privilege and configuration management (CA-9(1), CM-2) and NIST 800-171 boundary protection. A failed S3.6 on a bucket holding regulated data is the kind of finding that turns up in audit reports, breach disclosures, and customer security questionnaires. The cost when it goes wrong isn't a storage line item — it's incident response, regulatory penalties, mandatory notification, and reputational damage, any of which can be orders of magnitude larger than the entire cloud bill.

Finally, this category is a discipline signal. A bucket that over-grants cross-account administrative actions almost always got there by copy-pasting a template, using a * principal to save time, or never revisiting a grant after the project that needed it ended. Where you find one, you tend to find a pattern: sharing set up in a hurry and never reviewed. Cleaning up the finding is the small part; closing the gap that produced it is the durable win.

How do you remediate an S3.6 finding safely?

Remediation is a four-step loop: inventory which buckets grant the dangerous actions cross-account, confirm what the external party actually needs, rewrite to least privilege without breaking the integration, and add a guardrail so the gap doesn't reopen.

1. Inventory every bucket policy that grants a blacklisted action externally

Pull every bucket policy in the account and search each one for the five S3.6 actions — s3:DeleteBucketPolicy, s3:PutBucketAcl, s3:PutBucketPolicy, s3:PutEncryptionConfiguration, s3:PutObjectAcl — granted with Effect: Allow to a principal in another account or to *. Tag each hit with the data sensitivity of the bucket; that's what sets remediation priority. A * principal on any of these is the highest urgency regardless of contents, because it's open to the whole internet.

2. Confirm what the external party actually uses before changing anything

Don't strip permissions blind — check CloudTrail data events to see which actions the external account has actually called over the last 90 days. Most over-grants are leftovers from a template or an earlier phase of the integration and have never been invoked. Where an administrative action genuinely is in use (rare, but it happens with some backup or replication tooling), that's a conversation with the owner about whether a different, scoped mechanism fits better — not a silent removal.

3. Rewrite the statement to least privilege

Replace any * principal with the specific external account or, better, a specific role ARN. Remove every blacklisted administrative action from the cross-account statement, keeping only the data actions the integration needs (typically s3:GetObject, sometimes s3:PutObject). Scope the Resource to the exact prefix in use rather than the whole bucket. Apply the new policy and re-run the S3.6 finding to confirm it flips to PASSED.

4. Add a confused-deputy guard and a recurring check

For partner and service integrations, add an aws:SourceAccount or aws:SourceArn condition so the bucket can only be reached on behalf of the intended account, closing the confused-deputy gap. Then make S3.6 a standing item: leave the AWS Config rule enabled so any future policy change re-triggers the check, and route new High-severity findings into the security review with an SLA. The rewrite fixes today's finding; the guardrail and the cadence stop the next one.

# tightened-policy.json: external account keeps read-only, all admin actions removed,
# principal scoped to a role, and a confused-deputy guard added.
cat > tightened-policy.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PartnerExportReadOnly",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::222233334444:role/acme-export-reader" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::acme-partner-exports/exports/*",
      "Condition": { "StringEquals": { "aws:SourceAccount": "222233334444" } }
    }
  ]
}
JSON

aws s3api put-bucket-policy --bucket acme-partner-exports --policy file://tightened-policy.json

# Verify no blacklisted action survives in the cross-account grant.
aws s3api get-bucket-policy --bucket acme-partner-exports --query Policy --output text \
  | grep -E 's3:DeleteBucketPolicy|s3:PutBucketAcl|s3:PutBucketPolicy|s3:PutEncryptionConfiguration|s3:PutObjectAcl' \
  && echo 'STILL FAILING' || echo 'clean'

Quick quiz

Question 1 of 5

A bucket policy grants an external partner account s3:GetObject and s3:PutObjectAcl on your exports bucket. CloudTrail shows the partner has only ever called GetObject. What's the right remediation?

You've completed Lock down S3 bucket policies that hand sensitive actions to other AWS accounts. You now know the five administrative actions Security Hub S3.6 forbids granting cross-account, why control of the bucket's posture is far more dangerous than read/write of its data, and the safe four-step loop — inventory, confirm usage, rewrite to least privilege, add a guardrail — that clears the finding without breaking a legitimate integration. The next time S3.6 fires, you'll have a defensible path from "flagged" to "PASSED" in under ten minutes.

Back to the library