Education › Security › Stage 4: Operations & response

Capstone: secure a service end to end

Threat-model, harden, scan, monitor and write the incident runbook for a real web service.

Advanced ~40 min read Module 16 of 16

Fifteen modules have each handed you one layer. The capstone is where you stack them on a real service and find out which ones actually hold. You will take a small web service — the DevOps track's zero-to-production API is ideal, but any service you can deploy works — and carry it through the full cycle: threat model, harden, scan, sign, monitor, detect, respond, and prove it with evidence. The result is a repository and a written record that show a hiring manager, an auditor or your future self exactly how you make a service defensible. Nothing here is new; the skill being built is doing all of it, in order, without skipping the steps that feel like paperwork.

After this module you can
  • Produce a threat model for a real service and derive its control list from it
  • Apply identity, network, host, container and secrets controls to the service and verify each with a test
  • Put scanning, signing and admission verification into the service's pipeline
  • Instrument security logging and at least three working detections with runbooks
  • Run a simulated incident end to end and write the postmortem and the evidence pack

The target and the deliverables

Choose a service with the shape of something real: an HTTP API with authentication, a database, at least one secret (an API key or a database password), a container image, a pipeline and a deployed environment you control. The zero-to-production project supplies all of that. Keep a security/ directory in the repository for everything you produce, because the deliverable is as much the record as the configuration.

DeliverableWhere it livesModule it comes from
THREAT_MODEL.md with register and controlssecurity/Threat modelling
Hardened infrastructure and workload config, with testsinfra/, deploy/, tests/security/Identity, network, hardening, containers, secrets
Pipeline with gates, SBOM, signature, provenance, admission check.github/workflows/Supply chain, DevSecOps
Security events, detections and runbookssecurity/detections/, security/runbooks/Logging and detection
Incident timeline and postmortem from the drillsecurity/incidents/Incident response
Control records with evidence scriptssecurity/controls/Compliance
Note

Budget two to three focused days. Do the phases in order; each one's output is the next one's input, and the threat model is what stops this from becoming an unbounded checklist.

Phase 1: model, then decide

Run the one-hour threat model from the first module against the service as deployed, not as designed. Draw the data flow diagram with trust boundaries, list assets in rank order, enumerate entry points including the pipeline and the dependency manifest, walk STRIDE, and rate. Take the top eight threats and, for each, write the response and the specific control with a test name. This list is your work plan for the next phases; anything not on it is optional.

The register you should end up with for a small API. Yours will differ; the shape should not.
text
T-01 E1 Elevation: read another user's order by id            -> MITIGATE  scoped queries      test_orders_other_user_404
T-02 E1 Spoofing: forged JWT accepted                           -> MITIGATE  pinned alg, aud, exp test_jwt_wrong_alg_rejected
T-03 E4 Tampering: unsigned image deployed                      -> MITIGATE  cosign + admission   test_admission_rejects_unsigned
T-04 E5 Info disclosure: DB password in image layer             -> ELIMINATE runtime secret fetch test_image_has_no_secrets
T-05 E2 Spoofing: forged payment webhook                        -> MITIGATE  HMAC + timestamp     test_webhook_bad_sig_rejected
T-06 --  Elevation: pod escape via root + hostPath              -> MITIGATE  restricted PSS       test_pod_spec_restricted
T-07 --  Info disclosure: DB reachable from CI runners          -> MITIGATE  sg references + NP   test_db_unreachable_from_ci
T-08 --  Repudiation: admin deletes with no record              -> MITIGATE  security events      test_admin_delete_emits_event

Phase 2: harden and prove it

Work down the register. For each control, make the change and write the test that would fail if the control were removed. Application controls get unit or integration tests (tests/security/). Infrastructure controls get policy tests on the Terraform (checkov rules or a small script that parses the plan) and, where possible, a live probe (nc -zv from the wrong place must fail). Kubernetes controls get a conformance test: apply a deliberately bad manifest and assert rejection.

A test that proves the container image carries no secret and runs as non-root; it fails the build if someone regresses the Dockerfile.
python
import json
import subprocess

IMAGE = "ghcr.io/acme/orders:test"


def inspect(image: str) -> dict:
    out = subprocess.run(["docker", "image", "inspect", image], capture_output=True, text=True, check=True).stdout
    return json.loads(out)[0]


def test_image_runs_as_non_root():
    user = inspect(IMAGE)["Config"].get("User", "")
    assert user not in ("", "0", "root"), f"image runs as root (User={user!r})"


