cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2026-03-03-auditing-aws-s3.org · raw

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