Commit d6b8faec53

d6b8faec53c9eedaec1fe02984da223af96b0331

parent: de23fd8882

Unregistered key

cmc <hello@cleberg.net> · 2026-03-04 00:55 UTC
committer: <noreply@github.com>

publish new post: auditing-aws-s3 (#17)

* publish new post: auditing-aws-s3

* format latest post and remove some unnecessary wording

Layout: unified · split

content/blog/2026-03-03-auditing-aws-s3.org added +235
@@ -0,0 +1,235 @@
1#+date: [2026-03-03 Tue 18:50:23]
2#+title: Auditing AWS S3 Buckets
3#+description: Learn how to audit AWS S3 buckets for public access.
4#+slug: auditing-aws-s3
5
6This is the latest in my series of posts on auditing AWS, a cloud platform that
7has existed for around two decades but can still be a mystery to auditors who
8aren't familiar with how cloud platforms operate.
9
10One of the older, and most popular, offerings from AWS is Simple Storage Service
11(S3), a scalable object storage service that can hold any type of data. With
12this ease of use comes risk.
13
14Public S3 buckets are one of the most common and highest-profile AWS
15misconfigurations. The challenge for auditors is that "public" in S3 isn't a
16single setting. It's the combination of three separate controls, and a bucket
17can appear restricted at one layer while still being exposed at another.
18
19This post walks through a script that checks all three layers for every bucket
20in the account and produces a CSV report.
21
22The script used in this post is available at [[https://github.com/audit-labs/audit-tools/blob/main/applications/aws/aws_s3_buckets.sh][audit-labs/audit-tools]].
23
24* Background: How S3 Controls Public Access
25
26Let's start with the basics. Before running anything, it helps to understand the
27three layers the script checks and how they interact.
28
29** Public Access Block
30
31Public Access Block (PAB) is a set of four flags that can be applied at the
32account level, the bucket level, or both. When all four are enabled, they
33override any bucket policy or ACL that would otherwise grant public access.
34
35The four flags are:
36
37- ~BlockPublicAcls~: Prevents new ACLs that grant public access and ignores
38 existing ones.
39- ~IgnorePublicAcls~: Ignores all public ACLs on the bucket.
40- ~BlockPublicPolicy~: Prevents bucket policies that grant public access.
41- ~RestrictPublicBuckets~: Restricts access to buckets with public policies to
42 only AWS services and authorized users within the account.
43
44The script checks whether all four flags are enabled at the bucket level. If any
45one of them is missing or disabled, the bucket is marked ~FALSE-VULNERABLE~. If
46PAB is missing entirely (no configuration exists at all), the bucket is marked
47~CRITICAL-MISSING~, which is the highest-risk state.
48
49#+begin_note
50*Note:* While this script checks the bucket level PAB, an account-level PAB may
51 exist as well. If it's enabled at the account level, the bucket is safe
52 regardless of its individual settings.
53#+end_note
54
55** Bucket Policy Status
56
57AWS evaluates each bucket policy and exposes an ~IsPublic~ flag that reflects
58whether the policy grants public access. The script checks this flag directly
59using ~get-bucket-policy-status~. If no bucket policy exists, this column shows ~No
60Policy~, which is not a finding on its own. Rather, it's a data point that
61instructs you to keep looking at further evidence.
62
63** ACLs
64
65S3 ACLs predate bucket policies and are largely considered legacy at this point,
66but they're still in use and still a source of public exposure. The script
67checks whether any ACL grants ~READ~ or ~WRITE~ permissions to the ~AllUsers~ group,
68which represents the public internet.
69
70** How the Three Layers Interact
71
72PAB is the highest authority. If PAB is fully enabled at the bucket level, it
73overrides any public bucket policy or ACL. This means a bucket can have a
74publicly permissive policy and still be safe, as long as PAB is fully
75restricted.
76
77The reverse is also true. A bucket with no public policy and no public ACLs is
78still at risk if PAB is missing or incomplete, because nothing is in place to
79prevent a future policy or ACL change from exposing it.
80
81* What the Script Does
82
83The script lists every bucket in the account, determines each bucket's region,
84runs all three checks against it, and appends the results to a CSV file.
85
86It runs in three steps for each bucket:
87
881. Determines the bucket's region by trying ~get-bucket-location~ against a list
89 of configured regions;
902. Checks PAB, bucket policy status, and ACLs independently;
913. Derives an ~OverallPublicStatus~ from the three checks and writes the row to
92 ~s3_full_public_access_audit.csv~.
93
94* Prerequisites
95
96You'll need:
97
98- AWS CLI installed (or access to CloudShell) and configured with credentials
99 that have read access to ~s3:ListAllMyBuckets~, ~s3:GetBucketLocation~,
100 ~s3:GetBucketPublicAccessBlock~, ~s3:GetBucketPolicyStatus~, and ~s3:GetBucketAcl~
101- ~jq~ installed
102- The ~AWS_REGIONS~ variable in the script updated to include any regions your
103 organization uses
104
105Check and update the region list at the top of the script before running:
106
107#+begin_src bash
108AWS_REGIONS="us-east-1 us-west-2 eu-central-1 ap-southeast-2"
109#+end_src
110
111Then run it:
112
113#+begin_src bash
114chmod +x aws_s3_buckets.sh
115./aws_s3_buckets.sh
116#+end_src
117
118* Deriving Overall Public Status
119
120The most important logic in the script is how it combines the three checks into
121a single ~OverallPublicStatus~. PAB is evaluated first and takes precedence:
122
123#+begin_src bash
124if [ "$PAB_FULLY_RESTRICTED" = "CRITICAL-MISSING" ]; then
125 OVERALL_PUBLIC_STATUS="TRUE - PAB Missing (CRITICAL)"
126elif [ "$OVERALL_PUBLIC_STATUS" != "FALSE" ] && [ "$PAB_FULLY_RESTRICTED" != "TRUE" ]; then
127 : # Status already set by Policy or ACL check above
128fi
129#+end_src
130
131If PAB is fully restricted (~TRUE~), the overall status stays ~FALSE~ regardless of
132what the policy or ACL checks find. If PAB is missing entirely, the overall
133status is immediately set to critical. If PAB is present but incomplete
134(~FALSE-VULNERABLE~), the overall status reflects whatever the policy or ACL
135checks found.
136
137* Reading the Output
138
139The script prints progress to the terminal as it runs and saves the full results
140to ~s3_full_public_access_audit.csv~:
141
142#+begin_src text
143Starting FULL S3 Public Access Audit for the CURRENT account...
144---
1451. Retrieving all bucket names...
146Processing bucket: 13bf5920-a09f-47bc-a75a-394a09f18d6a
147 Region determined: eu-west-1
148 Final Status: FALSE
149Processing bucket: c67fa6bd-2fd5-4bc5-825d-587fb535bf2e
150 Region determined: eu-west-1
151 Final Status: FALSE
152---
153Audit Complete.
154Final report saved to s3_full_public_access_audit.csv
155#+end_src
156
157#+begin_src text
158BucketName,Region,PAB_FullyRestricted,Policy_IsPublic,ACL_AllUsersRead,ACL_AllUsersWrite,OverallPublicStatus
15913bf5920-a09f-47bc-a75a-394a09f18d6a,eu-west-1,FALSE-VULNERABLE,No Policy,FALSE,FALSE,"FALSE"
160c67fa6bd-2fd5-4bc5-825d-587fb535bf2e,eu-west-1,TRUE,No Policy,FALSE,FALSE,"FALSE"
161#+end_src
162
163#+caption: S3 Public Access Audit Results
164#+attr_html: :alt Terminal output of aws_s3_buckets.sh showing the per-bucket audit results and CSV report.
165[[https://img.cleberg.net/blog/20260303-auditing-aws-s3/output.webp]]
166
167Here's how to read each column:
168
169- *PAB_FullyRestricted*: ~TRUE~ means all four PAB flags are enabled at the bucket
170 level. ~FALSE-VULNERABLE~ means PAB exists but is incomplete. ~CRITICAL-MISSING~
171 means no PAB configuration exists at all.
172- *Policy_IsPublic*: ~true~ means AWS has determined the bucket policy grants public
173 access. ~false~ means it doesn't. ~No Policy~ means no bucket policy is attached.
174- *ACL_AllUsersRead / ACL_AllUsersWrite*: ~TRUE~ means the bucket has an ACL
175 granting that permission to the public internet. ~FALSE~ means it doesn't.
176- *OverallPublicStatus*: ~FALSE~ means the bucket is not publicly accessible based
177 on all three checks. ~TRUE~ values include the specific reason (e.g., ~TRUE - ACL
178 Read~, ~TRUE - Policy~, ~TRUE - PAB Missing (CRITICAL)~).
179
180Note the first bucket in the example above: ~PAB_FullyRestricted~ is
181~FALSE-VULNERABLE~ but ~OverallPublicStatus~ is still ~FALSE~. This means the bucket
182isn't currently public, but it's missing the PAB configuration that would
183prevent it from becoming public if a policy or ACL were changed.
184
185* Common Exceptions and False Positives
186
187- *Static website hosting*: Buckets used for static website hosting are
188 intentionally public. These will show up with ~TRUE~ overall status and public
189 ACLs or policies. Confirm the business purpose with IT and document them as
190 accepted exceptions rather than findings.
191- *Policy_IsPublic with No Policy*: A ~No Policy~ result in the policy column is not
192 a finding. It simply means no bucket policy is attached. The overall status
193 depends on PAB and ACLs.
194- *FALSE-VULNERABLE with FALSE overall status*: This is a configuration weakness
195 rather than an active exposure finding. The bucket isn't currently public, but
196 PAB is not fully enabled, meaning a future change could expose it. Write this
197 up separately from buckets that are actively public, as the risk and
198 remediation are different.
199- *Cross-account or service-specific policies*: Some bucket policies grant access
200 to specific AWS accounts or services (e.g., CloudFront, Config, ELB logging).
201 AWS may flag these as ~IsPublic~ even though they're not publicly accessible in
202 practice. Review the actual bucket policy before raising it as a finding.
203- *Region coverage*: If a bucket's region isn't in the ~AWS_REGIONS~ list, the
204 script can't determine its location and will skip it with a warning. Make sure
205 the region list in the script covers your organization's full footprint before
206 treating the CSV as a complete population.
207
208* How to Write Up the Finding
209
210There are two distinct finding types this script can surface, and they should be
211written up separately.
212
213*Finding 1: Bucket with incomplete or missing PAB (configuration weakness)*
214
215*Deficiency:* S3 bucket ~13bf5920-a09f-47bc-a75a-394a09f18d6a~ does not have all four
216Public Access Block flags enabled at the bucket level (~PAB_FullyRestricted:
217FALSE-VULNERABLE~).
218
219*Risk:* Without fully enabled PAB, a future bucket policy or ACL change could
220expose the bucket to the public internet without additional controls in place to
221prevent it.
222
223*Finding 2: Bucket actively accessible to the public*
224
225*Deficiency:* S3 bucket ~example-bucket~ has an ACL granting ~READ~ access to the
226~AllUsers~ group (~ACL_AllUsersRead: TRUE~, ~OverallPublicStatus: TRUE - ACL Read~).
227
228*Risk:* Publicly accessible S3 buckets expose any objects stored within them to
229the internet, potentially including sensitive data.
230
231To filter the CSV to only buckets with a non-FALSE overall status:
232
233#+begin_src bash
234awk -F',' 'NR==1 || $7 != "\"FALSE\""' s3_full_public_access_audit.csv
235#+end_src