Skip to content

Configuration ​

vulnboxd reads /etc/vulnboxd/config.yml, or the file given with -c. The file is optional: without it, every setting has its default. Unknown keys are errors, so a typo is reported rather than ignored.

Durations are written like 500ms, 10s, 2m or 1h 30m. Sizes here are plain byte counts, except pcap.buffer_mb, which is megabytes. Sizes in a rules file take K, M and G — see larger_than.

Complete example ​

These are the defaults, with every key spelled out:

yaml
services_dir: /srv
exclude: []
rules_file: vulnboxd-rules.yml
state_dir: /var/lib/vulnboxd
listen: "::"
backend_port_base: 20000
poll_interval: 10s
metrics: 127.0.0.1:9330
bypass: {}
webhook_url: ""

game:
  mode: GENERIC
  team: null
  gameserver: ""
  poll: 15s
  services: {}

flag: null

tick:
  start: 1970-01-01T00:00:00Z
  interval: 60s
  window: 5s

deploy:
  stop_timeout: 1s
  grace: 15s
  health_timeout: 30s

rollback:
  enabled: true
  consecutive: 1
  when: now
  services: {}

monitor:
  interval: 10s
  failures: 3

pcap:
  enabled: false
  listen: 127.0.0.1:4242
  interface: any
  ports: null
  filter: ""
  command: ""
  buffer_mb: 64

minecraft:
  enabled: false
  listen: 127.0.0.1:25565
  password: ""
  max_players: 64
  motd: Welcome. The yard is yours to build in.
  scoreboard: ""

upgrade:
  repo: aarondewes/vulnboxd
  token: ""
  drain: 30s

timeouts:
  idle: 2m
  http_request: 30s
  stall: 1m

limits:
  buffered_body: 8388608
  stalled: 256

init:
  github_owner: ""
  github_token: ""
  repo_prefix: service-
  ssh_dir: /root/.ssh/vulnboxd
  max_file_size: 524288000
  vulnbox_ip: ""
  capture_dir: .
  webhook_url: ""

Services ​

services_dir ​

  • Default: /srv

The directory holding one directory per service, each with a compose file. Can be overridden on the command line with --services-dir.

exclude ​

  • Default: []

Names of service directories to leave alone entirely.

rules_file ​

  • Default: vulnboxd-rules.yml

The name of the rules file inside each service directory.

state_dir ​

  • Default: /var/lib/vulnboxd

Where vulnboxd keeps its state: the generated compose overrides (<state_dir>/<service>/compose.override.yml), the backend port assignments, each service's rollback history (<service>/history.json), the lock that makes one daemon the manager (manager.lock), the upgrade request (upgrade-pending) while one is waiting, and the secret that encrypted flags are keyed with.

Network ​

listen ​

  • Default: "::"

The address the proxies listen on. :: is every address; on Linux it accepts ipv4 connections too. Set 0.0.0.0 for ipv4 only, or a specific address to listen on one interface.

backend_port_base ​

  • Default: 20000

Intercepted containers are republished on loopback ports counting up from here. Assignments are remembered in state_dir, so a service keeps its ports across restarts.

Keep this below the kernel's ephemeral range, cat /proc/sys/net/ipv4/ip_local_port_range, usually from 32768. A port inside it can be handed to an outgoing connection as its source port, and docker then cannot publish the container there at all: the deploy fails with "address already in use", which SO_REUSEADDR does not help with. vulnboxd asks the kernel to keep any backend port that does fall inside the range out of it, through ip_local_reserved_ports, and drops a connection that is holding one when a deploy needs it. Staying below the range needs neither.

A box already running keeps the assignments it has, so changing this does not move a service that is up. Deleting backends.json from state_dir reassigns every port from the new base, at the cost of one redeploy of each service in the next window.

metrics ​

  • Default: 127.0.0.1:9330

Where Prometheus metrics are served, and what vulnboxctl top reads. null turns them off.

DANGER

Keep this on localhost. The numbers say which rules are firing, which is exactly what an attacker would like to know.

bypass ​

  • Default: {}

Headers with exact values, as {header: value}, that let an http request skip every rule of every service. The headers are removed before the request reaches the service, and values need at least 12 characters. A service's rules file can set its own bypass as well; a request that matches either one is let through. See testing your own exploits.

yaml
bypass:
  x-team-bypass: Qm8vT3kX9rWz4LpA

webhook_url ​

  • Default: empty

A Discord-compatible webhook that alerts are posted to: ports that stop answering, checker verdicts, failed builds and deploys, rollbacks, broken rules files. Empty sends none.

The game ​

game ​

  • Default: mode: GENERIC

The game being played. A game mode fills in the flag format, the timing and the vulnbox address, and with team set, reads the checker's verdicts from the gameserver.

keydefault
modeGENERICGENERIC, FAUST_2026 or ECSC_2026
teamyour team's number on the gameserver
gameserverthe mode'swhere the gameserver is
poll15show often the verdicts are read
services{}service directory to scoreboard name, where they are not alike

