Skip to content

Getting started ​

The quickest way onto a fresh vulnbox is the setup wizard. It installs vulnboxd, gives every service a git repository and a private GitHub repository to push to, and writes the configuration for vulnboxd and for the traffic capture tools.

Prerequisites ​

  • An x86_64 Linux box with systemd (other architectures: build it yourself).
  • Docker with the compose plugin.
  • ssh-keygen, for the per-service deploy keys.
  • Optionally a GitHub token with repo access, if the services should be pushed to GitHub.

Git itself is not required: vulnboxd drives the repositories through libgit2. You will probably want it on the box anyway, for working on services by hand.

Run the wizard ​

Unpack a release, and run the wizard from it. It installs both binaries on the box itself:

sh
curl -fsSL https://github.com/aarondewes/vulnboxd/releases/latest/download/vulnboxd-x86_64-unknown-linux-gnu.tar.gz | tar -xz
export GITHUB_TOKEN=ghp_...     # needs repo access; it is never written to disk
sudo -E ./vulnboxd-x86_64-unknown-linux-gnu/vulnboxctl setup-interactive

The wizard asks for:

questionwhat it is for
services directoryone directory per service, each with a compose file
gamethe game mode: GENERIC, FAUST_2026 or ECSC_2026
team number, gameserveryour team on the gameserver, to read the checker's verdicts; only with a game
vulnbox addressthis box's address, for the capture tools' config; the game's if left empty
tick lengthhow long a round lasts, such as 60s; only without a game, which knows its own
deploy windowthe end of each tick vulnboxd restarts services in, such as 5s
flag formatthe game's flags, which rules write as {flag}; the game's if left empty
alert webhooka Discord webhook for alerts; the setup posts its own progress there too
bypass valuethe header value that lets your team's requests skip every rule; a random one is made up
roll back automaticallywhether vulnboxd undoes a change that breaks a service
health timeout, port check interval, failed checks before an alerthow patient the checks are
GitHub owner, GitHub tokenwhere the repositories go; leave the owner empty to skip GitHub
repository prefixput in front of each service's repository name, service- by default
ssh directorywhere the per-service deploy keys are kept
capture config directorywhere tulip.json and shovel.yaml are written
services fileoptional: a plain list of every service listener — name, address, port — for tools that want to be told where the services are
capture trafficrun the PCAP-over-IP broker as its own service
capture served on, capture ports, capture filterwhere readers connect, and what is captured; only with capture on
status worldrun the Minecraft status world as its own service; a toy, off by default
status world address, status world passwordwhere it listens and the word that lets somebody in to see what the rules are doing; only with the world on
release repository, release tokenwhere vulnboxctl upgrade gets releases; the token only for a private repository
install and start vulnboxdinstall the binaries, the unit and the compose guard, and start it; needs root

Every question is pre-filled from the existing config, so a second run is mostly confirming the answers. Picking a game fills in its gameserver, and dims the questions that game answers itself.

↑ ↓ or Tab move between questions, typing edits, Space or ← → change a yes/no or a choice, Enter starts and Esc cancels. The mouse works too: click a question to select it, click it again to change a yes/no or a choice, scroll to move, and click Start or Cancel. If an answer does not add up (a directory that does not exist, a window longer than a tick, a game without a team) the wizard says so instead of starting.

Services are then prepared in parallel, each shown with its progress.

Without a game mode, set the tick start

A GENERIC config has tick.start: 1970-01-01T00:00:00Z. Set it to when the game actually began, so vulnboxd's restart windows line up with the real ticks. See Deploys.

Or script it ​

vulnboxctl init does the same without asking. It reads everything from the config file and the GITHUB_TOKEN environment variable:

sh
sudo -E vulnboxctl init --install

Without --install it prepares the repositories and writes the configs, but leaves installing and starting the daemon to you.

What happens to each service ​

Every service goes through these steps, in order, and stops at the first that fails:

  1. git repository: git init if it is not one already.
  2. branch: rename the branch to main if it is something else.
  3. large files: anything at or over init.max_file_size (500 MiB) is added to .gitignore rather than failing the push later.
  4. initial commit: commit the current state as vulnboxd.

With a GitHub owner and token, also:

  1. deploy key: an ed25519 key for this service alone, in the ssh directory.
  2. GitHub repository: a private service-<name> repository, in the organisation or, failing that, the user account. The prefix is init.repo_prefix, and a name that belongs to something else gets a number: service-web-2, service-web-3, and so on.
  3. key on GitHub: the deploy key is added to that repository with write access.
  4. ssh access: the key is recorded in the repository, so git push by hand uses it too, along with GitHub's host keys (see below).
  5. remote: origin is pointed at git@github.com:<owner>/service-<name>.git. A remote that was there before is kept as origin-before-vulnboxd.
  6. push: main is pushed and set to track the remote.

Safe to run again

Every step looks before it acts, so a second run only does what is still undone. If one service fails, the others carry on, and the run ends with a list of what went wrong and a non-zero exit status. Fix the cause and run it again: only the failed service is touched.

Somewhere other than GitHub ​

Everything above is GitHub-shaped because that is what init can create repositories and deploy keys on. Any other remote works, you just do that part yourself: leave init.github_owner empty, and the setup makes each service a git repository locally without pushing it anywhere. Add your own origin afterwards and vulnboxd polls it like any other.

For authentication it uses the deploy key for that service if there is one, and otherwise whatever ssh would use, including an agent. A service that already has an origin keeps it.

GitHub's host keys ​

A fresh vulnbox knows no ssh hosts, so the first connection to GitHub would normally be a question nobody is there to answer: the daemon refuses it outright (invalid or unknown remote ssh hostkey) and the git cli would wait for a yes.

vulnboxd carries GitHub's three host keys, so it recognises GitHub without being told. Nothing else is taken on trust: on a game network, where every other team is a neighbour, accepting whatever key is offered the first time is exactly what you do not want.

Setup also writes them to <ssh_dir>/known_hosts and points each service's core.sshCommand at it with StrictHostKeyChecking=yes, so pushing by hand verifies GitHub the same way and still needs no yes.

GitHub rotates these keys rarely and announces it when it does. If it has, pushes stop with an unknown host key until vulnboxd is upgraded; adding the new key to ~/.ssh/known_hosts also works, since any host vulnboxd does not carry keys for is looked up there as usual.

After setup ​

Once vulnboxd is running it takes over the services at the end of the next tick. That moves every public port behind the proxy and recreates the containers, which costs a few seconds of downtime once. After that:

  • Each service has a vulnboxd-rules.yml, committed and pushed. Edit it through git or on the box; see Rules.
  • Use vulnboxctl compose <service> ... instead of docker compose; see Day-to-day operation.
  • Watch it with vulnboxctl top.

Checking it worked ​

sh
systemctl status vulnboxd
vulnboxctl top

In the first window you should see one deploy per service and then nothing: ports answering, rules loaded, no rollbacks. journalctl -u vulnboxd | grep -i warn is the quickest way to find anything it did not like — a rules file that would not load, a port the compose file does not publish.