4 chapters · 16 labs
The lab board
Every lab covers a real class of weakness in a setup you run yourself: how it's abused, how you'd catch it, how to fix the cause, and how to verify the fix.
Secrets
The most common way real systems get breached: a credential that ended up somewhere it shouldn't — git history, an image layer, a CI log — and was found by someone else first.
The key that was 'already deleted'
A developer committed an AWS key, noticed, and deleted it in the next commit. The key is still one git log -p away for anyone who clones the repo.
Deleted in the Dockerfile, alive in the image
The Dockerfile copies .env to build the app, then runs rm .env. Anyone who can pull the image can read every value in it.
Masked in the log, stolen anyway
CI 'masks' secrets in logs. A one-line change in a third-party action, or a base64 pipe, prints them in the clear to anyone who can read build logs.
Used four minutes after the leak
A key leaked despite everything. The question now is how fast you find out someone is using it — and what they did.
Supply Chain
Most of the code you ship, you didn't write. Four labs on securing everything between the package registry and the image your cluster runs.
A four-year-old lodash in the lockfile
package-lock.json pins a lodash version with a published prototype-pollution vulnerability, used on a code path that deep-merges request bodies.
Code that runs when you type npm install
Packages can run arbitrary scripts at install time — on developer laptops and CI runners full of tokens — before a single test runs.
'Are we affected?' in five minutes, not five days
A critical vulnerability in a widely used library is announced on a Friday evening. Leadership asks which of our images contain it. Nobody knows.
The image your cluster runs — is it the one CI built?
Anyone who can push to the registry can replace shoplite:1.4.0 with something else, and every node that pulls that tag will run it.
Containers & Infrastructure
Where your code runs decides how bad a bug can get. Four labs on shrinking the blast radius of containers and catching insecure infrastructure before it's applied.
Everything runs as root
Every ShopLite container runs as root, with a writable filesystem and the default Linux capabilities. Any code-execution bug gets root inside the container, and a much shorter path out of it.
The log shipper that owned the host
The observability lab mounts /var/run/docker.sock into Alloy so it can read container logs. That socket is full control of the Docker host.
900 MB of someone else's vulnerabilities
ShopLite's first Dockerfile used node:latest: a full Debian with compilers, curl, git, and hundreds of CVEs — none of which the app ever uses.
The pull request that opened the database
A Terraform change to 'debug a connection issue' opens the RDS security group to 0.0.0.0/0 and makes the bucket policy public. The plan looked small, and it was approved.
Pipeline & Runtime
The last four gaps: a CI trigger that hands secrets to strangers, injection bugs a scanner could have caught, a URL feature that reaches cloud credentials, and noticing an intruder inside a running container.
A CI trigger that trusts strangers
To label PRs from forks, a workflow uses pull_request_target and checks out the PR's code. That runs untrusted code with repository secrets and a write token.
The injection bug review missed
A new product-search endpoint builds its SQL query by concatenating the search term. Two reviewers approved it on a busy Friday.
A URL field that can reach cloud credentials
A new 'import product image from URL' feature fetches whatever URL it's given — from inside the VPC, next to the instance metadata service.
Someone opened a shell in production
Every preventive layer can fail. When someone does get inside a running container, how long until you know?