def test_image_has_no_secret_shaped_env():
    env = inspect(IMAGE)["Config"].get("Env") or []
    bad = [e for e in env if any(k in e.upper() for k in ("PASSWORD", "SECRET", "TOKEN", "AKIA"))]
    assert not bad, f"secret-shaped environment in image: {bad}"


def test_image_layers_contain_no_private_keys():
    r = subprocess.run(["trivy", "image", "--scanners", "secret", "--exit-code", "1", "--quiet", IMAGE],
                       capture_output=True, text=True)
    assert r.returncode == 0, r.stdout

The minimum set for most services: SSO plus phishing-resistant MFA for anything administrative; authorisation scoped at the data layer; security group references and a default-deny NetworkPolicy; a hardened host image with auditd and automatic updates; a distroless non-root image under the restricted profile; secrets fetched at runtime by workload identity; TLS on every hop. Record each as a control with owner and evidence as you go — it costs minutes now and saves days later.

Phase 3: the pipeline as the only road

Extend the service's pipeline until nothing reaches production that has not been checked, inventoried, signed and verified. Two-tier gates on pull requests (secrets, diff-scoped SAST, dependency audit, IaC policy), image scanning by digest at build, SBOM generation and attestation, keyless signing, provenance, and an admission policy in the cluster (or an equivalent check in the deploy step for a non-Kubernetes target) that refuses unsigned images. Pin every action, minimise token permissions, and confirm fork pull requests receive no secrets.

The end-to-end proof: an unsigned image must be refused by the cluster, and the signed one from the pipeline must be accepted.
bash
# an image built outside the pipeline, pushed by hand, with no signature
docker build -t ghcr.io/acme/orders:rogue . && docker push ghcr.io/acme/orders:rogue
kubectl -n shop set image deployment/orders app=ghcr.io/acme/orders:rogue
# expected: admission webhook denies the change; the running pods are unchanged
kubectl -n shop get events --field-selector reason=PolicyViolation | tail -3

# the pipeline-built, signed image by digest is accepted
cosign verify --certificate-identity-regexp '^https://github.com/acme/orders/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/acme/orders@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 >/dev/null && echo "signed by our workflow"
Tip

Keep the pull request pipeline under five minutes. Everything slower runs on merge or on a schedule. A capstone that produces a pipeline nobody would tolerate has not solved the problem.

Phase 4: see it, then break in

Add structured security events to the application for authentication results, authorisation denials, admin actions and data exports. Ship them, the host auth and audit logs, and the cloud audit trail to central storage. Write at least three detections with runbooks: for example, five failed logins then a success from one address; an authorisation denial burst from one user (id enumeration); a shell spawned in an application container; a new IAM user or key created outside the pipeline's identity. Then test them: trigger each with a script or an emulation tool and record whether and how fast the alert arrived.

Now run the incident drill. Have a colleague (or a script you wrote earlier and forgot the details of) do one realistic thing: leak a test credential into a public paste, add an SSH key to the host, or exploit a deliberately re-introduced injection bug in a branch deployed to staging. Respond as the incident module describes: declare, roles, timeline, evidence, scope, contain, eradicate, recover. Time yourself. Write the postmortem with dwell time and time to containment, and the detection or control that would have caught it sooner.

A minimal detection test harness: trigger the behaviour, poll for the alert, record the latency.
bash
#!/usr/bin/env bash
set -euo pipefail
START=$(date -u +%s)

# behaviour: five failed logins then a success from this machine
for i in 1 2 3 4 5; do curl -s -o /dev/null -X POST https://staging.example.com/login -d 'user=alice&pass=wrong'; done
curl -s -o /dev/null -X POST https://staging.example.com/login -d "user=alice&pass=$ALICE_PASS"

# poll the alert API (adapt to your SIEM) for up to 10 minutes
for _ in $(seq 1 60); do
  if curl -s "$ALERT_API/search?q=rule:brute-force-then-success&since=$START" | grep -q '"count":[1-9]'; then
    echo "alert fired after $(( $(date -u +%s) - START ))s"; exit 0
  fi
  sleep 10
done
echo "NO ALERT within 10 minutes"; exit 1

Phase 5: the evidence pack and the write-up

