Data from the qualifiers including database, API snapshot, replays, bots and handouts can be found in this GCS bucket: https://storage.googleapis.com/livectf26-archive
| Crate | Description |
|---|---|
emulator |
Emulator for the custom architecture |
driver |
Runs bots and the game engine |
coordinator |
Retrieves game data and bots, runs driver, reports results |
api |
Web API for teams and admins |
game-engine |
Game logic and simulation |
game |
Game visualizer |
sudo apt-get update && sudo apt-get install -y \
libwayland-dev libasound2-dev libudev-dev pkg-config \
libx11-dev libxkbcommon-x11-0 libssl-dev
cargo install 'sea-orm-cli@^2.0.0-rc'
sudo -i -u postgres psql <<'SQL'
CREATE DATABASE livectf;
CREATE USER livectf WITH ENCRYPTED PASSWORD 'livectf';
GRANT ALL PRIVILEGES ON DATABASE livectf TO livectf;
\c livectf
GRANT ALL ON SCHEMA public TO livectf;
SQL
export DATABASE_URL=postgres://livectf:livectf@localhost/livectf
sea-orm-cli migrate
sea-orm-cli generate entity \
--output-dir entities/src/entities \
--database-url "$DATABASE_URL"Operational guide for setting up and running the bot-game infrastructure. For the architectural rationale and the GCP alternative design see docs/cicd.md.
| Tool | Version | Install |
|---|---|---|
| Terraform | ≥ 1.6 | https://developer.hashicorp.com/terraform/install |
| Ansible | ≥ 2.15 | pip install ansible |
| make | any | system package |
Docker is only needed locally if you want to run cargo make docker-build-* inside bot-game/ to test image builds. CI handles all production builds. The Hetzner CLI is not required — Terraform manages all resources.
All make commands below run from the repo root.
- Log in to console.hetzner.cloud and create a new project
- Generate an API token (Read & Write) — Project → Security → API tokens
- Create an Object Storage access key — Project → Security → S3 credentials — note the access key and secret key
- Generate an SSH key pair if you don't have one:
ssh-keygen -t ed25519 -C "bot-game-organizer"
This creates the Hetzner Object Storage bucket that holds all future Terraform state. Run once.
cp infrastructure-bootstrap/terraform.tfvars.example infrastructure-bootstrap/terraform.tfvars
# fill in storage_access_key and storage_secret_key
make infra-bootstrapNote the outputs:
state_bucket_name = "bot-game-tfstate"
s3_endpoint = "https://nbg1.your-objectstorage.com"
cp infrastructure/terraform.tfvars.example infrastructure/terraform.tfvars
# fill in hcloud_token and ssh_public_keysServer types and other per-environment settings are hardcoded in infrastructure/main.tf.
cp infrastructure/backend.hcl.example infrastructure/backend.hcl
# fill in bucket, endpoint, access_key, secret_key
make infra-init
make infra-apply # both environments at once
# or scope to one:
make infra-apply-staging
make infra-apply-prodNote the IP addresses from the Terraform output:
staging_api_ip = "1.2.3.4"
staging_runner_ip = "5.6.7.8"
production_api_ip = "9.10.11.12"
production_runner_ip = "13.14.15.16"
Edit ansible/inventory.yml and fill in the IPs from step 4:
api-staging:
ansible_host: 1.2.3.4
runner-staging:
ansible_host: 5.6.7.8
api-prod:
ansible_host: 9.10.11.12
runner-prod:
ansible_host: 13.14.15.16Alternatively, regenerate it from the Terraform template:
terraform -chdir=infrastructure output -raw ansible_inventory > ansible/inventory.ymlServers pull images from ghcr.io at deploy time. Create a PAT with read:packages scope:
GitHub → Settings → Developer settings → Personal access tokens
The secrets files are gitignored. Fill them in for each environment:
ansible/group_vars/all/secrets.yml (shared across all hosts):
ghcr_token: "ghp_YOUR_PAT"ansible/group_vars/staging/secrets.yml:
db_password: "CHANGEME"
paseto_secret: "000...000" # 32-byte hex string
admin_password: "CHANGEME"
storage_access_key_id: "YOUR_KEY"
storage_secret_access_key: "YOUR_SECRET"
coordinator_token: "CHANGEME"Do the same for ansible/group_vars/production/secrets.yml.
Non-sensitive settings (bucket names, endpoints, registry path) are in ansible/group_vars/staging/vars.yml and ansible/group_vars/production/vars.yml — update those if your values differ.
Then run:
make ansible-setup-staging
make ansible-setup-prodGo to GitHub → Repository → Settings → Secrets and variables → Actions.
Secrets (repository-level):
| Secret name | Value |
|---|---|
HETZNER_SSH_KEY |
Contents of your private SSH key (~/.ssh/id_ed25519) |
Go to GitHub → Repository → Settings → Environments and create two environments:
staging — no protection rules needed.
| Variable | Value |
|---|---|
API_HOST |
Public IP of staging API server |
RUNNER_HOST |
Public IP of staging runner server |
production — add at least one required reviewer to gate the prod deploy in release.yml.
| Variable | Value |
|---|---|
API_HOST |
Public IP of production API server |
RUNNER_HOST |
Public IP of production runner server |
The world.bin file is not generated by CI. Copy it to both runner servers:
scp /path/to/world.bin root@STAGING_RUNNER_HOST:/opt/bot-game/runner/world.bin
scp /path/to/world.bin root@PROD_RUNNER_HOST:/opt/bot-game/runner/world.binAll ports except SSH are firewalled. Use SSH port forwarding to reach the API.
cd bot-game/cli
cargo build --release
# binary: bot-game/cli/target/release/livectfAdd it to your PATH or refer to it by path in the commands below.
Copy the template and point it at the forwarded port:
cp bot-game/cli/cli/Config.toml.tpl bot-game/cli/Config.toml
# api_url = "http://localhost:3000"
# token = "" ← fill in after loginThe CLI looks for Config.toml in the current directory. Run all CLI commands from bot-game/cli/ or pass --config /path/to/Config.toml.
In a dedicated terminal (keep it open):
ssh -L 3000:localhost:3000 root@API_HOSTlivectf auth login --username admin --password <admin_password>
# prints: {"token":"v4.local..."}Copy the token value into Config.toml:
token = "v4.local..."Verify it works:
livectf auth me
# {"Admin": null} or {"GameManager": null}Teams:
livectf teams list
livectf teams create --name "Team Rocket"
livectf teams get <team-id>Bots:
livectf teams upload-bot <team-id> path/to/bot.bin
livectf teams bots <team-id>Games:
# Create a game for a specific phase with a random seed
livectf games create --seed $(uuidgen) --phase phase1
# Add a bot as a participant
livectf games add-participant <game-id> --bot-id <bot-id>
# Start the game (coordinator picks it up)
livectf games start <game-id>
# Check status
livectf games get <game-id>
# Record the result (one --score per participant)
livectf games finish <game-id> --score <bot-id>:<points> --score <bot-id>:<points>Leaderboard:
livectf leaderboardImpersonate a team (admin support flow):
When an organizer needs to see the player UI exactly as a particular team sees it — to debug a bot listing, check a download link, reproduce a player report — mint a short-lived session token for that team and use it in the frontend.
# Mint a 24-hour session token for the team. Requires admin credentials in
# Config.toml (see step 4 above).
livectf auth impersonate <team-id>
# prints: {"status":"ok","token":"v4.local..."}Then paste the v4.local.… value into the frontend's Login screen
(textarea). The frontend recognises the paseto prefix and validates the
token directly via /api/auth/me — no further exchange — so the session is
indistinguishable from a normal team login. livectf auth me will report
{"Team":{"team_id":"…"}} when run with that token.
Automatic — push to main. build.yaml runs first; if it passes, staging.yml builds images and deploys.
To check deployment status:
ssh root@STAGING_API_HOST systemctl status bot-game-api
ssh root@STAGING_RUNNER_HOST systemctl status 'bot-game-runner@*'- On GitHub, create a Release targeting the commit you want to ship
release.ymlre-tags the images built for that commit and deploys to production
When the competition goes live:
make infra-open-firewallTo close it again after the event:
make infra-close-firewallNo server restart needed — Hetzner applies firewall changes immediately.
Re-run the relevant Ansible playbook. It is idempotent.
make ansible-setup-staging # api + runner
make ansible-setup-prodscp /path/to/new-world.bin root@PROD_RUNNER_HOST:/opt/bot-game/runner/world.bin
ssh root@PROD_RUNNER_HOST systemctl restart 'bot-game-runner@*'ssh root@STAGING_API_HOST journalctl -u bot-game-api -f
ssh root@STAGING_RUNNER_HOST journalctl -u 'bot-game-runner@*' -fContainers not starting after deploy:
ssh root@HOST systemctl status bot-game-api
ssh root@HOST journalctl -u bot-game-api -n 50Image pull fails on server:
Check the ghcr.io token is valid and has read:packages scope:
ssh root@HOST docker pull ghcr.io/OWNER/REPO/api:stagingIf it fails, update ansible/group_vars/all/secrets.yml with a fresh token and re-run make ansible-setup-*.
Terraform state locked:
terraform -chdir=infrastructure force-unlock LOCK_IDRunner nsjail errors:
ssh root@RUNNER_HOST docker run --cap-add SYS_ADMIN --cap-add SYS_PTRACE debian:bookworm-slim echo okIf this fails the host kernel may not support user namespaces — check /proc/sys/kernel/unprivileged_userns_clone.