If you already have CTFd running with challenges, this is all you need:
git clone https://github.com/codewithdaniel1/IsolateX
cd IsolateX
./setup.sh --external-ctfdsetup.sh --external-ctfd starts only the IsolateX core services and then attempts to auto-install/configure the plugin in your existing CTFd.
Optional explicit targeting (recommended if multiple CTFd instances are running):
./setup.sh --external-ctfd --external-ctfd-container <ctfd-container-name>
# or
./setup.sh --external-ctfd --external-ctfd-path <path-to-CTFd>Path expectations:
- Bundled mode (
./setup.shwith no external flags): CTFd comes from this repo at./ctfd. - External filesystem mode (
--external-ctfd-path): pass the CTFd repo root path (the directory that containsCTFd/), and IsolateX installs the plugin toCTFd/plugins/isolatex. - External container mode (
--external-ctfd-container): IsolateX installs the plugin inside the container at/opt/CTFd/CTFd/plugins/isolatex.
Bundled mode details:
- The CTFd image built from
ctfd/Dockerfileincludes the IsolateX plugin by default. - In local development,
docker-compose.ymlalso bind-mounts./ctfd-plugininto CTFd so plugin edits apply immediately. - The bundled CTFd build tries
ctfd/ctfd:latestfirst and automatically falls back to older tags if latest fails to build. - CI checks this fallback path in
.github/workflows/ctfd-build-fallback.yml. - Optional overrides:
CTFD_BASE_IMAGE=ctfd/ctfd:3.8.2 ./setup.shCTFD_FALLBACK_IMAGES=ctfd/ctfd:3.8.2,ctfd/ctfd:3.8.1 ./setup.sh
After setup, restart CTFd only if your environment requires it. You should see IsolateX in the admin navbar under Plugins.
Enabling instancing on your challenges:
- Run
scripts/import-challenges.sh [path-to-challenge-root]to import challenges and upload any downloadable files listed inchallenge.json(existing CTFd challenge names are skipped and not overwritten). If no path is passed, it defaults to./challenges. Challenges are registered with IsolateX only whenchallenge.jsonincludesisolatexmetadata (for exampleisolatex: { "image": "myctf/web1:latest", "port": 80 }). - Go to Admin → Plugins → IsolateX — only challenges registered with the orchestrator appear here
- Adjust the runtime tier per challenge if needed and click Save
- Players immediately see the Launch/Stop/Renew panel on registered challenges; all other challenges are completely unaffected
If your CTFd admin login is not admin / admin, export CTFD_USER and CTFD_PASS first so the file-upload step can log in to CTFd. When that is unavailable, the script falls back to syncing files directly into the local Docker Compose CTFd instance.
After setup/import, run a live security smoke test:
./scripts/security-smoke.shThe setup.sh script installs and configures everything. Safe to re-run — existing tools are updated, not reinstalled.
git clone https://github.com/codewithdaniel1/IsolateX
cd IsolateX
./setup.sh./setup.sh auto-detects host capabilities and installs all supported layers:
| Host capability | Tools installed | Runtimes unlocked |
|---|---|---|
| Any host | Docker, Docker Compose | docker |
| Linux host | + kubectl, k3s, kCTF namespace + NetworkPolicy | docker, kctf |
Linux + KVM (/dev/kvm) |
+ Kata Containers, Firecracker, kata-firecracker RuntimeClass |
+ kata-firecracker |
macOS / Windows: Only
dockerruntime works locally.kctfrequires a Linux host.kata-firecrackerrequires Linux + KVM hardware virtualization (VT-x for Intel, AMD-V for AMD — enable in BIOS). For production, use a Linux server or cloud VM (AWS, GCP, DigitalOcean, Hetzner). If a runtime is disabled in the IsolateX admin page, that toggle cannot be enabled from the page itself. Fix host prerequisites and rerun./setup.sh.
After the script finishes:
- Go to http://localhost:8000 and complete the CTFd setup wizard
- Go to Admin → Plugins → IsolateX to enable instancing per challenge
On first run, setup.sh generates a .env file with random secrets. Keep this file — it contains your API_KEY and FLAG_HMAC_SECRET.
Everything runs in Docker Compose: orchestrator, a Docker worker, CTFd, Postgres, Redis, and Traefik.
git clone https://github.com/codewithdaniel1/IsolateX
cd IsolateX
# Create secrets for compose (or run ./setup.sh once to auto-generate .env)
cat > .env <<EOF
API_KEY=$(openssl rand -hex 32)
FLAG_HMAC_SECRET=$(openssl rand -hex 32)
SECRET_KEY=$(openssl rand -hex 32)
CTFD_SECRET_KEY=$(openssl rand -hex 32)
# Keep CTFd sessions for ~30 days so users do not re-login often.
# Default CTFd value is 604800 (7 days).
CTFD_PERMANENT_SESSION_LIFETIME=2592000
EOF
docker compose up -dServices after startup:
- CTFd: http://localhost:8000
- Orchestrator API: http://localhost:8080/docs
- Go to http://localhost:8000 and complete the CTFd setup wizard (name, admin account).
- The IsolateX plugin is already installed — you will see IsolateX in the admin navbar under Plugins.
In CTFd (Admin → Challenges → New Challenge), create a challenge as usual. The description can be anything — IsolateX automatically injects the instance panel; no special markup needed.
Register challenges with the orchestrator (the import script handles this automatically):
./scripts/import-challenges.shThe same script also attaches downloadable files for any challenge whose challenge.json includes a files array. Use files: [] for challenges that should not expose downloads. If you ever need to re-sync attachments later, run python3 scripts/upload-challenge-files.py.
For IsolateX registration, add isolatex metadata to each instanced challenge's challenge.json (for example isolatex: { "image": "myctf/web1:latest", "port": 80 }).
Then go to CTFd Admin → Plugins → IsolateX:
- Only challenges registered with the orchestrator appear — no toggling needed
- Adjust the runtime or resource tier per challenge if needed and click Save
- Players immediately see the Launch/Stop/Renew panel on registered challenges
Challenges not registered with the orchestrator are unaffected — no panel is shown to players.
Your challenge Docker image must be accessible to the worker. For local dev:
# Build your challenge image
docker build -t my-challenge:latest ./challenges/my-challenge/
# The worker container shares the host Docker socket, so the image is immediately availableChanges to TTL and resources take effect on the next launched instance. Running instances are not affected.
- Log in as a player (or in a private/incognito window).
- Open an instancing-enabled challenge.
- The Live Instance panel appears automatically.
- Click Launch — the instance starts, you get an endpoint URL and countdown timer.
- Click the link — it opens your challenge in a new tab.
- Test Restart, Renew, and Stop.
- Open a challenge that does not have instancing enabled — confirm no panel is shown.
Your challenge Docker image must:
- Run as a non-root user where possible
- Listen on a single port (the
portfield in the orchestrator registration) - Accept the flag via the
ISOLATEX_FLAGenvironment variable (IsolateX injects this automatically)
Example minimal Dockerfile for a Flask web challenge:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN useradd -m ctf && chown -R ctf:ctf /app
USER ctf
ENV PORT=8080
EXPOSE 8080
CMD ["python", "app.py"]In your app, read the flag from the environment:
import os
FLAG = os.environ.get("ISOLATEX_FLAG", "flag{placeholder}")| Challenge type | Recommended runtime |
|---|---|
| Static web (no shell) | docker |
| Web with server-side code | docker or kctf |
| Reversing, crypto | docker or kctf |
| Binary exploitation (pwn) | kata-firecracker |
| RCE / arbitrary code execution | kata-firecracker |
| Kernel challenges | kata-firecracker |
| Tier | CPU | Memory | Use case |
|---|---|---|---|
| 1 | 1 core | 512 MB | Static sites, typical web / reversing |
| 2 | 2 cores | 1 GB | Pwn, heavier services |
| 3 | 4 cores | 2 GB | AI, compilation, heavy compute |
You can set these per-challenge in Admin → Plugins → IsolateX.
The admin UI is the easiest way to enable challenges. For scripted or CI/CD workflows you can also call the orchestrator API directly:
#!/bin/bash
API_KEY="$(grep '^API_KEY=' .env | cut -d= -f2-)"
ORCH="http://localhost:8080"
register() {
local id=$1 name=$2 image=$3 port=$4 runtime=${5:-docker}
curl -s -X POST "$ORCH/challenges" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d "{\"id\":\"$id\",\"name\":\"$name\",\"runtime\":\"$runtime\",\"image\":\"$image\",\"port\":$port}"
}
register "cmdinj" "Command Injection" "myctf-cmdinj:latest" 80
register "sqlinj" "SQL Injection" "myctf-sqlinj:latest" 80
register "bof" "Buffer Overflow" "myctf-bof:latest" 8888 kata-firecrackerAPI_KEY=$(openssl rand -hex 32)
FLAG_HMAC_SECRET=$(openssl rand -hex 32)
echo "API_KEY=$API_KEY"
echo "FLAG_HMAC_SECRET=$FLAG_HMAC_SECRET"Never use the default dev keys in production.
In docker-compose.yml or your deployment config, set:
# Orchestrator
API_KEY=<generated above>
FLAG_HMAC_SECRET=<generated above>
DATABASE_URL=postgresql+asyncpg://isolatex:<password>@postgres:5432/isolatex
REDIS_URL=redis://redis:6379/0
BASE_DOMAIN=ctf.yourdomain.com
TLS_ENABLED=true
DEFAULT_TTL_SECONDS=1800 # 30 min default, override per-challenge in admin UI
# CTFd plugin
ISOLATEX_URL=http://orchestrator:8080
ISOLATEX_API_KEY=<same API_KEY>kubectl create namespace isolatex
kubectl create secret generic orchestrator-secrets \
--namespace isolatex \
--from-literal=api-key="$API_KEY" \
--from-literal=flag-secret="$FLAG_HMAC_SECRET" \
--from-literal=database-url="$DATABASE_URL" \
--from-literal=redis-url="$REDIS_URL"
kubectl apply -f infra/kctf/manifests/For Kubernetes-based runtimes, workers run as pods on cluster nodes. See kctf-setup.md for cluster setup and kata-setup.md for Kata + Firecracker setup.
For Docker runtime (no Kubernetes needed):
RUNTIME=docker \
ORCHESTRATOR_URL=http://orchestrator:8080 \
ORCHESTRATOR_API_KEY=$API_KEY \
ADVERTISE_ADDRESS=<worker-host-ip> \
DOCKER_GATEWAY_CONTAINER=<traefik-container-name> \
uvicorn main:app --host 0.0.0.0 --port 9090DOCKER_GATEWAY_CONTAINER should point to the reverse-proxy container that is allowed to reach challenge backends.
cp -r ctfd-plugin/ <CTFd>/CTFd/plugins/isolatex/
pip install httpx
cat > <CTFd>/CTFd/plugins/isolatex/.isolatex.env <<EOF
ISOLATEX_URL=http://orchestrator:8080
ISOLATEX_API_KEY=<API_KEY>
EOFRestart CTFd. You will see [IsolateX] plugin loaded in the CTFd logs.
Traefik is bundled in docker-compose.yml and starts automatically. For production, edit gateway/traefik/traefik.yml with your domain and TLS email, then:
kubectl apply -f gateway/traefik/Each instance gets a subdomain: <instance-prefix>.<challenge-id>.<base-domain>
e.g. ab12cd34.web100.ctf.yourdomain.com
For local dev, the endpoint is routed through Traefik:
http://<instance-prefix>.<challenge-id>.localhost
Traefik enforces team ownership via forward-auth to CTFd (/isolatex/authz) before proxying traffic.
Instance stuck in "pending"
- Check worker logs — the image may not be reachable from the worker
- Check orchestrator logs for launch errors
- Make sure the worker is registered:
GET /workers
"No available worker for this runtime"
- The worker for that runtime is not registered or is unhealthy
- Check:
curl http://localhost:8080/workers -H "x-api-key: $API_KEY"
Link does not work (connection refused)
- Confirm Traefik is healthy and can fetch dynamic config from
/traefik/config - For Docker runtime: check that each instance network exists and Traefik is attached to it
- Check that the challenge container/pod is actually running and listening on the configured internal port
- If the URL opens but denies access, verify you are logged into the correct CTFd team (forward-auth enforces team ownership)
Live Instance panel not showing
- The IsolateX plugin is mounted and CTFd restarted? Check CTFd logs for
[IsolateX] plugin loaded - The challenge ID must be registered in the orchestrator — IDs are case-sensitive
- Open browser devtools → Console for JS errors
Buttons (Restart/Renew/Stop) not working
- Open browser devtools → Network tab → click the button → check the response
- Make sure you are logged in to CTFd (the plugin uses your session)
- If players are getting logged out too often, increase
CTFD_PERMANENT_SESSION_LIFETIME(seconds) in.envand restart CTFd
Renew extends longer than expected
- Renew resets
expires_attonow + ttl_secondsfor the challenge. If players keep renewing, the instance lifetime can keep extending.
CTFd plugin not loading
- Make sure
httpxis installed in the CTFd container:pip install httpx - Check that
ISOLATEX_URLandISOLATEX_API_KEYenvironment variables are set