flag ​

  • Default: the game mode's, or none

The game's flag format, which rules write as {flag}. Written as the game states it; anchors and slashes are dropped, since flags are found inside traffic:

yaml
flag: 'FAUST_[A-Za-z0-9/+]{32}'

vulnboxd works out what encryption needs from it: the part between the fixed text at either end is what gets encrypted, with the characters of its class as the alphabet. To say so yourself, give a capture group around the part to encrypt, an alphabet, and the marker an encrypted flag is stored behind:

yaml
flag:
  pattern: 'flag_([0-9a-f]{32})'
  alphabet: 0123456789abcdef
  stored_prefix: ENCRYPTED_FLAG_
keydefault
patternthe game's flag format, as the game states it
alphabetthe characters of the varying partwhich characters encryption may use
stored_prefixENCRYPTED_FLAG_what a stored flag is written behind, so it cannot be handed in as a flag. A shorter one keeps what the service holds closer to the length it expects

Timing ​

poll_interval ​

  • Default: 10s

How often each service's git repository is fetched for new commits.

tick.start ​

  • Default: 1970-01-01T00:00:00Z, or the game mode's

The start of any tick, as an RFC 3339 timestamp. All ticks are aligned to it, so set it to when the game began. See lining up with the game.

tick.interval ​

  • Default: 60s, or the game mode's

How long a tick lasts.

tick.window ​

  • Default: 5s

The end of each tick in which vulnboxd restarts services and end_of_tick applies. Must be shorter than the tick. This is vulnboxd's own window; if your game defines one, as the ECSC does, set it to match.

Deploys ​

deploy.stop_timeout ​

  • Default: 1s

How long a container being replaced gets to shut down before it is killed. Most services run their app as pid 1, where SIGTERM is ignored, so waiting longer usually only wastes the window. See services that stop slowly.

deploy.grace ​

  • Default: 15s

How long the proxy keeps retrying a service that is not listening, so a deploy that runs over its window shows up as a slow request rather than a failed one.

deploy.health_timeout ​

  • Default: 30s

How long a deployed service gets to have its containers running and every tcp port answering. A new version that does not is rolled back.

Rollback ​

rollback ​

  • Default: enabled: true, consecutive: 1, when: now

When vulnboxd undoes a change by itself, with a pushed commit, because the service stopped working after it. See Rollback for what each setting does and when to change it.

yaml
rollback:
  enabled: true
  consecutive: 1
  when: now
  services:
    notes: { consecutive: 3 }
    legacy: { enabled: false }

Monitoring ​

monitor.interval ​

  • Default: 10s

How often every tcp port of every service is checked for answering. See port checks.

monitor.failures ​

  • Default: 3

Checks in a row a port has to miss before it counts as down and is alerted.

Capture ​

pcap ​

  • Default: off

The capture broker: captures every packet the box sees and serves it over PCAP-over-IP, so tools like tulip and shovel read it live. It runs as its own service, vulnboxd-pcap.

yaml
pcap:
  enabled: true
  listen: 127.0.0.1:4242   # readers connect here; keep it off the public internet
  interface: any           # which interface to capture
  ports: 9000,9100-9200    # only these ports; empty captures every one
  filter: not port 22      # a pcap filter for what to leave out
  command: ""              # the whole capture command, when the above is not enough
  buffer_mb: 64            # how far behind one reader may fall before it is disconnected
keydefault
pcap.enabledfalserun it at all. vulnboxctl install writes the unit when this is set
pcap.listen127.0.0.1:4242where readers connect
pcap.interfaceanywhich interface to capture
pcap.portsthe game mode'ssingle ports and ranges in any mix: 9000, 9000-9999, 9000,9100-9200,8000. Empty captures every port
pcap.filtera pcap filter for what to leave out
pcap.commandthe whole capture command, which must write classic pcap to its output
pcap.buffer_mb64how far behind one reader may fall before it is disconnected

What you set for ports wins over the game mode's. The broker's own listen address is always excluded from the capture, whatever is set here; a command of your own replaces all of it and has to keep its own traffic out itself. See what ends up in the capture.

minecraft ​

  • Default: off

The status world: a Minecraft server where each service is a pillar, lit while the checker says it works. A toy, its own service (vulnboxd-mc), and only in a build made with the minecraft feature — the release archive ships one. The setup asks about it; turning it off here and running the setup again stops the service and removes its unit.

yaml
minecraft:
  enabled: true
  listen: 127.0.0.1:25565   # keep it off the public internet
  password: 'a word'        # empty leaves the gated plot shut to everyone
  max_players: 64
  motd: Welcome. The yard is yours to build in.
  scoreboard: ''            # empty takes the game's gameserver; `none` leaves the screen blank
