Quick Start

This guide takes you from a clean Docker host to a running platform with your first Attack & Defense (A&D) or King of the Hill (KotH) game. You stand the platform up from the TCP1P/gzctf-platform-template repo: it ships the published image, Traefik + Let's Encrypt TLS, and a one-shot wizard. Three make commands and you have a live, HTTPS platform.

The short version:

git clone https://github.com/TCP1P/gzctf-platform-template
cd gzctf-platform-template
make wizard       # interactive — prompts for your hostname, prints the admin password ONCE
make setup        # create the external docker networks
make platform-up  # start gzctf + db + cache + traefik (+ wireguard + ssh-jump for A&D)

Then log in at https://PUBLIC_ENTRY as user Admin with the password the wizard printed.

For exhaustive deployment options see /guide/deployment/docker; for every configuration key see /config/appsettings.

Info

The template deploys the published image dimasmaualana/gzctf:develop — it does not build GZCTF from source. All per-deployment configuration lives in compose/appsettings.json (rendered from compose/appsettings.example.json), and secrets live in compose/.env (both auto-generated by the wizard, both chmod 600).

Prerequisites

  • Docker Engine with the Compose v2 plugin (docker compose ..., not the legacy docker-compose binary).
  • The Docker daemon socket at /var/run/docker.sock — the gzctf container bind-mounts it so it can spawn and manage challenge containers directly on the host.
  • A DNS name pointing at the host: set PUBLIC_ENTRY to a hostname (e.g. ctf.example.com) whose A record resolves to this machine. Let's Encrypt issues the TLS cert over an HTTP-01 challenge, so the name must be publicly reachable on port 80 at boot.
  • Inbound ports: 80/tcp + 443/tcp (Traefik / ACME / web UI). For A&D you also need 51820/udp (WireGuard) and 22022/tcp (SSH jump, override with AD_SSH_PUBLIC_PORT).
  • A host kernel with the tun module available (/dev/net/tun) if you run the WireGuard sidecar for A&D VPN access.
Docker socket trust

gzctf mounts /var/run/docker.sock, which is effectively root on the host — that is how it launches and tears down challenge containers. Run the platform only on a host you control, and never expose the socket to anything other than the gzctf service.

Jeopardy-only?

If you only want a vanilla jeopardy CTF you can delete the wireguard and ssh-jump services from compose/compose.yml (and the wg-config volume + wg-bootstrap network) — they exist solely to give A&D/KotH teams VPN and SSH access to their containers. Everything else (gzctf, db, cache, traefik) is the standard deployment.

1. Clone + run the wizard

git clone https://github.com/TCP1P/gzctf-platform-template
cd gzctf-platform-template
make wizard

make wizard (scripts/wizard.sh) is an interactive first-time setup. It prompts for:

PromptWhat it is
PUBLIC_ENTRYThe public hostname participants type into their browser — no scheme, no path (e.g. ctf.example.com). Must contain a dot.
ACME_EMAILWhere Let's Encrypt sends cert-expiry warnings. Defaults to admin@<PUBLIC_ENTRY>.
SMTP relay (optional)Enables email verification + password reset. Skippable — set it later under /admin/settings → Email.
Cloudflare Turnstile captcha (optional)Slows down account-creation bots on public CTFs. Skippable — set it later under /admin/settings → Captcha.

Everything else is auto-generated so you never hand-pick a secret:

  • WORKSPACE — a gzctf-<8 hex> compose project name + Traefik route suffix.
  • XOR_KEY — openssl rand -hex 32; encrypts registry passwords + repo-binding tokens at rest.
  • ADMIN_PASSWORD — the seed password for the initial Admin user. The Aa1 prefix satisfies ASP.NET Identity's password policy (uppercase + lowercase + digit).
  • POSTGRES_PASSWORD — the Postgres credential, kept in step between the db container and gzctf's connection string.

The wizard writes compose/.env and compose/appsettings.json (both chmod 600, both gitignored) and refuses to overwrite an existing appsettings.json — delete it first if you need to re-run.

Copy the admin password — it is printed ONCE

At the very end the wizard prints the generated admin seed password. It is not stored anywhere you'll look again (it lives only in .env), so copy it now. You log in with it, then change it in the UI.

Never rotate

XOR_KEY or POSTGRES_PASSWORD after first boot XOR_KEY decrypts every registry password and repo-binding token already in the database; rotating it breaks them all. POSTGRES_PASSWORD is consumed by Postgres only on its first init — changing it later requires an ALTER USER inside the running db container. Leave both as the wizard generated them.

2. Create the networks + bring it up

make setup        # creates the external 'traefik' + 'challenges' docker networks (idempotent)
make platform-up  # renders appsettings.json if missing, then 'docker compose up -d'

make platform-up first runs init-config (which renders compose/appsettings.json from appsettings.example.json if it is not already present) and then docker compose -f compose.yml -f compose.traefik.yml up -d. That brings up the full stack:

