Ops CLI reference (cli.py)¶
The ops CLI is the primary automation interface for CTFHive. It wraps Flask app context, the Docker SDK, and the SSH client to let you manage every aspect of a running event from the command line.
Entry points:
# Preferred (uses the project virtualenv):
uv run python cli.py <group> <command> [OPTIONS]
# Alias (same file, different name):
python ctfctrl.py <group> <command> [OPTIONS]
Global options¶
Every command accepts these two flags:
| Flag | Default | Description |
|---|---|---|
--app-env TEXT |
development |
Flask config profile. Set to production for live deployments. Reads APP_ENV from the environment if not passed. |
--json |
false |
Emit structured JSON instead of pretty-printed output. Use this in scripts and CI. |
env — environment validation¶
Validate that a config profile satisfies the requirements for a given server role. Run this before any deployment to catch missing env vars early.
env validate¶
| Argument / Flag | Description |
|---|---|
ROLE (positional) |
One of role-a / registry, role-b / control-plane, role-c / vpn |
--app-env |
Config profile to validate (default: development) |
--json |
Structured output |
Returns a validation report. Exit code 1 if validation fails.
Example:
db — database bootstrap¶
db bootstrap¶
Create all database tables (SQLAlchemy create_all). Safe to re-run — additive
only, never drops tables.
Returns the active database URI and a schema health summary.
admin — admin account management¶
admin bootstrap¶
Create or update the admin user. Idempotent: if the username already exists it is updated to the provided email and password.
uv run python cli.py admin bootstrap \
--username TEXT \
--email TEXT \
--password TEXT \
[--app-env TEXT] [--json]
| Flag | Required | Description |
|---|---|---|
--username |
Yes | Admin username |
--email |
Yes | Admin email address |
--password |
Yes | Admin password (plaintext; hashed on write) |
challenge — challenge lifecycle¶
challenge validate¶
Parse and validate one or more challenge YAML manifests without touching the database.
| Argument | Description |
|---|---|
MANIFEST (positional, repeatable) |
Path(s) to challenge YAML files |
Reports slug, title, whether the challenge is dynamic, the file list, and whether a lab stack spec is present.
challenge import¶
Parse and upsert challenge(s) into the database.
challenge build¶
Build local Docker images for a challenge's container(s). The challenge must
already be imported. Finds Dockerfile contexts using CHALLENGES_ROOT config.
| Argument / Flag | Description |
|---|---|
CHALLENGE_REF |
Challenge slug (e.g. ecbeast) or database ID |
--dry-run |
Print what would be built without running Docker |
challenge push¶
Push a challenge's image(s) to the configured registry.
uv run python cli.py challenge push <CHALLENGE_REF> \
[--registry-host TEXT] [--dry-run] [--app-env TEXT] [--json]
| Flag | Description |
|---|---|
--registry-host |
Override the registry host from config |
--dry-run |
Print what would be pushed without calling the Docker API |
registry — registry checks¶
registry check¶
List all challenge images and their local Docker source directories. Shows the
effective registry host derived from SITE_URL / REGISTRY_HOST.
registry login¶
Authenticate the Docker daemon to the configured registry using REGISTRY_USER
/ REGISTRY_PASS.
registry list¶
List all challenge images known to the database (same output as registry check
without the registry host).
instance — lab instance lifecycle¶
All instance commands require exactly one principal selector:
| Flag | Description |
|---|---|
--principal-id INT |
Direct principal ID |
--user-id INT |
Resolve the active principal for this user |
--team-id INT |
Resolve the active principal for this team |
instance spawn¶
Spawn a dynamic challenge container for a principal. Requires LAB_ENABLED=true
and the challenge to be dynamic.
uv run python cli.py instance spawn <CHALLENGE_REF> \
(--principal-id INT | --user-id INT | --team-id INT) \
[--app-env TEXT] [--json]
Fails with a clear error if the principal already has a running instance for that challenge.
instance destroy¶
Stop and remove a running instance.
uv run python cli.py instance destroy <CHALLENGE_REF> \
(--principal-id INT | --user-id INT | --team-id INT) \
[--app-env TEXT] [--json]
instance reset¶
Destroy the existing instance (if any) and immediately spawn a fresh one.
Equivalent to destroy + spawn in one command.
uv run python cli.py instance reset <CHALLENGE_REF> \
(--principal-id INT | --user-id INT | --team-id INT) \
[--app-env TEXT] [--json]
instance reap¶
Run the expired-instance reaper: stops all instances whose TTL has elapsed.
Requires LAB_ENABLED=true.
Returns the running instance count before and after reaping.
vpn — WireGuard operations¶
vpn status¶
Display current WireGuard state via the admin telemetry service.
vpn reconcile¶
Walk all running instances that have lab metadata, extract WireGuard peer bundles, and sync them to the WireGuard manager. Use this after a gateway restart to restore peers without re-spawning instances.
Reports how many peers were synced and any errors.
vpn test¶
Status: stub
vpn test currently calls the same underlying function as vpn status
(returns AdminTelemetryService().vpn_overview()). It does not perform any
live end-to-end connectivity test. This is a known gap — a real test would
attempt a WireGuard handshake to a probe peer. Use vpn status for now.
worker — lab worker management¶
worker list¶
List all registered lab workers and their state (requires LAB_ENABLED=true
and a running lab controller).
worker drain¶
Mark a worker as draining (no new instances will be scheduled to it) or un-drain it. Used before taking a worker host down for maintenance.
| Argument / Flag | Description |
|---|---|
WORKER_ID |
Worker identifier as returned by worker list |
--undrain |
Restore the worker to active scheduling |
deploy — remote deployment helpers¶
These commands connect to a remote host via SSH and run a small set of commands
appropriate to the server's role. They are thin wrappers around SSH — not full
provisioning. For complete automated provisioning, use the provisioner/ package
(see Provisioning tenants).
All deploy commands accept:
| Flag | Default | Description |
|---|---|---|
--host |
(required) | Remote hostname or IP |
--user |
(required) | SSH username |
--port |
22 |
SSH port |
--dry-run |
false |
Print commands without executing |
--json |
false |
Structured output |
deploy role-a — registry node¶
Status: placeholder
deploy role-a only runs two commands on the remote host:
mkdir -p <remote_path> and an echo. It does not install or configure
the registry. Use provisioner/ phases or server-setup.md for real
registry setup.
uv run python cli.py deploy role-a \
--host TEXT --user TEXT \
[--remote-path /opt/ctf-registry] \
[--domain TEXT] [--port INT] [--dry-run] [--json]
deploy role-b — app / control-plane node¶
Pulls the latest images and runs docker compose up -d --build on the remote
host. Assumes the compose file is already in project_dir.
uv run python cli.py deploy role-b \
--host TEXT --user TEXT \
[--project-dir /opt/ctf-ui] \
[--compose-file docker-compose.yml] \
[--port INT] [--dry-run] [--json]
deploy role-c — VPN / WireGuard node¶
Restarts the wg-quick@<interface> systemd service and shows its status.
uv run python cli.py deploy role-c \
--host TEXT --user TEXT \
[--interface wg0] [--port INT] [--dry-run] [--json]
health — operational health¶
health all¶
Run all health checks in one shot: role-b config validation, schema health, Docker daemon status, VPN overview, and worker list.
This is the recommended preflight check before opening an event to players. The three-server validation runbook (three-server-validation.md) shows how to integrate it into a full staging gate.