top of page
Search

Cloud Storage: Bucket Access, Evidence, and the Public-Exposure Problem

  • 2 days ago
  • 6 min read

GCS buckets are both a common source of evidence and, misconfigured, one of the most common ways data actually leaves a GCP environment.


Cloud Storage shows up in almost every incident twice: once as a target — the thing the attacker was actually after — and once as a source of evidence, if you configured it right beforehand.


This article covers both sides: how bucket access actually works, how to tell if an attacker exfiltrated data through a bucket, and how to collect GCS objects in a way that holds up as evidence.


IAM vs. Legacy ACLs

Buckets can be governed two ways, and it's important to know which one a given bucket uses before you try to reason about who could access it.


Uniform bucket-level access applies IAM policy consistently across every object in the bucket — one policy, no per-object exceptions, and it's what Google now recommends and defaults new buckets to.

The older model, fine-grained access, layers legacy ACLs on top of IAM at the individual object level, meaning a bucket's overall IAM policy can look reasonable while a specific object inside it has an ACL granting allUsers read access that nobody would find just by checking the bucket policy.


Solstice's security team standardized on uniform bucket-level access org-wide for exactly this reason — with fine-grained buckets, checking "is anything in this bucket public" means enumerating every object's ACL individually, not just reading one policy document. If you inherit an investigation involving a fine-grained bucket, budget the extra time for that enumeration.

gcloud storage buckets describe gs://solstice-prod-invoices-archive --format='value(iamConfiguration)'

Access Logging: Two Different Features, Only One Still Worth Using

GCS has two separate logging mechanisms, and it's easy to assume the newer one replaces the older one — it doesn't, not fully.


Data Access audit logs give you per-object-read/write attribution tied to a principal, in the same audit log pipeline as everything else, and that's the right default for tracking authenticated access. But Data Access audit logs only capture activity from an identified principal — they have a blind spot for exactly the traffic you most need during a public-exposure investigation: anonymous requests from allUsers or allAuthenticatedUsers.


For that, you still need the older, bucket-native Usage Logs and Storage Logs — CSV files written into a destination bucket, and the only mechanism that tracks unauthenticated object access at all.


Two catches worth knowing before you rely on them:

  • they can only be enabled via gsutil or the API, not the console

  • they don't route through Cloud Logging or the Log Routing pipeline at all — you have to retrieve them manually from wherever they're written, or process them with a Cloud Function.


