learn-cyber-security-with-phoebe / Builder session 3 of 8
Learn Cyber Security with Phoebe · Builder session 3 of 8

Misconfiguration: the doors nobody meant to leave open

Two of the three doors into a data estate take work to walk through: a credential has to be stolen, a package has to be poisoned. The third takes nothing. A bucket that lists its contents to anyone, a share granted to an account whose contract ended, a network policy that lets a key authenticate from any address on earth. Nobody attacked Ironwood's export bucket; somebody found it. Tonight is the inventory of those doors, the one control that closes the bucket path, and the weekly job that stops the next one appearing unnoticed.

🟡 Core Platform and analytics engineers 45 min
0-3 · Welcome 3-20 · The doors nobody meant to leave open 20-40 · Build-along: close the bucket 40-45 · Q&A
Part 0

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.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ 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.

Real world

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 stages the 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.
Part 1 · covers the three misconfiguration shapes and the path the warehouse cannot see

Misconfiguration is a door with no attacker skill required

9 min live

Three 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.

start the door what it yields the estate the prize Internet the warehouse path: eight of nine look like this phished analyst analyst login Warehouse customers Warehouse controls query logging no effect least privilege no effect MFA on logins no effect on the bucket path below the bucket path: one of nine, and it never enters the warehouse public export bucket nightly extract a copy of the table, no query, no login private bucket: 5 to 4 Eight paths pass through the warehouse and its controls see them. The ninth never enters it.
🔍 Click to zoom - the bucket path beside the warehouse path, and the one gate that closes it.
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.

Real world

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.

Part 2 · covers configuration review as a scheduled job, default deny, the vendor pattern and the allowlist

Finding them before somebody else does

8 min live

A 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.

1 · List 2 · Diff last week 3 · Flag 4 · Close Buckets and stages every copy of the data public-access flag per bucket and stage new bucket, or flag flipped to public any public bucket holding an extract block public access grant the reader Shares one door per grantee grantee list of every share grantee added, or share past expiry grantee no owner can vouch for revoke, or renew with a new expiry Network policies from where a key works allowlist of every network policy range widened, or policy detached an account with no policy at all attach the policy, narrow the range next week: same job, and this week's snapshot becomes the baseline A weekly review finds the door in seven days. An annual audit finds it in a year, if it is on the list.
🔍 Click to zoom - list, diff, flag, close, every week, across three lanes.
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.

Real world

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.

Build-along

Close the bucket

★ 20 min · everyone builds

Every 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.

★ Try it now · the warehouse side, then the cloud side show stages; -- every external stage: its location is a bucket path, write each one down show shares; -- every share, outbound and inbound show grants to share <share name>; -- for each outbound share: the grantee accounts, one door per row -- cloud side, your own account only: list every bucket, then for each one read its -- public-access configuration and its bucket policy. The command differs per cloud; -- the question is the same: can an unauthenticated principal list or read this bucket?

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.

★ The storage lane, first pass # storage lane, 2026-09-08 bucket ironwood-export-agency public-read: YES listing: YES holds: customers extract, nightly owner: (none found) bucket ironwood-raw-landing public-read: no listing: no holds: raw supplier feeds owner: platform stage STG_AGENCY_EXPORT location: ironwood-export-agency/customers/ used by: ETL role share SHR_PARTNER_SALES grantees: partner_acct, agency_acct (contract ended 2025-07) expiry: (none) policy NET_ETL attached to: etl_service allowlist: 0.0.0.0/0 policy (none) attached to: 14 service accounts have no policy

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.

★ Try it now · private, then a named reader # on the bucket, your own account (command shape differs per cloud): # 1 enable "block all public access" on ironwood-export-agency # 2 remove the statement granting read or list to everyone from the bucket policy # 3 grant the agency's OWN account read on customers/ only, no list on the rest of the bucket # 4 tag the bucket: owner=marketing-platform expiry=2027-03-01 purpose=agency weekly extract # then confirm from the bucket's access log, not from memory: # agency principal GET customers/customers_2026-09-08.csv 200 # anonymous GET customers/customers_2026-09-08.csv 403 # anonymous LIST / 403

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.