Finish by making the work legible to someone who was not there. The security/controls/ directory holds one record per control with its evidence script; run the scripts once and commit the dated output for the capstone period. security/README.md is the write-up: the service, the threat model summary, the controls and how each is tested, the pipeline guarantees, the detections and their measured latency, the drill's timeline and postmortem, and an honest list of what is not covered and why. This is the document you link from a CV or hand to an auditor; write it for that reader.

  • Every claim in the write-up points at a file, a test or a dated evidence export.
  • The threat register shows status for all eight threats, including any you accepted, with reasons.
  • The postmortem names dwell time, time to containment and one concrete improvement you then implemented.
  • The gaps section is real: nobody believes a service with no residual risk, and stating it shows judgement.

When it is done, you will have exercised every layer of this track on one system and seen how they interact: the threat model chose the controls, the controls produced the evidence, the pipeline enforced the integrity, the detections turned logs into alerts, the drill tested the response, and the record turned all of it into something you can show. That loop — model, harden, verify, detect, respond, record — is the job.

Hands-on practice

Complete the capstone

  1. Phase 1 (2 hours): threat-model the deployed service; commit security/THREAT_MODEL.md with a register of at least eight rated threats, each with response, control and test name.
  2. Phase 2 (half a day): implement the top eight controls across identity, network, host, container and secrets; write a test for each under tests/security/ or as an infrastructure policy check; all must pass in CI.
  3. Phase 3 (half a day): add the two-tier security workflow, image scanning by digest, SBOM attestation, keyless signing, provenance and an admission or deploy-time signature check. Prove an unsigned image is refused.
  4. Phase 4a (2 hours): add security events to the application, ship logs centrally, write three detections with runbooks, and measure alert latency for each with a trigger script.
  5. Phase 4b (2 hours): run the incident drill against staging with a colleague or a pre-written script; keep the timeline; write the postmortem with dwell time, time to containment and one implemented improvement.
  6. Phase 5 (2 hours): write security/README.md linking every claim to evidence, run the control evidence scripts once, commit the outputs, and list residual risks honestly.
  7. Ask someone who did not build it to read the README and try to break one control. Fix what they find, add it to the postmortem section, and you are done.
Cheat sheet

Capstone: secure a service end to end — at a glance

Main things to focus on

  • Model first: the top eight rated threats are the work plan, not a generic checklist
  • Every control has a test that fails when the control is removed
  • The pipeline is the only road to production: gates, SBOM, signature, provenance, admission
  • Detections are measured (alert latency) and tested by triggering the behaviour
  • The drill is real: timeline, dwell time, containment time, one implemented improvement
  • The write-up links every claim to a file; residual risks are stated

Repository layout

security/THREAT_MODEL.mdDFD, assets, entry points, STRIDE register with status
tests/security/Tests per control; run in CI
security/controls/CONTROL-*.md + evidence/Control records and dated evidence exports
security/detections/*.yml + security/runbooks/Sigma rules and what to do when they fire
security/incidents/YYYY-MM-DD-drill.mdTimeline and postmortem
security/README.mdThe write-up for an outside reader

Minimum control set

SSO + WebAuthn for admin; authz scoped at data layerIdentity
sg references, default-deny NetworkPolicy, egress allow-listNetwork
hardened image, auditd, unattended-upgrades, no SSH from internetHost
distroless non-root, restricted PSS, scoped RBACContainer
runtime secret fetch by workload identity; no secrets in layers or envSecrets
TLS every hop; security headersTransport and browser

Pipeline guarantees

PR: gitleaks, diff SAST (ERROR), pip-audit, checkovFast blocking tier
merge: trivy by digest, syft SBOM, cosign sign + attest, provenanceBuild-time integrity
deploy: admission verifies identity + issuer; digests onlyUnsigned is refused
pinned actions, contents: read default, no secrets for forksPipeline hardening

Detect and respond

events: auth.login.*, authz.denied, admin.*, data.export.*Application security telemetry
3+ detections with runbooks, latency measuredProve they fire
drill: declare, roles, timeline, evidence, scope, contain, eradicate, recoverThe incident loop
postmortem: dwell time, time to contain, improvement shippedNumbers that should improve

Common pitfalls

  • Skipping the threat model and applying every control you can think of; the write-up cannot explain why.
  • Controls without tests; a month later half of them have silently regressed.
  • A signing setup that is never verified anywhere, so unsigned images still run.
  • Detections that were written but never triggered; you do not know if they work.
  • A drill where the responder also planted the incident and knows exactly where to look.
  • A README with claims that point at nothing.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →