Secure a pipeline, stage by stage
Every earlier page in this course explained a control. This is where you install one. You'll take over Vulnerly — one small, deliberately vulnerable sample app and pipeline, seeded with the exact failure modes this course keeps coming back to — and lock it down across seven connected parts: threat model it, gate it with SAST and secrets scanning, add an SCA gate, harden and sign the container, scan its infrastructure-as-code and enforce policy, run DAST against a staging deploy, then ship compliance evidence and wire up monitoring. Each part hands the next one a pipeline that's a little more locked down than the one before, and nothing here is graded by a person — every part has an objective, checkable "done when."
Reading about a lock doesn't teach you to pick one, and reading about a good lock doesn't teach you to install one either. This is the part where you get the screwdriver out. You'll take a small app that was built with every door left unlocked on purpose, and go around the house yourself, room by room, until nothing is left open — and you'll know it worked because you'll have tried the door afterward, not because someone told you it was fine.
Everything below is local and throwaway — no production system, no real customers, no cloud bill you didn't choose to run up. You'll want Docker (or Podman), Git, Node.js (LTS) with npm, a local Kubernetes cluster via kind (minikube works too — translate the commands yourself), kubectl, and a GitHub account for a real Actions pipeline (GitLab CI works the same way; adjust the YAML). Each part introduces its own scanner or CLI as you reach it — Part 2 doesn't need cosign yet, so don't install everything up front. If you provision the Part 5 infrastructure against a real cloud account rather than a sandbox like LocalStack, keep it to the smallest possible footprint and tear it down (terraform destroy) the moment you're done. Tear the cluster down too when you're finished: kind delete cluster --name vulnerly-dev.
The app you're locking down
☺ Like you're 10: One small app, built badly on purpose, so you have real locks to install instead of imaginary ones.
Vulnerly is a small Node.js/Express service backed by Postgres, shaped like the payments-reconciliation API from this course's own Ledgerly case study — a fintech-style service a handful of clients hit to check transaction status. It is not the real Ledgerly; that story already ended, six weeks and four new controls later. Vulnerly is a fresh, throwaway sandbox built specifically for this capstone, seeded with the same shapes of mistake Ledgerly's postmortem named, plus a few more, so you can find and fix them yourself instead of reading about someone else doing it. Every seeded weakness maps to exactly one capstone part:
- A hardcoded payments-processor key sitting in a committed
.envfile, and a reconciliation endpoint that builds its SQL query with plain string concatenation instead of a parameterized query. - An old, known-vulnerable transitive dependency in
package.jsonthat nobody's looked at since the project was scaffolded. - A
Dockerfilethat pulls a full, unpinnednodetag and never drops root — and an image nobody has ever signed. - A Terraform-provisioned S3 bucket (for transaction-export logs, same as Ledgerly's own) with a public-read ACL, and a security group open to
0.0.0.0/0on the database port. - A reflected XSS on the transaction-search endpoint and an IDOR that lets one merchant fetch another merchant's reconciliation record just by incrementing an ID.
- No centralized findings, no evidence trail, no control mapped to a framework, and no alert that would ever notice if any of the above came back.
vulnerly/ # github.com/<you>/vulnerly
├── app/
│ ├── src/ # Express routes + the Postgres client
│ ├── package.json # a stale lodash/jsonwebtoken-class dependency, pinned on purpose
│ └── .env # a real-looking payments-processor key, committed on purpose
├── Dockerfile # FROM node:18, runs as root, nothing pinned — Part 4 rewrites this
├── infra/
│ └── main.tf # public-read S3 bucket + a 0.0.0.0/0 security group — Part 5
├── k8s/
│ └── deploy.yaml # the Vulnerly Deployment + Service Parts 4–6 apply
└── .github/workflows/ci.yml # a pipeline with zero required checks — Parts 2–6 each add oneThe cluster is created once with kind create cluster --name vulnerly-dev and stays up for the whole capstone. It holds two namespaces: vulnerly for the app itself, and platform for the add-ons you'll install along the way — Kyverno in Part 4, DefectDojo in Part 7. Keep that split consistent and you'll never be confused about where something lives.
The seven stages
☺ Like you're 10: Seven checkboxes, each one a real lock you actually installed — not a description of one someone else installed.
Work them in order — Part 3's SCA gate assumes Part 2's scanning gates already exist in the pipeline, and Part 7's compliance evidence has nothing to collect if Parts 2 through 6 haven't produced any findings yet. Each row below is its own deep, standalone page with runnable manifests and config, not just prose — this hub just tracks whether you've cleared each one. Checking a box here only tracks your own progress; the actual proof lives in your terminal, your CI run, and your cluster.
.env gets caught. Add Semgrep (or CodeQL/SonarQube) as a required check flagging the string-concatenated SQL query. Fix both findings for real — move the payments key into HashiCorp Vault instead of just deleting the file, and rewrite the query to use parameters.git log.Dockerfile as a pinned, non-root, multi-stage build on a slim or distroless base. Scan the rebuilt image with Trivy or Grype until it's clean of criticals. Sign it keylessly with cosign/Sigstore, then add a Kyverno policy in the vulnerly namespace that rejects any image at admission that isn't signed.kubectl apply of an unsigned test image is rejected by the policy, and the signed image deploys and runs cleanly as a non-root user.infra/main.tf as a required check. Fix the public-read S3 ACL and the security group open to 0.0.0.0/0. Write an OPA/Conftest policy that blocks both patterns before terraform apply ever runs. If you provisioned against a real account or a LocalStack sandbox, run Prowler or ScoutSuite once to see the same gap Sol would find reviewing it by hand.terraform plan for the original permissive config is blocked by the policy check, and the fixed plan applies with no open findings.staging namespace on your kind cluster. Run an OWASP ZAP baseline scan, then a full active scan, against it. Confirm ZAP independently rediscovers the reflected XSS and the IDOR — the same two threats your Part 1 model already flagged. Fix both, then re-scan until the report comes back clean, or with only explicitly accepted findings, documented as such.Part 1 comes before any tool runs, on purpose. A scanner can only find the shape of bug it already knows to look for; a threat model is what tells you the SQL-injection path and the IDOR are worth looking for in the first place, before Part 2's gate and Part 6's scan go find them for real. Skip the threat model and you're still going to find both — you're just going to find them the way Ledgerly found its leaked key: by accident, after the fact, from whoever got there first.
Why this order, and what "done" actually means
☺ Like you're 10: The order isn't arbitrary — each part needs the one before it to already be true, the same way you can't sign a container image before you've decided it's clean.
The seven parts trace this course's own maturity ladder, roughly the shape the DSOMM/SAMM/BSIMM page names explicitly: design-time analysis (Part 1) before build-time gates (Parts 2–3), build-time gates before deployment hardening (Parts 4–5), deployment hardening before runtime verification (Part 6), and none of it counts as a program rather than a one-time fix until it's evidenced and monitored (Part 7). Finish all seven and you'll have hands-on reps in every category the CDP exam tests — it's a hands-on, task-based exam itself, five live challenges against a running environment, no multiple choice — so this capstone is closer to exam rehearsal than a side exercise. Pair it with the CDP study plan for where it fits in a full prep schedule.
Once all seven are checked, run one more pass end to end: push a branch that reintroduces the leaked key, the raw SQL, the vulnerable dependency, an unsigned image, the public bucket, and the XSS — all at once, deliberately — and confirm the pipeline stops every single one before anything reaches staging. That single run is the real done-when for the whole capstone, not the seven checkboxes above; the checkboxes just track your own progress toward being ready to run it.
Professor Owl: Seven parts, one app, in order. Rocky opens with the threat model — everyone else is just closing what he finds.
Rocky: Found four things worth ranking in under an hour. The SQL string-concat and the IDOR are both in there — remember those, you'll need them again in Parts 2 and 6.
Timmy: And they won't get past me twice. SAST, secrets, SCA, then DAST at the end to check the whole thing from outside — I gate four of the seven parts myself.
Benny: I'll rebuild the image clean — non-root, pinned, distroless. Pip, you're not letting it run unsigned though, are you?
Pip: Not a chance. No signature, no admission. That's the whole point of Part 4.
Recon: The bucket and the security group are mine and Sol's. A policy that blocks them before apply beats a human noticing them after.
Nutty: And when all six of you are done, I file the evidence — every finding, every fix, mapped to a control. A fix nobody can prove happened didn't happen, as far as an auditor's concerned.
1. What is Vulnerly, and why does this capstone use a fresh sandbox instead of just re-running the real Ledgerly incident? 2. Name the seven parts in order, and for each one, the single class of vulnerability it closes. 3. Why does Part 1 (threat modeling) have to come before Part 2's scanners run, rather than starting with tooling directly? 4. What two things both have to be true for Part 4 to count as "done"?
Check your answers
- Vulnerly is a small, deliberately vulnerable Node.js/Postgres sample app built specifically for this capstone, shaped like Ledgerly's own payments-reconciliation service but not the same system — Ledgerly's story already resolved, six weeks after its incident, with four new controls in place. Vulnerly exists so you can find and fix the same shapes of mistake yourself, hands-on, without touching (or contradicting) that already-finished story.
- 1) Threat model — closes an undocumented trust boundary. 2) SAST & secrets — closes a leaked key and a SQL injection. 3) SCA gate — closes a known-vulnerable dependency. 4) Harden & sign the container — closes a root, unsigned image. 5) IaC & policy — closes a public bucket and an open security group. 6) DAST — closes a reflected XSS and an IDOR. 7) Compliance & monitoring — closes the absence of evidence and of any alert that would catch a regression.
- A scanner only finds the shape of bug it's already configured to look for. The threat model is what identifies the SQL-injection and IDOR paths as worth looking for in the first place — without it, Parts 2 and 6 would still eventually surface both issues, but only by accident, the same way Ledgerly found its own leaked key: after the fact, from whoever got there first.
- Both must hold at once: an unsigned test image has to be actively rejected by the Kyverno admission policy, and the properly signed, hardened image has to deploy and run cleanly as a non-root user. Either one alone — a policy that blocks everything, or a clean image with no enforcement behind it — doesn't count.
Ready to start? Open Part 1 — Threat Model the App. Want the shorter, one-page version of every concept these seven parts put into practice first? Read the Ledgerly case study or the security checklist before you begin.