★ Try it now · config-review.sh, runs every Monday 06:00 #!/bin/sh # weekly configuration review: list, diff against last week, flag, rotate the baseline set -e today=$(date +%F) mkdir -p config # 1 list: one line per bucket, stage, share and policy, same columns as the inventory's storage lane ./snapshot-config.sh > "config/$today.txt" # 2 diff: every added or changed line is a review item for a human, with the change author if the platform records it diff "config/last-week.txt" "config/$today.txt" > "config/$today.diff" || true # 3 flag: the settings that are never acceptable, regardless of whether they changed grep -E 'public-read: YES|listing: YES|allowlist: 0.0.0.0/0|expiry: \(none\)|owner: \(none' "config/$today.txt" > "config/$today.flags" || true # 4 rotate: this week becomes next week's baseline cp "config/$today.txt" config/last-week.txt # deliver the diff and the flags to the owner rota; an empty diff is a result too, and it gets sent

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.

★ Try it now · the share and the policy -- the expired grantee: revoke tonight, the contract ended fourteen months ago alter share SHR_PARTNER_SALES remove accounts = agency_acct; -- the partner stays, with an expiry and an owner written where the diff job will read them comment on share SHR_PARTNER_SALES is 'owner=sales-platform expiry=2027-01-31 grantee=partner_acct'; -- the ETL key: allowed only from the pipeline's own range; syntax differs by warehouse, the shape is a list of ranges create or replace network policy NET_ETL allowed_ip_list = ('<pipeline range, placeholder>'); alter user etl_service set network_policy = NET_ETL; -- then confirm from the login history that the pipeline's next run authenticated, before touching any other account

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 inventory file after tonight # from -> to | the move | closed by | seen by internet -> bucket_export | bucket listable without auth | private bucket, public access blocked, 2026-09-08 | bucket access log (session 5) bucket_export -> crown | nightly extract of the table | private bucket; agency reads by named account, expiry 2027-03-01 | bucket access log (session 5) svc_key -> warehouse | key authenticates from anywhere | network policy NET_ETL, pipeline range only, 2026-09-08 | login history internet -> ci_runner | malicious package in the build | pinned dependencies (session 4) |

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".

Homework

Before session 4

30 min
  1. 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.
  2. 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.
  3. 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.
  4. 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.
Check yourself

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.

Sources covered

What tonight maps to

✓ NIST CSF 2.0 - Protect function, configuration of platform and storage Safeguards to manage cybersecurity risk are used: public-access blocks, share grantee control, network policies, and a scheduled review that keeps them in force.
✓ The misconfiguration door on the attack graph Public buckets, open shares, permissive network policy; the private bucket as the 5 to 4 rung, and why it is the one path no warehouse control reaches.
✓ Configuration review as a scheduled diff List, diff against last week, flag, close; default deny for new storage and the friction it costs.
◐ Grant expiry and access review The rule is the access-control course's. Applied here to shares and external bucket grants without change.
◐ Watching the bucket: access logs and egress alerting The bucket's log exists after tonight; session 5 makes somebody read it.
· Network engineering beyond allowlists Not covered by design. The allowlist appears as one setting in one lane of the inventory.

Session 3 cheat sheet

Three shapesPublic bucket holding a copy, share or stage granted too wide, network policy that allows everywhere.
Private bucket: 5 to 4One path on the ladder, and the only one that never enters the warehouse.
Warehouse controls cannot see itNo login, no query, no role. Logging, least privilege and MFA all have no effect on the bucket path.
Own row, own control, own logEvery copy of the crown jewels outside the warehouse: exports, shares, stages, replicas, backups.
Grant, block, logNamed account with an expiry first, public access blocked second, access log read third. Never the other order.
Weekly diff, not annual auditList every setting, diff against last week, flag what is never acceptable, rotate the baseline.
Created for a vendor, never removedEvery external grant carries an expiry and an owner, or the diff job flags it every week.
Allowlist: real, smallCloses one door, the leaked key used from outside. On its own: 7 paths, 7 unseen, 0 recoverable.