Features β’ Quick Start β’ Documentation β’ Screenshots β’ Contributing
Whaley is a production-ready Docker instance manager designed specifically for Capture The Flag (CTF) competitions. Deploy it on a dedicated server to provide each participant with their own isolated challenge environmentβcomplete with automatic port allocation, resource limits, and seamless CTFd integration.
| Problem | Whaley's Solution |
|---|---|
| Shared challenge instances cause interference | π Isolated containers per user/team |
| Manual port management is error-prone | π― Automatic port allocation with collision prevention |
| No visibility into player resource usage | π Real-time monitoring dashboard |
| Difficult to detect flag sharing | π Suspicious submission detection |
| Need packet-level evidence during incidents | π‘ Native packet capture with flow search and raw PCAP download |
| Complex setup for dynamic flags | π Simple CTFd integration with auto flag injection |
| Need external observability | π Prometheus /metrics endpoint protected by a secret |
|
|
- Docker Engine 24.0+ with Docker Compose v2
- 4+ CPU cores, 8GB+ RAM (see capacity planning)
- Linux server (Ubuntu 22.04+ or Debian 12+ recommended)
# Clone the repository
git clone https://github.com/jonscafe/whaley.git
cd whaley
# Configure environment
cp .env.example .env
nano .env # Edit with your settings
# Start Whaley
docker compose up -d| Interface | URL | Description |
|---|---|---|
| User Dashboard | http://your-server:8000/ |
Challenge spawning interface |
| Admin Panel | http://your-server:8000/admin |
Monitoring & management |
| API Docs | http://your-server:8000/docs |
Swagger API documentation |
| Prometheus Metrics | http://your-server:8000/metrics |
Protected metrics export when METRICS_SECRET is set |
# Authentication
AUTH_MODE=ctfd # "ctfd" or "none"
CTFD_URL=https://your-ctfd.com # Your CTFd instance URL
CTFD_API_KEY=ctfd_xxx... # CTFd admin API key for dynamic flags/sync
# Network
PUBLIC_HOST=auto # VPS IP ("auto" for detection)
PORT_RANGE_START=20000
PORT_RANGE_END=50000
# Admin
ADMIN_KEY=your-secure-key # Local admin key when AUTH_MODE=none
METRICS_SECRET=change-me # Enables protected /metrics endpoint
# Dynamic Flags
DYNAMIC_FLAGS_ENABLED=true
FLAG_PREFIX=FLAG # e.g., FLAG{...}
# Packet Capture
PCAP_ENABLED=true
PCAP_MODE=all # "all", "selected", or "none"
PCAP_MAX_SIZE_MB=25
PCAP_RETENTION_HOURS=24
PCAP_SNAP_LEN=1024
PCAP_BPF_FILTER=not (host 127.0.0.11 and port 53)
# Team Mode
TEAM_MODE=auto # "auto", "enabled", or "disabled"# PostgreSQL for high availability
DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/whaley
# Redis for distributed locking (required for multiple workers)
REDIS_URL=redis://redis:6379/0
# Network Isolation (recommended)
NETWORK_ISOLATION_ENABLED=true
NETWORK_ICC_DISABLED=true
NETWORK_SUBNET_BASE=10.240.0.0/16
NETWORK_SUBNET_PREFIX=28
# Host Firewall Rate Limits (recommended for public events)
FIREWALL_RATE_LIMIT_ENABLED=true
FIREWALL_CHAIN=DOCKER-USER
FIREWALL_CONN_LIMIT_PER_IP=60
FIREWALL_RATE_PER_MINUTE=120
FIREWALL_RATE_BURST=240
FIREWALL_REJECT_MODE=reject
TRUSTED_PROXIES=127.0.0.1,::1 # Only these proxies may set client IP headers
# Resource Limits (enforced on all containers)
CONTAINER_MAX_MEMORY=384m # Max memory per container
CONTAINER_MAX_CPU=0.5 # Max CPU cores per container
CONTAINER_PIDS_LIMIT=256 # Max PIDs per container (fork bomb protection)π‘ Tip: Most settings (including Authentication & CTFd integration) can be configured instantly via the Admin Panel β βοΈ Settings tab.
π Admin access: In
AUTH_MODE=ctfd, Whaley validates the submitted CTFd access token with CTFd's/api/v1/users/me, then fetches/api/v1/users/{id}and only enables/adminwhen that detailed user record hastype: "admin". InAUTH_MODE=none, admin APIs use the localADMIN_KEYfallback.
π Subnet pool: Whaley uses
NETWORK_SUBNET_BASE/NETWORK_SUBNET_PREFIXfor both its per-instance isolation network and compose-defined challenge networks. This keeps multi-network challenges from exhausting Docker's default address pools during large events.
π‘οΈ Host rate limits: Enable
FIREWALL_RATE_LIMIT_ENABLED=trueto install per-instanceconnlimit+hashlimitrules on Docker published ports viaDOCKER-USER. If Whaley itself runs inside a container, setFIREWALL_USE_NSENTER=trueor provide equivalent host firewall access.
π Prometheus metrics: Set
METRICS_SECRETto enable/metrics. Scrape with eitherAuthorization: Bearer <secret>orX-Metrics-Secret: <secret>.
π Full configuration guide: See DOCUMENTATION.md
Create challenges in the challenges/ directory:
challenges/
βββ my-web-challenge/
βββ challenge.yaml # Challenge metadata
βββ docker-compose.yaml # Container definition
βββ Dockerfile
βββ flag.txt # Flag file (auto-injected)
βββ src/
βββ app.py
id: my-web-challenge
name: "SQL Injection Lab"
category: web
description: "Can you bypass the login?"
ports:
- 80
timeout: 3600 # 1 hourservices:
web:
build: .
ports:
- "${PORT_80}:80"
environment:
- FLAG=${FLAG}
mem_limit: 256m
cpus: 0.5
β οΈ Resource enforcement: Even if a challenge setsmem_limit: 2g, Whaley will cap it to the globalCONTAINER_MAX_MEMORY(default384m). You can set per-challenge overrides via the admin panel.
π‘οΈ Compose hardening: Whaley prepares every spawn from a per-instance copy, attaches the instance network automatically, and rejects dangerous compose options such as
privileged,network_mode, host/container namespace sharing, added capabilities/devices, unsafe security options, Docker socket mounts, external networks/volumes, unsafe build/env file paths, symlinks, and bind mounts that escape the challenge directory. The hardening-safesecurity_opt: ["no-new-privileges:true"]option is allowed.
π More examples: See DOCUMENTATION.md
Whaley supports CTFd Team Mode where instances and flags are shared per-team:
| Feature | User Mode | Team Mode |
|---|---|---|
| Instance Ownership | Per user | Per team |
| Dynamic Flags | Unique per user | Shared per team |
| Instance Control | Only spawner | Any team member |
| Suspicious Detection | User vs User | Team vs Team |
Enable with TEAM_MODE=auto to automatically detect from CTFd settings.
Hard Cap = Teams Γ MAX_INSTANCES_PER_TEAM (default: 2)
Peak Instances = Hard Cap Γ Concurrency Factor (0.5 β 0.8)
RAM Required = 200 MB (infra) + Peak Instances Γ 264 MB
Storage Required = Docker Images + (PCAP Instances Γ PCAP Rate Γ Hours)
Ports Required = Peak Instances Γ Ports per Challenge
| Component | RAM | CPU | Disk/hr | Notes |
|---|---|---|---|---|
| Challenge containers (avg) | 256 MB | 0.5 cores | β | Capped by CONTAINER_MAX_MEMORY |
| tcpdump sidecar | ~5 MB | 0.02 cores | 5β25 MB | When PCAP_ENABLED=true |
| Isolated network | ~1 MB | negligible | β | Per-instance bridge + iptables |
| Forensics log (on terminate) | β | β | ~30 KB | Compressed gzip |
| Total per instance | ~264 MB | ~0.52 cores | 5β25 MB | Sidecar included |
| Event Size | CPU | RAM | Storage | Example |
|---|---|---|---|---|
| Small (β€50 teams) | 4 cores | 16 GB | 60 GB SSD | Local CTFs |
| Medium (50-150 teams) | 8 cores | 32 GB | 150 GB NVMe | University CTFs |
| Large (150-300 teams) | 16 cores | 64 GB | 300 GB NVMe | National CTFs |
Scenario A β University CTF: 100 teams, 8 challenges, 10-hour event
- Hard cap: 100 Γ 2 = 200 instances max
- Peak instances: 200 Γ 0.7 = 140 instances
- RAM: 200 MB + 140 Γ 264 MB = ~37 GB
- PCAP storage: 140 Γ 10 MB/hr Γ 10 hr = ~14 GB
- Ports: 140 Γ 1.5 avg = 210 ports
Scenario B β National CTF: 200 teams, 10 challenges, 12-hour event
- Hard cap: 200 Γ 2 = 400 instances max
- Peak instances: 400 Γ 0.7 = 280 instances
- RAM: 200 MB + 280 Γ 264 MB = ~74 GB
- PCAP storage: 280 Γ 10 MB/hr Γ 12 hr = ~34 GB
- Ports: 280 Γ 1.5 avg = 420 ports
Whaley includes a reusable stress harness at scripts/stress_test.py. It discovers active challenges from /challenges, spawns synthetic team-owned instances through the admin API, generates mixed HTTP/TCP traffic, samples admin and PCAP status, and can optionally clean up the instances it created.
Quick smoke test:
pip install -r requirements.txt
WHALEY_BASE_URL=http://your-server:8000 \
WHALEY_ADMIN_KEY=your-admin-key \
python3 scripts/stress_test.py \
--team-count 10 \
--instances-per-team 2 \
--traffic-seconds 120 \
--traffic-workers 16 \
--team-prefix smoke \
--cleanupLarger rehearsal:
WHALEY_BASE_URL=http://your-server:8000 \
WHALEY_ADMIN_KEY=your-admin-key \
python3 scripts/stress_test.py \
--team-count 160 \
--instances-per-team 2 \
--traffic-seconds 900 \
--traffic-workers 64 \
--spawn-concurrency 8 \
--admin-qps 2.0 \
--team-prefix fullrun \
--state-file /tmp/whaley-stress.jsonCleanup later from saved state:
WHALEY_BASE_URL=http://your-server:8000 \
WHALEY_ADMIN_KEY=your-admin-key \
python3 scripts/stress_test.py \
--cleanup-from-state /tmp/whaley-stress.jsonThe full runbook, caveats for AUTH_MODE=none, and tuning guidance live in DOCUMENTATION.md.
User Dashboard |
Admin Dashboard & Controls |
Event Logs & Audit Trail |
Challenge Manager |
Dynamic Flags & Suspicious Submissions |
CTFd Sync Wizard |
All key Whaley settings can be changed at runtime via the Admin Panel β βοΈ Settings tab:
| Category | Settings |
|---|---|
| Instance | Timeout, max instances per user/team |
| Resources | Container max memory, CPU cores, PID limit |
| Network | Port range, isolation, subnet pool, public host |
| Features | Dynamic flags, flag prefix |
| Authentication | Auth mode (CTFd/None), CTFd URL, CTFd API key, local Admin Key fallback, metrics secret |
| Forensics | Auto capture, retention period |
| Packet Capture | Mode (all/selected/none), selected challenges, max file size, retention, snap length, BPF filter |
Changes persist to the database and survive container restartsβno need to edit docker-compose.yaml or .env files.
Control which challenges are visible and spawnable from the Challenge Manager tab:
- π’ Active β Visible on the user dashboard, can be spawned
- π΄ Inactive β Hidden from users, spawn requests rejected (HTTP 403)
Use this during competitions to stage challenges for later rounds, or to quickly disable a broken challenge without deleting it.
Challenge uploads reject path traversal, absolute paths, and symlinks. Runtime challenge trees are also rejected if they contain symlinks. The browser editor only writes text files up to 2 MB, and Whaley blocks deleting a challenge while active instances are still using it.
The admin dashboard can manually spawn instances for a chosen user/team owner, force-destroy any active instance, inspect live per-instance Docker logs, and sample detailed resource metrics on demand. The Monitoring tab now uses a cheap host-level snapshot by default, so it stays usable even when hundreds of instances are alive, and it also shows firewall/rate-limit status plus per-instance rule details. The Packet Capture tab adds per-instance flow summaries, payload search, raw PCAP downloads, and retention cleanup controls. Failed spawn/stop actions return the backend error message directly in the UI, which makes broken compose files, resource exhaustion, firewall misconfiguration, and Docker cleanup issues easier to diagnose during an event.
Whaley enforces maximum resource limits on every container, regardless of what the challenge's docker-compose.yaml specifies:
CONTAINER_MAX_MEMORY=384m # Caps mem_limit in compose files
CONTAINER_MAX_CPU=0.5 # Caps cpus in compose files
CONTAINER_PIDS_LIMIT=256 # Injects pids_limit (fork bomb protection)
Per-challenge overrides can be set via the admin API if certain challenges need more resources.
For comprehensive documentation, see DOCUMENTATION.md:
- π§ Installation & Configuration
- π¦ Challenge Structure & Examples
- π API Reference
- π‘οΈ Security Considerations
- π Instance Forensics
- π‘ Native Packet Capture
- π Resource Monitoring
- π‘ Prometheus Metrics
- π Capacity Planning
- π§ͺ Stress Testing
- βοΈ Admin Settings & Challenge Management
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- π Bug fixes and improvements
- π Documentation enhancements
- π¨ UI/UX improvements
- π§ͺ Test coverage
- π Internationalization
This project is licensed under the MIT License β see the LICENSE file for details.
Some infrastructure ideas and hardening lessons for Whaley were inspired by the MCTF 5.0 post-mortem and follow-up work from MCTF Behind the Scenes: The Infra We Built and the Chaos We Caused, by Younes Ferradji (Ynxfdj) and Abderrahmane Yahiaoui (COn4n).
The packet-capture workflow and PCAP sidecar direction were also inspired by Tulip.
keii Creator & Maintainer |
Built with β€οΈ for the CTF community
If you find Whaley useful, please consider giving it a β





