Skip to content

Hands-on lab environment

Isolated Docker Compose lab for this course. All published ports bind to 127.0.0.1. The application network labnet is internal: true (no internet egress). Simulated attacks must target loopback; attack-sim/simulate.py exits if they do not.

AUTHORIZED LAB USE ONLY. Read ethics.md.

Architecture

flowchart TB
    workstation["learner workstation"] -- "127.0.0.1:8080 / 8090 / 8091" --> edgenet

    subgraph edgenet["edgenet — localhost publish, may have DNS/egress"]
        direction LR
        api["notes-api"]
        soc["soc-lite"]
        agent["agentic-soc"]
    end

    api --- labnet
    soc --- labnet
    agent --- labnet

    subgraph labnet["labnet (internal, 172.30.0.0/24) — east-west, no internet"]
        imds["mock-imds (synthetic metadata)"]
    end

    labnet --> volumes["volumes: logs, sqlite, cases"]

notes-api, soc-lite, and agentic-soc are dual-homed (edgenet + labnet). labnet being internal: true does not prove the API process has no DNS or egress: edgenet is a normal bridge. The /fetch allowlist is the application rail. See How defenders think — bulkheads only count if they actually partition the failure.

Resource requirements

Profile RAM Disk Required
Default compose stack ~1.0–1.5 GiB ~2 GiB images Yes for most labs
Host-only (no Docker), Python venv ~300 MiB ~200 MiB Alternative
Optional Grafana/Loki (not in default compose) +512 MiB +1 GiB No
Optional kind/k3d (module 5) +2 GiB +2 GiB No
Optional local LLM (Ollama) +4 GiB model size No

Runtime labs use the predict → run → compare loop in How defenders think. Design and writing labs predict findings, then compare. Do not invent extra labs.

Quick start

chmod +x labs/scripts/*.sh
./labs/scripts/lab-up.sh
curl -s http://127.0.0.1:8080/health
curl -s http://127.0.0.1:8080/.well-known/lab

Lab users (dummy passwords, local only):

User Password Role
alice alice-lab-password user
bob bob-lab-password user
admin admin-lab-password admin
# Simulated activity (local only)
python3 labs/attack-sim/simulate.py --scenario all

# Build alerts
curl -s -X POST http://127.0.0.1:8090/ingest
curl -s http://127.0.0.1:8090/alerts | python3 -m json.tool

# Agentic assistant (no response action without APPROVE)
ALERT_ID=$(curl -s http://127.0.0.1:8090/alerts | python3 -c "import sys,json; print(json.load(sys.stdin)['alerts'][0]['id'])")
curl -s -X POST http://127.0.0.1:8091/investigate -H 'Content-Type: application/json' \
  -d "{\"alert_id\":\"$ALERT_ID\"}" | python3 -m json.tool

Tooling map

Tool Role Lightweight alternative Required
Docker / Compose Isolation and reproducible services Podman Compose, or run Python apps in venvs Required (or venv alternative)
notes-api Intentionally dual-mode web API Required
mock-imds Synthetic cloud metadata Static JSON file Required for SSRF lab
soc-lite Log ingest, detections, cases jq + files Required from module 7
agentic-soc Policy-bound assistant Manual playbook reading Required from module 12
attack-sim Authorized local traffic generator curl Required from module 4 (used again in 7+)
curl, python3 Investigation jq (optional, nicer JSON) Required
tcpdump / tshark Packet capture on docker bridge Compose logs only Optional
Trivy / Grype Image and FS scanning pip-audit Optional
kind or k3d Local Kubernetes Skip module 5 k8s lab Optional
Ollama or hosted LLM Natural-language summaries Deterministic planner (default) Optional

The optional toolbox image is built in CI (.github/workflows/toolbox-image.yml).

Modes

  • LAB_MODE=true (default): application-level vulnerabilities enabled for teaching.
  • LAB_MODE=false: owner checks, parameterized search, metadata fetch blocked, bcrypt passwords, JWT expiry.

The lab safety rail remains on in both modes: /fetch cannot target hosts outside the compose allowlist (mock-imds, metadata.internal, and other compose service names) and only http on ports 80, 8080, 8090, or 8091. In LAB_MODE=false the OpenAPI UI (/docs, /openapi.json) is also removed — that is attack-surface reduction, not a network bulkhead.

No-Docker / venv alternative: Docker Compose is still preferred. If you cannot run containers, install the unified host pins and nothing else:

python3 -m venv .venv
.venv/bin/pip install -r requirements-labs.txt

requirements-labs.txt is the union of labs/notes-api/requirements.txt, labs/soc-lite/requirements.txt, and labs/agentic-soc/requirements.txt, plus pytest. Then run mock-imds on PORT=18080, set LAB_FETCH_EXTRA_HOSTS=127.0.0.1 only on your workstation, and still bind APIs to 127.0.0.1. Never point LAB_FETCH_EXTRA_HOSTS at a non-lab host.

Cleanup

./labs/scripts/lab-down.sh     # stop
./labs/scripts/lab-reset.sh    # stop and wipe volumes