Configuration¶
CTFHive is configured entirely through environment variables. The app reads them from a .env file in the working directory (via python-dotenv) or from the process environment. Source of truth: ctfapp/config.py.
The config class hierarchy is:
Config (base)
├── DevelopmentConfig APP_ENV=development (default)
├── ProductionConfig APP_ENV=production
└── TestingConfig APP_ENV=testing
DevelopmentConfig generates random values for SECRET_KEY, ADMIN_KEY, and ENCRYPTION_KEY at startup if they are not set in the environment — so you can run the dev server without a .env file, but you will get a new secret every restart (sessions will be invalidated). Always set these explicitly even in development for persistent behavior.
Danger zone — the three secrets¶
Change all three before any public deployment
These three variables are the cryptographic foundation of the platform. The defaults will cause a RuntimeError if APP_ENV=production and BOOTSTRAP_ADMIN_IN_PRODUCTION is not set, because validate_production_runtime explicitly rejects them.
| Variable | Default | Consequence of not changing |
|---|---|---|
SECRET_KEY |
change-me-in-production |
Flask sessions are forgeable; any attacker can sign arbitrary session cookies |
ADMIN_KEY |
change-me-admin-key |
Changing this invalidates every derived flag in the database and breaks the HMAC audit chain. Set once, never rotate mid-event. |
ENCRYPTION_KEY |
change-me-encryption-key |
Encrypted DB columns (team secrets) become decryptable by anyone who knows the default |
Generate secure values:
Run that three times — once per secret.
ADMIN_KEY is not rotatable mid-event
ADMIN_KEY is used as the HMAC key for flag derivation (HMAC-SHA3-256(ADMIN_KEY, team_secret || challenge_id)) and for the audit chain (sig_n = HMAC(ADMIN_KEY, sig_{n-1} || event || ts || payload)). Changing it after challenges are imported or after any flags have been submitted will invalidate all pre-computed flags and make the existing audit chain unverifiable. Set it once at provisioning time and keep it constant for the lifetime of the event. See Flag derivation and Audit chain.
Core / identity¶
| Variable | Default | Purpose |
|---|---|---|
APP_ENV |
development |
Selects config class: development, production, or testing |
SECRET_KEY |
change-me-in-production |
Flask session signing key |
ADMIN_KEY |
change-me-admin-key |
HMAC key for flag derivation and audit chain |
ENCRYPTION_KEY |
change-me-encryption-key |
Key for encrypted database columns (team secrets) |
CTF_NAME |
GIDK CTF |
Display name shown in the UI and page titles |
CTF_DESCRIPTION |
Capture the Flag competition hosted by GrizzHacks8 |
Short description shown on the landing page |
CTF_LOGO |
img/logo.png |
Path to the logo image (relative to static directory) |
CTF_FAVICON |
favicon.svg |
Path to the favicon |
DEFAULT_FLAG_PREFIX |
GRIZZ |
Prefix used in flag strings, e.g. GRIZZ{abc123...} |
DEFAULT_THEME |
minimal |
Active theme name seeded to the DB on first run |
DEFAULT_BG_ANIMATION |
matrix |
Background animation seeded to the DB on first run |
PREFERRED_URL_SCHEME |
http |
http or https; set to https in production |
SITE_URL |
http://localhost:5000 |
Canonical URL used in emails and absolute links |
BASE_DOMAIN |
`` (empty) | Apex domain for multi-tenant subdomain routing; set by CTFHive provisioner |
Admin bootstrap¶
| Variable | Default | Purpose |
|---|---|---|
ADMIN_USERNAME |
admin |
Username for the auto-bootstrapped admin account |
ADMIN_PASSWORD |
GrizzAdmin!2025 |
Password for the auto-bootstrapped admin account |
ADMIN_EMAIL |
admin@grizzhacks8ctf.local |
Email for the auto-bootstrapped admin account |
BOOTSTRAP_ADMIN_IN_PRODUCTION |
false |
Set to true to allow auto-bootstrap in production (not recommended; use cli.py admin bootstrap instead) |
Auto-bootstrap behavior
On startup, create_app() checks whether any admin user exists. If none does and APP_ENV != production (or BOOTSTRAP_ADMIN_IN_PRODUCTION=true), it creates one using the values above. In production, skip auto-bootstrap and use the CLI instead:
Database and cache¶
| Variable | Default | Purpose | Production note |
|---|---|---|---|
DATABASE_URL |
sqlite:///ctfapp.db |
SQLAlchemy database URI | Use postgresql://user:pass@host/db |
REDIS_URL |
redis://localhost:6379/0 |
Redis URI for cache and flag storage | Required for rate limiting to work correctly across workers |
RATELIMIT_STORAGE_URI |
memory:// |
Backend for Flask-Limiter counters | Must be redis://... in multi-worker production |
In-memory rate limiting is per-worker
RATELIMIT_STORAGE_URI=memory:// (the default) stores counters inside each gunicorn worker process. With -w 4 workers, clients can send 4× the configured limit before being blocked. Set RATELIMIT_STORAGE_URI=redis://localhost:6379/0 whenever you run more than one worker.
Session and security headers¶
| Variable | Default | Purpose |
|---|---|---|
SESSION_COOKIE_SECURE |
false |
Set to true in production (requires HTTPS) |
SESSION_COOKIE_SAMESITE |
Lax |
SameSite cookie policy |
TRUST_PROXY |
false |
Apply ProxyFix middleware; set true when behind nginx/Caddy |
PROXY_FIX_X_FOR |
1 |
Number of trusted X-Forwarded-For headers |
PROXY_FIX_X_PROTO |
1 |
Number of trusted X-Forwarded-Proto headers |
PROXY_FIX_X_HOST |
1 |
Number of trusted X-Forwarded-Host headers |
PROXY_FIX_X_PREFIX |
1 |
Number of trusted X-Forwarded-Prefix headers |
SECURE_HEADERS_ENABLED |
true |
Emit security headers (CSP, X-Frame-Options, etc.) |
ENABLE_HSTS |
false |
Include Strict-Transport-Security header |
HSTS_SECONDS |
31536000 |
max-age for HSTS (1 year) |
CONTENT_SECURITY_POLICY |
(strict default — see config.py) |
Override the full CSP header string |
MAX_CONTENT_LENGTH |
33554432 (32 MB) |
Maximum request body size in bytes |
Email¶
| Variable | Default | Purpose |
|---|---|---|
EMAIL_VERIFICATION_ENABLED |
false |
Require email verification on registration |
MAILTRAP_API_KEY |
`` (empty) | Mailtrap API key for sending email; required if email verification is enabled |
MAIL_SENDER |
no-reply@grizzhacks8ctf.us |
From address for outbound email |
Email verification
Setting EMAIL_VERIFICATION_ENABLED=true without a valid MAILTRAP_API_KEY will leave new registrations stuck in an unverified state. Ensure email delivery works before enabling in production.
Rate limiting¶
| Variable | Default | Purpose |
|---|---|---|
RATELIMIT_STORAGE_URI |
memory:// |
See Database and cache above |
LOGIN_RATE_LIMIT |
10 per minute |
Max login attempts per IP per minute |
REGISTER_RATE_LIMIT |
5 per minute |
Max registration attempts per IP per minute |
PASSWORD_RESET_RATE_LIMIT |
5 per hour |
Max password reset requests per IP per hour |
TEAM_JOIN_RATE_LIMIT |
10 per minute |
Max team join attempts per IP per minute |
ADMIN_SENSITIVE_RATE_LIMIT |
30 per minute |
Rate limit for sensitive admin actions |
FLAG_WRONG_LIMIT |
3 |
Wrong flag submissions before lockout |
FLAG_LOCKOUT_SECONDS |
30 |
Lockout duration after too many wrong submissions |
Rate limit strings use Flask-Limiter syntax: "N per period" where period is second, minute, hour, or day. You can also write "N/minute".
Event timing¶
| Variable | Default | Purpose |
|---|---|---|
EVENT_STARTS_AT |
2025-03-28T12:00:00+00:00 |
ISO 8601 datetime when the event opens |
EVENT_ENDS_AT |
2025-03-29T12:00:00+00:00 |
ISO 8601 datetime when the event closes |
The app uses these to control challenge visibility and flag submission windows. Set them in UTC ISO 8601 format:
Labs and VPN (dynamic challenges)¶
Lab features are optional
All lab variables are ignored when LAB_ENABLED=false (the default). You do not need Docker or WireGuard for static challenges.
| Variable | Default | Purpose |
|---|---|---|
LAB_ENABLED |
false |
Enable per-team Docker lab spawning |
LAB_DOCKER_HOST |
(host Docker socket) | Docker daemon URL, e.g. unix:///var/run/docker.sock or tcp://... |
LAB_WORKER_ID |
local-worker |
Identifier for this lab worker node |
LAB_WORKER_MAX_CPU_UNITS |
16000 |
CPU capacity units for scheduling (millicores × 1000) |
LAB_WORKER_MAX_MEMORY_MB |
32768 |
Memory capacity for scheduling in MB |
LAB_SUBNET_POOL_CIDR |
10.200.0.0/16 |
CIDR pool for per-team Docker bridge networks |
LAB_SUBNET_PREFIX |
24 |
Prefix length for each per-team subnet |
LAB_APPLY_IPTABLES |
false |
Enforce per-team iptables FORWARD rules for network isolation |
LAB_ISSUE_WG_CONFIG |
false |
Issue WireGuard client configs to players |
LAB_VPN_PEER_POOL_CIDR |
10.50.0.0/24 |
VPN IP pool for WireGuard peers |
LAB_VPN_PLAYERS_PER_TEAM |
4 |
Max WireGuard peers (VPN IPs) allocated per team |
LAB_WG_INTERFACE |
wg0 |
WireGuard interface name on the server |
LAB_WG_ENDPOINT |
vpn.example.com |
Public WireGuard server address given to players |
LAB_WG_LISTEN_PORT |
51820 |
WireGuard UDP listen port |
LAB_WG_SERVER_PUBLIC_KEY |
`` (empty) | Server's WireGuard public key included in player configs |
LAB_REQUIRE_PINNED_IMAGES |
false |
Require image@sha256:... digest pins for all challenge images |
LAB_ALLOW_BASENAME_IMAGE_FALLBACK |
true |
If false, a missing tag will not fall back to a same-named public image (closes a supply-chain hole) |
INSTANCE_TTL_SECONDS |
7200 |
Lab instance lifetime in seconds (2 hours) |
INSTANCE_DNS_DOMAIN |
`` (empty) | Wildcard DNS domain for dynamic instance URLs; empty = show localhost in UI |
ADMIN_DOCKER_TIMEOUT_SECONDS |
3 |
Timeout for Docker API calls from the admin panel |
Dispatch service (lab orchestration)¶
| Variable | Default | Purpose |
|---|---|---|
DISPATCH_INTERNAL_URL |
http://localhost:5001 |
Internal URL of the dispatch (lab orchestration) service |
DISPATCH_ADMIN_TOKEN |
change-me-dispatch-token |
Shared secret for the dispatch service API |
DISPATCH_USE_REMOTE |
true |
When false, the app skips HTTP calls to dispatch and uses local Docker fallback |
DISPATCH_PREFER_LOCAL |
true |
Prefer local Docker spawning over remote dispatch |
Container registry¶
| Variable | Default | Purpose |
|---|---|---|
CTF_DEPLOY_MODE |
local |
local or remote; affects image resolution |
REGISTRY_HOST |
`` (empty) | Registry hostname for pulling challenge images (e.g. registry.example.com) |
REGISTRY_USER |
`` (empty) | Registry authentication username |
REGISTRY_PASS |
`` (empty) | Registry authentication password |
CHALLENGES_ROOT |
<repo>/challenges |
Absolute path to the challenges directory; used by the importer and image builder |
Minimal production .env¶
The smallest .env you need for a real single-server production deployment (static challenges only, no labs):
APP_ENV=production
SECRET_KEY=<generated>
ADMIN_KEY=<generated>
ENCRYPTION_KEY=<generated>
DATABASE_URL=postgresql://ctfuser:strongpassword@localhost/ctfapp
REDIS_URL=redis://localhost:6379/0
RATELIMIT_STORAGE_URI=redis://localhost:6379/0
CTF_NAME=My CTF 2026
DEFAULT_FLAG_PREFIX=FLAG
PREFERRED_URL_SCHEME=https
SITE_URL=https://ctf.example.com
TRUST_PROXY=true
SESSION_COOKIE_SECURE=true
ENABLE_HSTS=true
EVENT_STARTS_AT=2026-09-01T18:00:00+00:00
EVENT_ENDS_AT=2026-09-02T18:00:00+00:00
Related references¶
- Flag derivation — how flags are computed from
ADMIN_KEY - Audit chain — how
ADMIN_KEYsigns the audit log - Environment variables (consolidated) — single-page reference for all variables
- Installation — prerequisites and Makefile targets