ServiceImage / buildPurpose
gzctfdimasmaualana/gzctf:developThe platform: web UI, API, scoreboard, and the container orchestrator. Behind Traefik on 443; healthchecks :8080.
dbpostgres:17Primary datastore (users, teams, games, challenges, submissions).
cacheredis:alpineCache + the SignalR backplane for the real-time scoreboard/notifications.
traefiktraefik:latestReverse proxy on 80/443; obtains the Let's Encrypt cert via the ACME HTTP-01 challenge and routes Host(PUBLIC_ENTRY) to gzctf.
wireguardbuilt from ../wireguardA&D only. WireGuard endpoint on 51820/udp so teams reach their containers over a private network.
ssh-jumpbuilt from ../ssh-jumpA&D only. SSH bastion on 22022/tcp; a team can ssh <challenge-id>@PUBLIC_ENTRY -p 22022 straight into its own challenge container.

Watch progress and verify health:

make platform-logs    # tail all services
make gzctf-logs       # tail gzctf only

make help lists every target — including platform-down, platform-restart, flush-cache (redis FLUSHALL, rebuilds the scoreboard cache on next request), and the image-update flows (pull / update, or the targeted pull-gzctf / update-gzctf which only recreates the gzctf container).

Danger

platform-clean drops your data make platform-clean runs docker compose down -v — it deletes the postgres-data, gzctf-files, and wg-config volumes. Use make platform-down to stop the stack while keeping all data.

3. Log in

Open the web UI:

https://PUBLIC_ENTRY

Log in as user Admin with the password the wizard printed in step 1, then change it from the profile menu. From here, head to the admin area to set up your game.

No DNS / no TLS — run it locally on :8080

Skip Traefik and Let's Encrypt with:

make platform-up-no-traefik

This brings up gzctf + db + cache only (via the compose.standalone.yml overlay) and publishes the platform directly on host port 8080. For standalone use, edit compose/appsettings.json and set ContainerProvider.PublicEntry to http://<host>:8080 so emails and scoreboard links resolve to the right place.

Load example challenges

The fastest way to see the A&D/KotH engine end to end is to import the public, ready-to-run event repo TCP1P/TCP1PADTesting. The GZCTF server clones the repo itself, globs its .gzevent manifest, and creates one game with four challenges — no challenge authoring required.

  1. In the admin area open Repo Bindings and click Add:
    • Repo URL: https://github.com/TCP1P/TCP1PADTesting
    • Ref: leave empty (default branch)
    • Interval (seconds): 60
    • Token: none — the repo is public
  2. Click Scan now to import immediately rather than waiting for the poll interval.

The scan creates one game, "TCP1P A&D + KotH Testing", with four challenges under events/tcp1p-testing/:

ChallengeTypeWhat it is
owasp-portalAttackDefenseA notes portal; every OWASP Top 10 (2021) vuln leaks the round flag.
pwn-armoryAttackDefenseA heap item-manager binary (no canary/PIE/RELRO) with ten memory-corruption bug classes.
koth-throneKingOfTheHillAn OWASP web hill — each vuln lets you write your token to /koth/king.
koth-pwnKingOfTheHillA binary hill — flip is_admin or ret2 do_crown() to enthrone your token.

None of the four pins a containerImage or checkerImage, so each ships an auto-built src/ service plus an enochecker3 checker that builds on import. Watch the 8 images (a service + a checker per challenge) come up under admin → Builds.

The game imports hidden. To run it, open admin → game → Info, set the start/end times, and unhide it.

Full repo-binding walkthrough

For the complete binding workflow — poller behaviour, shallow-clone short-circuiting, binding health, and a step-by-step of this exact repo — see /guide/authoring/repo-bindings.

The A&D/KotH challenge types

When you author challenges by hand instead, the challenge editor lets you pick the challenge type. Alongside the classic jeopardy types (Static/Dynamic Attachment, Static/Dynamic Container), this fork adds two A&D-engine types:

TypeWhat it is
AttackDefenseClassic Attack & Defense: every team gets its own instance of the same vulnerable service; they patch their own and exploit others' to steal round flags.
KingOfTheHillA single shared/contested target where teams compete for control each round.

Both A&D-engine types run as managed containers and skip the per-flag configuration jeopardy challenges use — the flags are issued and rotated by the A&D engine each round, not entered by hand. Selecting AttackDefense or KingOfTheHill surfaces the container image and the relevant A&D fields and hides the manual flag inputs.

Tip

For the full schema of an A&D/KotH challenge — image, checker, exposed ports, network mode — see /guide/authoring/challenge-yaml.

A&D round settings live on the game's Info page

The settings that govern the rhythm of an A&D/KotH event are per-game, not per-challenge, and they live in admin → game → Info (the Info tab of the game editor). The key fields and their defaults, straight from the editor:

FieldDefaultRangeMeaning
A&D tick (seconds)6030–600Length of one round/tick. Flags rotate and SLA checks run on this cadence.
Flag lifetime (ticks)51–50How many ticks a planted flag stays valid for submission before it expires.
Reset cooldown (minutes)50–60Minimum time a team must wait between resetting its own A&D instance.
Snapshot retention (days)empty = forever1–3650How long post-game container snapshots are kept; leave blank to keep them indefinitely.

There is also a get-flag window fraction and related fields below these in the same section. Set the tick length and flag lifetime here before the game starts — they define how the whole event is scored.

Tip

Tick cadence and flag lifetime drive how points accrue. To understand how round flags, SLA, and stolen-flag points turn into scoreboard standing, read /guide/features/scoring.

Where to go next