Closed 2026-08-28. Tracking issue for on-demand, per-team containerised challenge instances. The Docker MVP (Phases 0–1) and the unique per-instance flags slice (Phase 2a) have shipped to main; the remaining phased work is split into dedicated issues (below). This issue is now the record of what landed. Full design lives in ADR-0036 (docs/adr/0036-challenge-instancing-provisioners.md).
What this delivered
The optional Challenge Instances module (ADR-0036) — off by default, toggled per competition, inert until an operator configures a provisioner. It hands each team (or user, in individual mode) an isolated, running instance of a challenge with live connection details, on a pluggable provisioner backend.
Phase 0 — Foundations
- Provisioner kind registry + backend-agnostic contract (
create/status/endpoints/destroy/list) — a new backend is a new kind, not a fork.
- Data model + migrations for
challenge_deployment and challenge_instance (no impact on existing challenge queries).
Phase 1 — Docker MVP (verified live against a real Docker daemon: launch → connect → extend → destroy)
- Hardened
DockerProvisioner over a least-privilege socket proxy (CapDrop ALL, no-new-privileges, ReadonlyRootfs, tmpfs-only writable).
- Background-lane instance lifecycle (
requested → provisioning → running → expiring → destroyed, terminal failed) with a TTL reaper + orphan GC on the existing scheduler — no new process.
- TCP exposure + host-port allocation (configurable range; exhaustion is a clean, evented refusal).
- Competitor launch / extend / destroy API + WebSocket status push; competitor challenge-modal UI (launch, expiry countdown, connection info, extend).
- Staff ops view (running instances, health, force-kill, audited).
- Site infra settings + a staged Test connection validator; RBAC (
instance_launch / instance_manage / instance_view, and manage_instance_infra for site config).
- Egress-deny is a host-firewall responsibility, not a Docker internal network (ADR-0036 amended after live testing — an internal network can't publish TCP ports).
Phase 2a — Unique per-instance flags (PR #318, ADR-0036 §3)
flag_mode: unique_per_instance — a fresh flag rendered from a template (<random> → random token) at provision time, injected into the container once, with only the salted hash stored (plaintext never persisted).
- Grading against the submitting subject's live instance(s); static / regex / MCQ paths and first-blood / decay unchanged.
challenge.flag_shared_detected — a wrong submission carrying another subject's live flag is signalled to staff + automation as provable sharing (no automatic penalty).
Docs: docs/CHALLENGE_INSTANCES.md (infra + authoring guide) and ADR-0036.
Delivered checklist
Follow-up work — split into dedicated issues
Delivered across ADR-0036 and PRs #303–#315 (Docker MVP) and #318 (unique flags).
What this delivered
The optional Challenge Instances module (ADR-0036) — off by default, toggled per competition, inert until an operator configures a provisioner. It hands each team (or user, in individual mode) an isolated, running instance of a challenge with live connection details, on a pluggable provisioner backend.
Phase 0 — Foundations
create/status/endpoints/destroy/list) — a new backend is a new kind, not a fork.challenge_deploymentandchallenge_instance(no impact on existing challenge queries).Phase 1 — Docker MVP (verified live against a real Docker daemon: launch → connect → extend → destroy)
DockerProvisionerover a least-privilege socket proxy (CapDrop ALL,no-new-privileges, ReadonlyRootfs, tmpfs-only writable).requested → provisioning → running → expiring → destroyed, terminalfailed) with a TTL reaper + orphan GC on the existing scheduler — no new process.instance_launch/instance_manage/instance_view, andmanage_instance_infrafor site config).Phase 2a — Unique per-instance flags (PR #318, ADR-0036 §3)
flag_mode: unique_per_instance— a fresh flag rendered from a template (<random>→ random token) at provision time, injected into the container once, with only the salted hash stored (plaintext never persisted).challenge.flag_shared_detected— a wrong submission carrying another subject's live flag is signalled to staff + automation as provable sharing (no automatic penalty).Docs:
docs/CHALLENGE_INSTANCES.md(infra + authoring guide) and ADR-0036.Delivered checklist
challenge_deployment,challenge_instance)DockerProvisioner(single host) via a least-privilege socket proxyinstance_launch/instance_manage/instance_view/manage_instance_infraFollow-up work — split into dedicated issues
KubernetesProvisioner+ ctfcli portability + a security-hardening pass (egress control, metadata blocking, seccomp/caps, threat model).Delivered across ADR-0036 and PRs #303–#315 (Docker MVP) and #318 (unique flags).