Where tonight sits on the ladder
Session 1 drew nine paths to Ironwood's customers table. Session 2 closed three with MFA and one more with key rotation, leaving five. Tonight closes one, and it is the strangest of the nine: the only path that never enters the warehouse. Making the export bucket private takes the count from five to four. It is one path on the ladder and the single largest blind spot on the estate, because every other control on the board is a warehouse control and this path never meets one.
★ What you walk out with tonight. A storage-lane inventory of every bucket, stage and share that holds a copy of the crown jewels, with each one's public setting and grantee list written down. Ironwood's export bucket made private without breaking the agency that reads it. A weekly configuration review that diffs the estate against last week, so the next door opened for a vendor is found in seven days rather than two years. And the inventory file's "closed by" column filled for the bucket edge.
LiveWhy a misconfiguration is a different kind of door3 min▶
A credential door needs a stolen thing. A supply-chain door needs code that runs. A misconfiguration door needs a URL. There is no login event, because nothing logged in; there is no query, because nothing queried. The data was simply reachable, and reachable data gets reached.
That is why the bench counts the export bucket as a path with no undetected-to-detected transition available: query logging moves eight paths from unseen to seen and leaves this one exactly where it was. The only way to see a bucket being read is a log on the bucket, and the only way to stop it is a setting on the bucket.
Ironwood's export bucket was created two years ago for a marketing agency that needed a weekly customer extract. The engineer who set it up made it publicly readable because the agency's tool did not support signed requests, ticketed a follow-up to fix that, and left the company four months later. The bucket appeared in no warehouse grant, no access review and no audit log. It appeared in a search engine's index of open storage.
Self-studyThe vocabulary for the storage lane3 min▶
- Bucket - an object store container. It has its own access model, separate from the warehouse's, and usually weaker because fewer people look at it.
- Public access - two distinct settings: whether an unauthenticated principal can list the objects, and whether it can read them. Listing without reading still tells an attacker the file names and the nightly cadence; reading without listing still works for anyone who has ever seen a URL.
- External stage - the warehouse's pointer to a bucket, used to load from it or unload to it. The stage's location is a bucket path, which makes
show stagesthe fastest way to find buckets the warehouse touches. - Share - a grant of read access on warehouse objects to another account. Its grantee list is a second set of doors, one per account, each of which has its own users and its own hygiene you do not control.
- Network policy - the allowlist of addresses a credential is allowed to authenticate from. A missing or wide-open policy is the setting that turns a leaked key into a key usable from anywhere.
- Configuration drift - the gap between the setting somebody chose and the setting in force today. Drift is not usually malicious. It is a vendor onboarding that never got its offboarding.
Misconfiguration is a door with no attacker skill required
9 min liveThree shapes cover nearly every misconfiguration on a data estate: a public bucket holding a copy of the data, a share or stage granted wider than intended, and a network policy that lets a credential work from anywhere. Each is a setting someone chose for a reason that made sense that week.
LiveShape one: a public bucket holding a copy of the crown jewels3 min▶
Ironwood's export bucket holds a nightly extract of the customers table, written by the ETL job for the agency. Two settings on the bucket were wrong: anyone could list the objects, and anyone could read them. The first tells a stranger that a file called customers_2026-09-07.csv exists and that a new one arrives every night. The second lets them download it.
On the graph the bucket is two edges: internet to bucket, because it is listable without authentication, and bucket to crown jewels, because the object is the table. Making the bucket private removes both. That is the 5 to 4 rung on the ladder, and the count is one because the path is one; its weight, if the bench weighted by records reached, would be the whole table.
The extract was 1.9 million rows. The warehouse's access review listed 41 grants on the customers table, every one of them justified. Not one of them was the door, because the door was not a grant.
LiveShapes two and three: the wide share and the open network policy3 min▶
A share granted wider than intended. Ironwood shares a sales summary with a partner. The share's grantee list has two accounts on it: the partner, and an agency whose contract ended fourteen months ago. The agency's account is still live, still has users, and still reads the share on a schedule nobody at Ironwood set. An external stage has the same shape from the other side: its location is a bucket, and whoever can write to that bucket can put objects where the warehouse will load them.
A network policy that allows everywhere. The ETL service key authenticates from any address. Session 2 rotated it, so the copy in git history no longer works. The current key still works from anywhere, which means a future leak is a full door again. An allowlist that restricts the key to the pipeline's own address range turns that future leak into a refused login. It closes exactly one edge on the graph, the service key to the warehouse when used from outside, and it is the one edge the "perimeter firewall only" over-correction actually closes.
Self-studyWhy no warehouse control can touch the bucket path3 min▶
Eight of the nine paths pass through the warehouse. A query log on the customers table sees all eight; least privilege narrows what several of them can read; MFA removes the three that start with a phished human. The ninth reads a file from a bucket. It never authenticates to the warehouse, never runs a query, never uses a role. So no warehouse control, not logging, not least privilege, not MFA, has any effect on it. The bench confirms it: switch on every warehouse control and leave the bucket public, and one unseen path remains that is not the insider.
The general rule follows. Every copy of the crown jewels outside the warehouse, every export, share, replica, stage and backup, needs its own inventory row, its own control and its own log. A copy without those three is an open door in a room with no camera, and the warehouse's excellent camera is pointed at a different room.
Finding them before somebody else does
8 min liveA misconfiguration is a setting, and settings can be listed. The discipline is listing them every week and diffing against the previous week, so a new door shows up as a changed line rather than as an incident. An annual audit finds the door in a year; the search engine found Ironwood's in weeks.
LiveConfiguration review is a scheduled job, not an annual audit3 min▶
The job lists four things: every bucket's public-access setting, every share's grantee list, every stage's location, and every network policy's allowlist. It writes them to a dated file and diffs the file against last week's. A new line is a new object somebody created; a changed line is a setting somebody changed. Both go to a human, with the name of whoever made the change if the platform records it.
Two properties make it work. It runs without anyone remembering to run it, so the finding does not depend on the audit calendar. And it compares against last week rather than against a policy document, so it catches the drift the policy never anticipated. A bucket that was private for two years and became public on Tuesday is a one-line diff on Monday.
Ironwood's first run of the job produced 63 lines: 19 buckets, 7 stages, 4 shares, 33 accounts with a network policy or none. Two lines were public buckets. The second run, a week later, produced a one-line diff: a new stage pointing at a bucket in a different cloud account, created by a contractor for a proof of concept. It was closed the same morning, which is the entire argument for weekly.
LiveDefault deny for new storage, and what it costs2 min▶
Every new bucket starts with public access blocked at the account level, so an individual bucket cannot be made public by a policy statement alone; the block has to be lifted deliberately, by someone with the right to do it, in a change that leaves a record. Every new share starts with an empty grantee list and an expiry date. Every new service account starts attached to a network policy.
The cost is real and worth naming. A team that needs a public bucket for a static site has to ask, and the ask takes a day. A vendor onboarding that used to be an afternoon becomes an afternoon plus a ticket. The friction is the control: the day the ask takes is the day somebody writes down why the door exists and when it closes. Ironwood's export bucket had no ticket, because nothing required one.
Self-study"Created for a vendor, never removed", and the expiry rule3 min▶
Nearly every misconfiguration on a data estate has the same origin story. A vendor, agency, contractor or partner needed access; somebody granted it in the fastest way that worked; the engagement ended; nobody owned taking it back. The bucket, the share grantee and the wide-open policy are all this story at different lanes.
The access-control course sets the expiry rule for grants: every grant to a human or an external party carries a date, and on that date it is revoked unless someone renews it in writing. The same rule applies without change to shares, to bucket grants for external accounts, and to network policy exceptions. An expiry that is written into the inventory turns "never removed" into "removed on the date, or renewed by a named person". The weekly diff then flags any share past its expiry as a changed line, whether or not anyone remembered.
- Grant to a named account, never to the public. The agency has its own cloud account; the bucket grants that account read on the prefix it needs. Nothing anonymous.
- Write the expiry next to the grant. In the inventory file, and in the share's comment or the bucket's tags if the platform allows it.
- Name the owner. The person who renews or lets it lapse. A grant without an owner is a grant the diff job will flag every week until someone claims it.
Self-studyThe network allowlist: a real control with a small effect on the graph3 min▶
An allowlist on the warehouse says a credential may authenticate only from these addresses. For a service key that is always used by the same pipeline on the same hosts, the allowlist is nearly free and closes a whole class of future incident: the key leaks again, someone tries it from outside, and the login is refused before any query runs.
The bench is honest about how much that is worth on its own. The "perimeter firewall only" over-correction is exactly this control with nothing else, and it leaves 7 paths, 7 undetected, 0 recoverable. It closes one door, the leaked key used from outside. A phished analyst logs in from their own laptop inside the range. The malicious package runs on the CI runner, which is inside the range because it has to be. The export bucket sits outside the warehouse altogether, so the warehouse's allowlist never sees the read. And both insiders were never outside.
So the allowlist belongs on the estate, as the third part of the service-account pattern from session 2, and it belongs in the weekly diff so a detached policy is a flagged line. It does not belong at the top of anyone's list of what to buy, and the number that says so is 7.
Close the bucket
★ 20 min · everyone buildsEvery command runs against your own estate and your own accounts. The output is three things: a storage-lane inventory with each object's public setting and grantee list, a private export bucket that the agency can still read, and a diff job that runs next Monday without you.
Inventory every bucket, stage and share that touches customers data. Start from the warehouse side, because the stages tell you which buckets the warehouse reads from and writes to, and the shares tell you which external accounts can read at all. Then go to the cloud side for the buckets the warehouse does not know about, which is where Ironwood's export lived.
The listing command differs between platforms and the shape does not: a list of objects, then one detail call per object. Ironwood found 19 buckets from the cloud side and only 7 stages from the warehouse side. The 12 buckets the warehouse did not know about were the lane nobody had walked.
Write each object into the storage lane of the inventory file with its setting. One line per bucket, stage, share and network policy. The columns that matter are the public flag, the grantee list, the location, the allowlist, what the object holds, and who owns it. A blank owner is a finding on its own.
Ironwood's first pass had two public buckets, one share with an expired grantee, one policy that allowed everywhere and fourteen accounts with no policy. Six lines, six doors, none of them a grant on the customers table.
Make the export bucket private, and confirm the agency's own account still reads it. Two changes on the bucket, then one grant, then two checks. Block public access at the bucket, remove the public-read statement from the bucket policy, grant the agency's own cloud account read on the prefix it needs, with an expiry written into the inventory. Then read the bucket's access log: the agency's principal succeeds, anonymous requests are refused.
The confirmation is the step teams skip. Ironwood's first attempt blocked public access and forgot the grant; the agency's Monday pull failed and the change was reverted by lunchtime, which reopened the door for another week. Grant first, then block, then read the log.
Write the weekly diff job. A snapshot script that emits the storage lane in the same shape as step 2, a diff against last week's snapshot, a grep for the settings that are never acceptable, and a rotation of the baseline. Schedule it for Monday morning, so the diff is on someone's desk when the week starts.
snapshot-config.sh is the step 1 commands with their output reshaped into one line per object. It is the only platform-specific part, and the diff and flag steps never change when you move clouds.
Write the share and the policy findings as edges, with their closed-by control. The expired agency grantee on the share is revoked tonight; the partner's grantee gets an expiry and an owner. The ETL key's network policy is narrowed to the pipeline's range, which is the third part of the service-account pattern from session 2. The fourteen accounts with no policy go on the list for the owner rota; they are not changed tonight, because a wrong allowlist is an outage.
Update the inventory file's "closed by" column for the bucket edge, with the date. Two edges close tonight, the same two the bench removes when the private-bucket control is on. Write the agency's named grant and its expiry into the same cell, so next year's reader knows why the bucket still exists.
The bucket now has a control. It does not yet have a log anyone reads. The "seen by" cell says "bucket access log", and tonight that log exists and nobody looks at it. Session 5 wires bucket reads and external-stage unloads into the same alerting as the query log, which is the step that turns a copy outside the warehouse from "closed" into "closed and watched".
Before session 4
30 min- Finish the storage lane for one warehouse: every bucket from the cloud side, every stage and share from the warehouse side, each with its public flag, grantee list, location, owner and expiry. Count the blanks in the owner column; that number is the finding.
- Make every bucket that holds a copy of the crown jewels private, in the order grant first, block second, read the access log third. One bucket per day, and never on a Friday.
- Schedule the weekly diff job and send yourself the first diff. Read it. If it is empty, that is a result; if the job did not run, that is the finding.
- Write an expiry and an owner against every share grantee and every external-account bucket grant. Take the ones nobody will claim to whoever runs access review, as a list, not as a revocation.
Three questions
Query logging is switched on for every table, least privilege is enforced, every human has MFA. Ironwood's export bucket is still public. Why does the bench still show one unseen path that is not the insider?
The extract is a file. Reading it is a request to the bucket, not to the warehouse, so the warehouse's log, grants and second factor are pointed at a different room. The bucket needs its own control and its own log.
The marketing agency still needs its weekly extract. What is the right shape for closing the bucket?
Grant first, block second, log third. A named grant with an expiry is a door with an owner and a closing date; an unguessable name is still a public door, and stopping the extract gets the change reverted by lunchtime.
A network allowlist on the warehouse, on its own, leaves seven of nine paths open. Why does it still belong on the estate?
The allowlist closes exactly one door: a leaked key used from outside the range. It cannot see a bucket that sits outside the warehouse, and a phished user logs in from their own laptop inside the range. Small effect, real effect, and never the first purchase.