From sre-skills
Audits an AWS S3 bucket estate for genuine public or cross-account exposure by composing Block Public Access, bucket policy, ACL, and access points per bucket, avoiding false positives from neutralized policies.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sre-skills:s3-estate-calibration-auditorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Effective-exposure audit skill for an estate of AWS S3 buckets. Takes the config layers
FAILURE_MODES.mdfixtures/01-media-platform-clean/01-acme-media-thumbnails/bucket-acl.jsonfixtures/01-media-platform-clean/01-acme-media-thumbnails/meta.jsonfixtures/01-media-platform-clean/01-acme-media-thumbnails/public-access-block.jsonfixtures/01-media-platform-clean/02-acme-media-origin/bucket-acl.jsonfixtures/01-media-platform-clean/02-acme-media-origin/bucket-policy.jsonfixtures/01-media-platform-clean/02-acme-media-origin/meta.jsonfixtures/01-media-platform-clean/02-acme-media-origin/public-access-block.jsonfixtures/01-media-platform-clean/03-acme-media-shared-config/bucket-acl.jsonfixtures/01-media-platform-clean/03-acme-media-shared-config/bucket-policy.jsonfixtures/01-media-platform-clean/03-acme-media-shared-config/meta.jsonfixtures/01-media-platform-clean/03-acme-media-shared-config/public-access-block.jsonfixtures/01-media-platform-clean/04-acme-media-uploads/bucket-acl.jsonfixtures/01-media-platform-clean/04-acme-media-uploads/meta.jsonfixtures/01-media-platform-clean/04-acme-media-uploads/public-access-block.jsonfixtures/01-media-platform-clean/05-acme-media-transcode-tmp/bucket-acl.jsonfixtures/01-media-platform-clean/05-acme-media-transcode-tmp/meta.jsonfixtures/01-media-platform-clean/05-acme-media-transcode-tmp/public-access-block.jsonfixtures/01-media-platform-clean/06-acme-media-cdn-logs/bucket-acl.jsonfixtures/01-media-platform-clean/06-acme-media-cdn-logs/bucket-policy.jsonEffective-exposure audit skill for an estate of AWS S3 buckets. Takes the config layers
for every bucket in the estate (public-access-block.json, bucket-policy.json,
bucket-acl.json, and access-points.json where present), resolves each bucket's
effective verdict by composing all four layers, then answers one question a per-layer
read cannot: across 8-12 buckets that mostly READ as exposed, which one is genuinely
live, and is the estate otherwise clean. It returns the live bucket as the headline,
ranked by severity, with a fix, then names exactly where the bucket configs stop being
able to answer the question.
Effective S3 exposure is a join across four layers that each get read wrong one at a time. A public-looking bucket policy is inert under RestrictPublicBuckets; a cross-account grant survives BPA-all-on; a public-group ACL is dead under IgnorePublicAcls but a cross-account canonical user beside it is not; a clean bucket can still be public through an access point. In an estate, the trap doubles: several buckets carry a Principal '*' policy, an AllUsers ACL, or a wide-open look that is genuinely neutralised, and exactly one bucket carries a real live grant that reads just like its neutralised siblings. This skill composes the layers per bucket instead of clearing each layer in isolation, then calibrates the estate: it does not over-flag the neutralised baits, and it does not miss the buried needle.
It reads the static configuration of an estate of buckets: per bucket, any subset of
the Block Public Access booleans, the bucket policy, the bucket ACL, and the access
points. Those are S3 control-plane reads (get-public-access-block, get-bucket-policy,
get-bucket-acl, list-access-points + get-access-point-policy). That is the entire
input. The audit is correct and complete for what the bucket configs can tell you, and
it is explicit about the rest. Reachability-on-paper is not exposure-in-fact, and every
audit ends by naming the joins it cannot make:
A clean (deceptive-clean) estate still gets a boundary section, because a BPA-neutralised estate is not a proven-safe system: turning BPA off would expose the latent statements.
For every bucket in the estate, build the effective verdict by composing four layers. Never read one layer alone. A finding is LIVE only when a real public or cross-account grant survives the layer that would neutralise it:
The estate is clean iff no bucket is live. The needle is whichever bucket carries a live finding among many neutralised/scoped lookalikes.
Before any judgment, read each layer for each bucket. Process EVERY bucket in the estate, not the first couple:
public-access-block.json. An absent file means
all four are False (no BPA). The two that neutralise existing grants are
RestrictPublicBuckets / BlockPublicPolicy (for a public policy) and
IgnorePublicAcls (for a public-group ACL). BlockPublicAcls only blocks new public
ACLs and does not disable an existing one.Statement in bucket-policy.json. A Deny grants nothing and
cannot make a bucket public; classify only the Allow statements. For each Allow, read
the Principal (public '', a named AWS account, or '' with a Condition) and the
Condition.bucket-acl.json. A Grantee that is the AllUsers or
AuthenticatedUsers Group URI is a public-group grant; a CanonicalUser that is not the
bucket owner is a cross-account grant; an owner-only ACL produces nothing.access-points.json. Each AP has its OWN
PublicAccessBlock and Policy; resolve the AP policy exactly like a bucket policy,
against the AP's own BPA.Recognise the BPA switches by name, and recognise a narrowing Condition: aws:PrincipalOrgID,
aws:PrincipalOrgPaths, aws:PrincipalAccount, aws:PrincipalArn, aws:SourceArn,
aws:SourceAccount, aws:SourceVpc, aws:SourceVpce, aws:SourceIp, aws:VpcSourceIp,
sts:ExternalId, and the access-point delegation keys s3:DataAccessPointAccount /
s3:DataAccessPointArn / s3:AccessPointNetworkOrigin. Any of these on a Principal '*'
scopes it to a bounded caller set.
Read each bucket's BPA booleans verbatim — do not let a bucket's name or its grants tell
you what they are. Each boolean being true is the safe direction: IgnorePublicAcls: true means an existing public ACL is ignored (dead); RestrictPublicBuckets: true means a
public policy is denied. Do not invert it. The dominant miscalibration is asserting a boolean
value to fit an expectation: a bucket named exports, public, share, partner, or cdn,
or any bucket carrying an AllUsers / AuthenticatedUsers grant, invites the assumption that
it must be the exposed one — and that assumption makes you misread its IgnorePublicAcls as
false. In these estates the common case is the opposite: a shareable-sounding bucket with
an AllUsers grant and IgnorePublicAcls: true, which is neutralised, not live. The
presence of a grant is not evidence about the boolean. To keep the transcription faithful: for
any bucket carrying an AllUsers / AuthenticatedUsers ACL grant or a Principal '*' policy,
paste that bucket's entire public-access-block.json as a verbatim JSON block before you
classify it, and read the four booleans out of the pasted block. Pasting the raw object is
harder to get wrong than filling a value in from memory, which is where the misread creeps in.
Compose the layers; do not condemn a bucket on "Principal '*' is present" alone, and do not clear it on "BPA is on" alone. The codes:
These five are the only codes that count as live exposure. The next three READ as exposed but are NOT live, and must never be reported as a live public/exposed bucket:
On a NEEDLE estate, name the ONE genuinely live bucket as A (the) PRIMARY finding, with the reason it is live, instead of burying it among the neutralised lookalikes or missing it. The live bucket reads just like its neutralised/scoped siblings; the pass is naming exactly that one and why:
State the live bucket by name, the code, and the layer it is grounded in.
This is the half the naive read gets wrong in the other direction. An estate where every bucket is neutralised or scoped is CLEAN, and the audit must say so instead of manufacturing a finding. The composition in step 2 is what proves it. Specifically:
On a clean estate the audit reports: NO live exposure anywhere, why the exposed-looking buckets are neutralised or scoped (the BPA switch or the Condition), and the boundary. It does not invent a critical. Noting the neutralised statements as latent / defence-in-depth is fine; asserting live public exposure is not.
Order findings by severity (critical for a public policy or public access point, high for a cross-account or public-group grant). Rank the live needle as the headline; do NOT headline a neutralised/scoped bucket, and on a clean estate do not invent a critical. For each finding: the bucket and layer it is grounded in, what the exposure is, and the fix. Then list the boundary from "What this skill reads." A clean estate still gets a boundary section.
Fix the live bucket; do not rip out intentional scoped sharing or the BPA-neutralised buckets as if they were live leaks:
aws:PrincipalOrgID /
sts:ExternalId condition over a bare account root. For an ACL grant, express the sharing
as a scoped bucket policy and disable ACLs with Bucket Owner Enforced. BPA-all-on does not
make a cross-account bucket safe.| Severity | Meaning |
|---|---|
| critical | Live public exposure: a Principal '*' bucket policy with BPA not restricting (POLICY-PUBLIC), or a public access-point policy (AP-PUBLIC). |
| high | Live cross-account or public-group exposure that BPA does not close: cross-account policy (XACCT-POLICY), cross-account canonical-user ACL (XACCT-ACL), public-group ACL with IgnorePublicAcls off (ACL-PUBLIC). |
| low | A Principal '*' scoped by a Condition (COND-SCOPED): conditional sharing to verify, not public exposure. |
| info | A neutralised grant present but inert (POLICY-PUBLIC-BLOCKED, ACL-PUBLIC-IGNORED): latent risk if BPA is turned off, not live exposure. |
Only the critical and high bands are LIVE exposure and count toward the estate verdict. The low and info bands are exposed-looking-but-not-live; they are notes, never the headline.
| Code | Rule | Severity | Live | Grounded in |
|---|---|---|---|---|
| POLICY-PUBLIC | Bucket policy Principal '*', no Condition, BPA not restricting | critical | yes | bucket policy x BPA |
| AP-PUBLIC | Access-point policy Principal '*', AP BPA not restricting | critical | yes | access-point policy x AP BPA |
| XACCT-POLICY | Bucket policy grants a named other account | high | yes | bucket policy (BPA does not touch it) |
| XACCT-ACL | Bucket ACL grants a non-owner canonical user | high | yes | bucket ACL (IgnorePublicAcls does not touch it) |
| ACL-PUBLIC | Bucket ACL grants AllUsers / AuthenticatedUsers, IgnorePublicAcls off | high | yes | bucket ACL x BPA |
| COND-SCOPED | Principal '*' narrowed by a Condition | low | no | bucket policy Condition |
| POLICY-PUBLIC-BLOCKED | Principal '*' policy neutralised by RestrictPublicBuckets / BlockPublicPolicy | info | no | bucket policy x BPA |
| ACL-PUBLIC-IGNORED | Public-group ACL neutralised by IgnorePublicAcls | info | no | bucket ACL x BPA |
The matching half of every live rule is the clean verdict: POLICY-PUBLIC neutralised to POLICY-PUBLIC-BLOCKED, ACL-PUBLIC neutralised to ACL-PUBLIC-IGNORED, a Principal '*' scoped to COND-SCOPED, on an estate of these is the correct, complete output, not a failure to find something. Reporting a neutralised bucket as a live leak is the dominant failure mode this skill prevents.
The agent's final message in any invocation must include:
Seven end-to-end fixtures are committed under fixtures/, each an estate of 8-12 buckets
with a runnable replay test. The set is deliberately weighted toward deceptive-clean, because
over-flagging a neutralised estate is the cold agent's dominant failure here. No loud, obvious
public bucket appears: the base model already aces those.
05-logging-estate-needle: the needle. acme-log-shipping
has all four BPA switches on (reads as locked down) but its policy grants a named other
account read/list. A cross-account grant survives BPA: XACCT-POLICY (high), live.06-analytics-estate-needle: acme-analytics-clickstream
grants Principal '*' GetObject with NO Condition and BPA not restricting, sitting next to
siblings that carry an org id / ExternalId or have RestrictPublicBuckets on: POLICY-PUBLIC
(critical), live.07-partner-share-needle: acme-share-partner-drop
grants READ to another account's canonical user via its ACL; IgnorePublicAcls (on for this
estate) only ignores the AllUsers lookalikes beside it: XACCT-ACL (high), live.01-media-platform-clean: an AllUsers ACL, a Principal
'*' policy, org/IP-scoped policies, and an access-point delegation, all neutralised or scoped.
Clean.02-data-lake-clean: public-looking policies and a public
ACL, all neutralised by BPA or scoped by org-path / external-id. Clean.03-saas-tenancy-clean: tenant-shared buckets scoped by
org id / ExternalId, plus one ignored AllUsers ACL. Clean.04-backup-estate-clean: BPA-neutralised policies, an
ignored public ACL, and a SourceIp office allowlist. Clean.Every fixture has a replay test in tests/ that runs the methodology (via the deterministic
reference engine tests/_resolve.py, aggregated across the estate by tests/_estate.py)
against the committed JSON, with no external credentials. Run from the skill directory:
for t in tests/replay_*.py; do python "$t" || exit 1; done
The seven tests cover the three needle estates (the one live code fires, on the named bucket)
and the four deceptive-clean estates (no live finding fabricated). Tests exit non-zero if the
audit names the wrong bucket or invents one on a clean estate. See
tests/README.md for the fixture schema.
This skill is wrong in predictable ways. Read FAILURE_MODES.md before
relying on it. Highlights:
The audit above runs end-to-end against the bucket-config JSON the user already has. No Anyshift dependency.
Every boundary note in this skill is a join: bucket to its per-object ACLs, bucket to its CloudFront distribution, bucket to the identity policies of the principals it trusts, bucket to its VPC-endpoint policies and org SCPs, estate to the account-level BPA the neutralisation depends on. The Anyshift MCP can act as a context primer by resolving those joins from a versioned resource graph, so an XACCT-POLICY finding ("cross-account, real only if the trusted account is privileged or re-shares") can be closed instead of deferred at the boundary. A measured "with vs without" delta will be published here once the integration has been exercised against the replay fixtures.
npx claudepluginhub anyshift-io/claude-plugins --plugin sre-skillsReviews S3 data perimeter posture including Block Public Access, bucket policies, encryption, cross-account access, and prefix boundaries. Use for S3 exposure audits.
Identifies and remediates S3 bucket misconfigurations exposing data to unauthorized access. Covers Block Public Access, bucket policies, ACLs, encryption, access logging, and automated remediation via AWS Config and Lambda.
Identifies and remediates S3 bucket misconfigurations exposing data to unauthorized access. Covers Block Public Access, bucket policies, ACLs, encryption, access logging, and automated remediation via AWS Config and Lambda.