§4.2 calls for target VMs we BUILD, not VMs we fetch. §4.13 demands
every target ship the same isolation posture (no upstream egress, no
host-shared FS, unprivileged QEMU, fresh snapshot per episode). This
commit lands the infrastructure for both.
New surface:
* orchestrator/target_spec.py
Loads + validates `vm/targets/<name>/spec.toml`. Containment
fields are not knobs — each has exactly ONE safe value, and a
spec asserting the unsafe value is rejected at load time. There's
no `--containment-override`; weakening §4.13 requires amending
PIPELINE.md and operator sign-off.
* tools/build_target.py
Orchestrates build → verify → publish for a single target. Spec
invalid → exit 78 (sysadmin error). build.sh failure → image not
published. verify.sh failure → image discarded; that's the §4.2
acceptance gate. Publishes sha256 + the manifest.toml stanza the
operator copies in to admit the image (§16 substantive amendment
with sign-off per §15).
* vm/targets/<name>/{spec.toml,build.sh,verify.sh}
Template structure. spec.toml is the contract; build.sh produces
$OUT_PATH; verify.sh boots the produced image under the §4.13
containment posture and asserts every promise.
* vm/targets/shellshock/
First real working target. CVE-2014-6271 (Apache mod_cgi + bash
4.2 mis-parsing function-export environment values). Replaces
the SourceForge Metasploitable2 path that §3 evidence proved
unverifiable. Bash 4.2 is built from sha256-pinned GNU source
inside an Alpine 3.21 cloudinit guest; the build script asserts
the produced bash actually triggers shellshock; the verifier
re-asserts it under restrict=on with a real CVE-2014-6271 probe.
* vm/targets/README.md
How operators add a target. Walks the spec → build → verify →
manifest amendment loop.
Containment regression tests (tests/test_containment.py) — 20 new
assertions, parameterized over every target with a build/verify trio:
* verify.sh MUST contain `restrict=on` on its netdev (§4.13)
* verify.sh MUST contain `snapshot=on` on the boot drive (§4.13)
* verify.sh + build.sh MUST NOT contain -virtfs / -fsdev / 9pfs
* verify.sh + build.sh MUST NOT wrap qemu-system in `sudo`
* Every target must ship the complete spec.toml + build.sh + verify.sh
trio — no half-built targets (§1 default-to-removal)
Spec validation tests (tests/test_target_spec.py): 13 new tests over
spec parse, name/dir mismatch, missing fields, out-of-range port, and
the §4.13 containment field validators (each unsafe value rejected
with a clear error).
The shellshock target's image is NOT yet published to manifest.toml's
[[targets.images]] — that's the §15 sign-off amendment that lands
after a successful operator-driven build_target.py run on a lab host
with KVM. Building takes ~10 min on x86_64; cannot run on the Pi
under TCG. Operator drives the first build, verifies the sha256, then
amends manifest.toml in a follow-up commit.
261 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| shellshock | ||
| README.md | ||
Target VM build specs (PIPELINE.md §4.2 / §4.13)
Every Tier-3 module in manifest.toml [catalog] MUST land its session
against a target VM that this directory defines. Targets are NOT
fetched from third-party blob stores (no Metasploitable2 from
SourceForge — that was the §3 evidence); they are built locally from
declarative specs, sha256-pinned, and re-verified at every release.
Layout
vm/targets/<name>/
├── spec.toml ← what this target promises (verified at load time)
├── build.sh ← declarative build steps; produces $OUT_PATH
└── verify.sh ← boots the produced image, asserts every promise
Adding a target
-
Create
vm/targets/<name>/. -
Write
spec.toml. Every field is required; containment fields all have ONE safe value (no knobs):name = "<name>" description = "<short prose>" base_image = "<e.g. alpine-3.21-virt>" [promises] cve = "CVE-YYYY-NNNN" service_name = "samba" # what the module catalog talks to service_port = 445 service_proto = "tcp" vulnerable_software = "samba" # the actual vulnerable component vulnerable_version = "3.0.20" [containment] upstream_egress = false # MUST shared_filesystem = false # MUST unprivileged_qemu = true # MUST fresh_snapshot_per_episode = true # MUST -
Write
build.sh. The orchestrator invokes it withOUT_PATH=<staging>.qcow2andBASE_IMAGE_NAME=<base>. The script should produce a valid qcow2 at$OUT_PATHand exit 0. -
Write
verify.sh. The orchestrator invokes it withIMAGE_PATH=<staging>.qcow2andEXPECTED_*env vars matching spec.promises. Boot the image in a containment-correct configuration (see "Verification harness" below), wait for the service to come up, assert the promised port + version. Exit 0 only if every promise verifies. -
Run the build:
sudo python tools/build_target.py <name>On success the script prints the sha256 + the manifest.toml stanza to add. Build artifacts go to
/var/lib/cis490/vm/images/by default. -
Operator amends
manifest.toml:[[targets.images]] image_name = "<name>" sha256 = "<from build_target output>" build_script = "vm/targets/<name>/build.sh"This is a substantive amendment per §16 — operator sign-off required. Lands in the same merge as any modules that depend on the target.
Verification harness
verify.sh MUST boot the image with the §4.13 containment posture:
-netdev user,...,restrict=on— no upstream egress- No
-virtfs/-fsdev/-9pfshost-shared mounts - Run QEMU as the unprivileged service user (no
sudo qemu-system-*) snapshot=onso the build artifact isn't mutated by verification
A tests/test_containment.py regression asserts every spec on disk
declares the correct containment posture. A spec that asserts
weakened containment is a containment regression and load_target_spec
rejects it before build_target.py even invokes build.sh.
Why this exists
Targets we don't build, we don't trust. PIPELINE.md §3 surfaced 0/67 session_open against the SourceForge Metasploitable2 image — and we couldn't even tell whether that was a payload bug, a hostfwd bug, a SLIRP timing race, or just the image being modified somewhere along the supply chain. With locally-built declarative targets:
- The vulnerable service is verified up at the promised port + version BEFORE the image is admitted.
- The image's sha256 is recorded in manifest.toml; tampering is visible.
- Build is reproducible: same spec.toml + same build.sh on a fresh base produces the same image.
This is non-negotiable per §4.2 / §4.3. Tier-3 modules that target unverified images stay out of the catalog.