keydefault
minecraft.enabledfalserun it at all. The setup writes the unit when this is set, and removes it when it is not
minecraft.listen127.0.0.1:25565where players connect. The password crosses the wire in the clear, so keep this on loopback or your team's vpn
minecraft.passwordsaid in chat to be let in to what the rules are doing. Empty keeps that shut to everyone, which is the safe way round
minecraft.max_players64how many at once
minecraft.motda welcomesaid to everyone who joins, so it gives nothing away
minecraft.scoreboardthe game'swhere to read the game's scoreboard from. none leaves the screen blank

Upgrades ​

upgrade ​

  • Default: repo: aarondewes/vulnboxd

Where vulnboxctl upgrade fetches releases from. A private repository needs token, one that can read it; VULNBOXD_UPGRADE_TOKEN in the environment takes precedence. A config file holding a token is written readable by root only.

yaml
upgrade:
  repo: TeamGeramy/vulnboxd
  token: github_pat_...
  drain: 30s
keydefault
upgrade.repoaarondewes/vulnboxdowner/name releases are fetched from
upgrade.tokena token that can read it, for a private repository
upgrade.drain30show long the daemon being replaced stays alive to finish the connections it already had open, while the new one serves everything else. 0 replaces it in place instead, which closes them

A drain needs a unit the daemon can tell systemd about; see the unit has to allow it.

Timeouts and limits ​

timeouts.idle ​

  • Default: 2m

tcp, ftp, http and WebSocket connections with no traffic in either direction for this long are closed.

timeouts.http_request ​

  • Default: 30s

The time a client gets to send its request headers, also between requests on a keep-alive connection, and then again for the body. A request that runs out is answered with 408.

timeouts.stall ​

  • Default: 1m

How long a stall rule holds a connection before dropping it. Longer wastes more of an attacker's time; it does not raise the worst-case cost to vulnboxd, which limits.stalled bounds, but it does mean the tarpit fills sooner.

limits.buffered_body ​

  • Default: 8388608 (8 MiB)

The largest http body held in memory, for rules that need a whole body at once (body, set_body) and for mirroring. Larger messages are refused rather than passed through unchecked. Rules that can stream never buffer more than this. See HTTP bodies.

limits.stalled ​

  • Default: 256

How many connections one port holds open at a time for a stall rule. Past it a stall rule blocks instead, so a flood of matching connections cannot pile up on vulnboxd. Each held connection costs one socket and one task. vulnboxd_connections_stalled says how many are held.

Setup ​

These are only used by vulnboxctl init and setup-interactive.

init.github_owner ​

  • Default: empty

The organisation or user the service repositories are created in. Empty skips everything GitHub-related.

init.github_token ​

  • Default: empty

A token with repo access. The GITHUB_TOKEN environment variable takes precedence, and is the better place for it: the setup wizard never writes the token to the config file.

init.repo_prefix ​

  • Default: service-

Put in front of each service's repository name on GitHub, so a service directory called web becomes service-web. It may hold letters, digits, dots, underscores and dashes, and may be empty for a repository named after the service alone.

If the name it wants belongs to something else, vulnboxd uses the next free one: service-web-2, service-web-3, and so on, up to twenty. A repository vulnboxd made for that service before is used again rather than duplicated, which is how a second run stays idempotent. To put a service in a repository of your own naming, set its origin yourself: a service that already pushes somewhere keeps that repository whatever its name is.

init.ssh_dir ​

  • Default: /root/.ssh/vulnboxd

Where the per-service deploy keys are kept, one key pair per service, along with the known_hosts file holding GitHub's host keys.

init.max_file_size ​

  • Default: 524288000 (500 MiB)

Files this size or larger are added to .gitignore instead of being committed, since GitHub would refuse the push.

init.vulnbox_ip ​

  • Default: the game mode's, or empty

This box's address, written into the capture tools' configuration and named in alerts. A game mode works it out from the team number, or asks the gameserver during setup. Empty writes 0.0.0.0.

init.capture_dir ​

  • Default: . (the current directory)

Where tulip.json (for Tulip) and shovel.yaml (for Shovel) are written, listing every published service port.

init.services_file ​

  • Default: empty, which writes none

Also write a plain list of every service listener to this file: its name, this box's address and the port, one entry each. For the tools that want to be told where the services are rather than work it out — exploit runners, flag submitters, a dashboard's own configuration.

yaml
init:
  services_file: /root/services.yml
yaml
# Written by vulnboxctl: one entry per service listener.
services:
  notes-notes:
    host: "10.60.13.2"
    port: 9100
  shop-web:
    host: "10.60.13.2"
    port: 9000

The names are the ones shovel.yaml uses — the service directory and the compose service, which are the same word when a service has one container. A compose service that publishes more than one port gets one entry per port, with the port on the end of the name, since two entries cannot share a key.

init.webhook_url ​

  • Default: empty, which uses the top-level webhook_url

If set, a JSON message {"content": "..."} is posted here when a setup run starts and when it finishes, with any failures. That is the format Discord webhooks expect.