Usage Logs are produced hourly and record every access from allUsers/allAuthenticatedUsers (Google explicitly doesn't guarantee no duplicates between hourly files — dedupe on the s_request_id field). Storage Logs are written once a day and just report total storage consumed over the prior 24 hours, useful for spotting unexplained growth or shrinkage rather than individual access events.

gsutil mb gs://solstice-access-logs
gsutil iam ch group:cloud-storage-analytics@google.com:legacyBucketWriter gs://solstice-access-logs
gsutil logging set on -b gs://solstice-access-logs gs://solstice-prod-invoices-archive

If Usage Logs weren't enabled on a bucket before an incident, there's no way to retroactively see who accessed it anonymously — which makes enabling them upfront, on any bucket handling sensitive data, one of the cheapest insurance policies in this article.


Versioning, Soft Delete, and Making Deletion Not Mean Deletion

Two separate features protect against data loss — including an attacker deliberately deleting objects to cover their tracks — and it's worth knowing both because they solve slightly different problems.


Object versioning keeps prior versions of an object when it's overwritten or deleted, and those noncurrent versions are retrievable by generation number until a lifecycle rule cleans them up.


Soft delete, a newer and separate feature, retains any deleted or overwritten object for a configurable retention window (seven days by default) regardless of whether versioning is even enabled — so even a bucket that was never configured for versioning still gives you a recovery window if soft delete is on, which it now is by default for new buckets.


For an investigation, this means an attacker's gsutil rm -r doesn't necessarily mean the objects are gone. Check both features before writing off deleted evidence as unrecoverable.
gcloud stora
ge ls --with-versions gs://solstice-prod-invoices-archive/
gcloud storage objects restore gs://solstice-prod-invoices-archive/q3-report.csv --generation=1723484921003912

Bucket Lock: Making Your Own Evidence Tamper-Proof

If you're the one setting up evidence retention rather than investigating after the fact, a retention policy combined with Bucket Lock gives you a genuine WORM (write-once-read-many) guarantee — once locked, not even a project Owner can shorten the retention period or delete objects before it expires, short of deleting the entire project.


Solstice's sec-ops team locks their audit-log export bucket this way specifically so that a compromised Owner-level identity — the worst case scenario — still can't destroy the evidence trail that would expose the compromise.


The Public-Exposure Problem

A bucket or an individual object becomes public the moment allUsers or allAuthenticatedUsers appears in an IAM binding or a legacy ACL entry. This remains one of the most common ways sensitive data actually leaves a GCP environment — not through a sophisticated exfiltration technique, but through a bucket that was made public temporarily for a legitimate reason and never locked back down.


Public Access Prevention, an org-policy or bucket-level setting, blocks these bindings outright regardless of what IAM policy says, and checking whether it's enforced organization-wide is one of the fastest ways to rule out this entire class of exposure in an investigation.


This same public-exposure pattern has an off-the-shelf attacker tool built around it — GCPBucketBrute automates exactly this recon: dictionary-guessing bucket names (which aren't secret, just unlisted), checking which ones respond as valid, then testing whether objects inside can be listed publicly.

If a bucket comes back positive on all three, downloading its contents is trivial. Worth checking your own naming conventions against this reality — a predictable scheme (solstice-prod-*, solstice-backup-*) is exactly what a dictionary attack is built to find.

gcloud storage buckets list --format='table(name, iamConfiguration.publicAccessPrevention)' \
  --project=solstice-prod-data

Collecting GCS Objects as Evidence

Every object carries Google-computed CRC32C and MD5 hashes as metadata, generated at write time — pull and record these alongside your own hash of the downloaded copy rather than only hashing after the fact, since it gives you an independent value to cross-check against. Always copy by generation number when the bucket has versioning or soft delete enabled, so you're provably pulling the exact version relevant to your timeline rather than whatever happens to be current at download time.

gcloud storage objects describe gs://solstice-prod-invoices-archive/q3-report.csv --format='value(md5Hash,crc32c,generation)'
gcloud storage cp gs://solstice-prod-invoices-archive/q3-report.csv#1723484921003912 ./evidence/


No Logging At All? Check the Bill

Sometimes you inherit an investigation where neither Data Access audit logs nor Usage/Storage Logs were ever enabled on the bucket in question — more common than it should be.


You're not entirely out of options:

Google Cloud tracks data egress for billing purposes regardless of whether bucket-level logging was configured, since data leaving a Cloud zone/region gets billed back to the customer either way.

The Billing section's Cost Table sub-menu breaks down cost per service per month, and that includes network egress from Cloud Storage. It won't give you a filename or a source IP — this is a blunt instrument — but it can tell you approximately when a spike in egress happened and roughly how much data moved, which is often the difference between having a rough exfil estimate for a client's insurer and having nothing at all to hand them.


One real limitation:

Cost Table data only finalizes at the end of a billing cycle (typically monthly) — if the incident is mid-cycle, this data isn't available yet and you'll be waiting on the cycle to close before you can pull it.

IR tip: check Cost Table egress by month before telling a client billing data can't help — it's a last resort, but it beats telling them there's simply no record at all.
📌 Soft delete moving to generally available and on-by-default for new buckets is the biggest practical change — it means "the attacker deleted the evidence" is a weaker claim than it used to be, and worth actively checking before accepting it. Public Access Prevention has also moved from an opt-in setting to something Google actively pushes organizations toward enforcing by default via org policy, which has measurably reduced (though far from eliminated) the accidental-public-bucket class of incident industry-wide.
IR tip: Before you tell a client their exfiltrated data is unrecoverable because the attacker deleted the bucket contents, check soft delete and object versioning status first — in a meaningful fraction of cases, the objects are still sitting there in a recoverable state and nobody's checked.

----------------------------------------------Dean----------------------------------------------------

 
 
 

Comments


Ready to discuss:

- Schedule a call for a consultation

- Message me via "Let's Chat" for quick questions

Let's connect!

Subscribe to our newsletter

Connect With Me:

  • LinkedIn
  • Medium

© 2023 by Cyberengage. All rights reserved.

bottom of page