CLI + server toolchain for managing CTFd competitions: deploy challenges from YAML, provision ephemeral per-team containers, and detect flag sharing — all from one command.
Grab nervctf (CLI) and remote-monitor (server) from the releases page. Place both somewhere in your PATH.
challenges/
├── web/sqli/challenge.yml
├── pwn/overflow/challenge.yml
└── misc/trivia/challenge.yml
Minimal challenge.yml:
name: My Challenge
category: web
value: 100
type: standard
flags:
- flag{example}nervctf setupPrompts for target IP, SSH user, and CTFd path. Deploys Docker, CTFd, the monitor service, and the CTFd plugin via an embedded Ansible playbook. Creates .nervctf.yml with all settings.
nervctf deployValidates, diffs, and pushes your challenges to CTFd. Run --dry-run to preview changes.
Download from GitHub Releases. You need:
nervctf— for your local machine (Linux x86_64, ARM64, Windows, macOS)remote-monitor— for the CTFd server (Linux x86_64 only)
# With Nix (provides all deps)
nix develop .# --command cargo build --release
# Without Nix (Debian/Ubuntu)
sudo apt install build-essential pkg-config libssl-dev
cargo build --releaseCross-compilation: make release-musl (static), make release-arm64, make release-windows. Run make help for all targets.
Created by nervctf setup. Searched upward from --challenges-dir.
# CTFd/monitor host
monitor_ip: 1.2.3.4
monitor_port: 33133 # default: 33133
# Authentication
monitor_token: <auto-generated> # written by nervctf setup
# Deployment credentials (used by nervctf setup / setup --upgrade)
monitor_user: root # SSH user on the CTFd host
monitor_ctfd_path: /home/admin/CTFd # defaults to /home/<monitor_user>/CTFd
ssh_key_path: ~/.ssh/id_rsa.pub
# Local challenge directory
challenges_path: ./challenges
# Tuning (optional)
# NOTE: max_concurrent_provisions and max_instances_per_team are baked into the
# remote-monitor's environment at setup time. Changing them here has no effect
# until you run nervctf setup --upgrade again.
max_concurrent_provisions: 4 # parallel Docker builds/provisions
max_instances_per_team: 3 # 0 = unlimited
# CTFd public domain shown in admin dashboard links (defaults to monitor_ip)
ctfd_domain: ctfd.example.com
# Split-machine mode (optional — run containers on a separate node)
runner_ip: 192.168.1.50
runner_user: docker
runner_domain: challenges.example.com # optional — shown to players instead of runner_ipPriority (highest wins): CLI flags → environment variables → .nervctf.yml
| CLI flag | Env var | Description |
|---|---|---|
--monitor-url |
MONITOR_URL |
Remote monitor URL |
--monitor-token |
MONITOR_TOKEN |
Monitor auth token |
| Command | Description |
|---|---|
nervctf setup |
Provision server (Docker, CTFd, plugin, monitor) |
nervctf setup --upgrade |
Push updated plugin + binary, restart services |
nervctf deploy |
Create/update changed challenges |
nervctf deploy --dry-run |
Preview diff without changes |
nervctf deploy --recreate |
Force re-deploy all (rebuild images, re-sync files) |
nervctf validate |
Check challenges for errors |
nervctf validate --debug |
Full field-by-field view |
nervctf list |
List local challenges |
nervctf scan |
Scan + print statistics |
nervctf fix |
Interactively patch missing YAML fields |
Full example with all supported fields:
name: Advanced Challenge
version: "0.3"
category: web
description: Find the vulnerability.
value: 300
type: standard # standard | dynamic | instance
state: visible
connection_info: "nc challenge.example.com 1337"
attempts: 5
topics: [web, owasp-top-10] # optional freeform topic labels
flags:
- flag{simple}
- type: static
content: "flag{alt}"
data: case_insensitive
tags: [web, sql-injection]
hints:
- "Free hint"
- content: "Paid hint"
cost: 50
files:
- dist/source.py
requirements:
- "Warmup"
next: "Follow-up Challenge"type: dynamic
value: 0
extra:
initial: 500
decay: 50
minimum: 100
decay_function: linear # linear (default) | logarithmictype: instance provisions ephemeral containers per team. See docs/instance-challenges.md for the full reference.
type: instance
value: 0
extra: { initial: 500, decay: 50, minimum: 100 }
instance:
backend: docker # docker | compose | lxc
image: . # local path or registry image
internal_ports: [1337] # array; multi-port: [80, 443] — each gets a random host port
connection: nc
flag_mode: random
timeout_minutes: 45Runs on the CTFd host. Writes directly to MariaDB (no CTFd API calls), manages instance lifecycle, and serves the admin dashboard.
CLI ──token──▶ remote-monitor:33133 ──SQL──▶ CTFd MariaDB
│
instance manager
┌─────────┴─────────┐
local split-machine
(docker daemon) (SSH to runner node)
Admin dashboard: http://<host>:33133/admin?token=<TOKEN>
See docs/remote-monitor.md for env vars, routes, and API reference.
| Symptom | Fix |
|---|---|
| No challenges found | Files must be <category>/<name>/challenge.yml (max depth 5) |
| File upload 500 | chown -R 1001:1001 <CTFd>/.data/CTFd/uploads |
state: Field may not be null |
Run nervctf fix |
| Monitor 401 | MONITOR_TOKEN mismatch between CLI and server |
ansible-playbook not found |
Run inside nix develop .# |
make check # cargo check
make test # unit tests
make fmt # rustfmtSee ARCHITECTURE.md for the full system reference and docs/ for per-module documentation.
MIT License. See LICENSE for details.