Skip to content

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

uv run python cli.py env validate <ROLE> [--app-env TEXT] [--json]
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:

uv run python cli.py env validate role-b --app-env production --json

db — database bootstrap

db bootstrap

Create all database tables (SQLAlchemy create_all). Safe to re-run — additive only, never drops tables.

uv run python cli.py db bootstrap [--app-env TEXT] [--json]

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.

uv run python cli.py challenge validate <MANIFEST> [<MANIFEST> ...] [--json]
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.

uv run python cli.py challenge import <MANIFEST> [<MANIFEST> ...] [--app-env TEXT] [--json]

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.

uv run python cli.py challenge build <CHALLENGE_REF> [--dry-run] [--app-env TEXT] [--json]
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.

uv run python cli.py registry check [--app-env TEXT] [--json]

registry login

Authenticate the Docker daemon to the configured registry using REGISTRY_USER / REGISTRY_PASS.

uv run python cli.py registry login [--app-env TEXT] [--json]

registry list

List all challenge images known to the database (same output as registry check without the registry host).

uv run python cli.py registry list [--app-env TEXT] [--json]

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.

uv run python cli.py instance reap [--app-env TEXT] [--json]

Returns the running instance count before and after reaping.


vpn — WireGuard operations

vpn status

Display current WireGuard state via the admin telemetry service.

uv run python cli.py vpn status [--app-env TEXT] [--json]

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.

uv run python cli.py vpn reconcile [--app-env TEXT] [--json]

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.

uv run python cli.py vpn test [--app-env TEXT] [--json]

worker — lab worker management

worker list

List all registered lab workers and their state (requires LAB_ENABLED=true and a running lab controller).

uv run python cli.py worker list [--app-env TEXT] [--json]

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.

uv run python cli.py worker drain <WORKER_ID> [--undrain] [--app-env TEXT] [--json]
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.

uv run python cli.py health all [--app-env TEXT] [--json]

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.