GitHub

Production deployment

A hardened systemd unit and a production config example ship in the repository's engine/deploy/ directory: engine/deploy/kapkan.service and engine/deploy/config.example.yaml. This page walks through installing the binary as a system service, where each file lives, how secrets are kept out of the YAML, and how to reload configuration without a restart.

iNo build required

The fastest path is a prebuilt package: apt install ./kapkan_*.deb (or the .rpm) does everything in this section for you — see Install the package below. The manual steps after it are for the release tarball. Either way there is nothing to compile; Building from source is only if you want to.

Install

Install the package

On Debian/Ubuntu (.deb) or RHEL/Fedora (.rpm) the package does everything the manual steps below do: it installs the binary to /usr/local/bin/kapkan, creates the unprivileged kapkan user, lays out /etc/kapkan with a dry-run config.yaml seeded from the example, creates the writable state directory, and installs the hardened systemd unit (to /usr/lib/systemd/system).

VER=v1.0.0
curl -fLO "https://github.com/fornex/kapkan/releases/download/$VER/kapkan_${VER#v}_linux_amd64.deb"
sudo apt install "./kapkan_${VER#v}_linux_amd64.deb"   # RHEL/Fedora: dnf install ./kapkan_<ver>_linux_amd64.rpm

The service is installed but left stopped — a mitigation daemon should not start before you have looked at its config. The shipped config.yaml has dry_run: true (Kapkan computes blackhole routes but does not announce them to your router). Review it, (recommended) set an API token in /etc/kapkan/kapkan.env, then start it on boot and now:

sudoedit /etc/kapkan/config.yaml          # adapt networks, BGP and thresholds
sudo systemctl enable --now kapkan

Upgrade with a newer package (apt install ./kapkan_<newer>.deb) or with update.sh — both keep your edited /etc/kapkan/config.yaml. apt purge kapkan removes every trace, including config and state.

Manual install (tarball)

Prefer to place files yourself, or on a distro without apt/dnf? After unpacking the release tarball (so the deploy/ directory sits next to the kapkan binary), run the equivalent as root:

sudo install -m 0755 kapkan /usr/local/bin/kapkan
sudo groupadd --system kapkan
sudo useradd --system --gid kapkan --no-create-home --home-dir /var/lib/kapkan --shell /usr/sbin/nologin kapkan
sudo install -d -o kapkan -g kapkan /etc/kapkan
sudo install -m 0640 -o kapkan -g kapkan deploy/config.example.yaml /etc/kapkan/config.yaml
# generate a real random API token, never deploy the literal placeholder
echo "KAPKAN_API_TOKEN=$(openssl rand -hex 32)" | sudo install -m 0600 /dev/stdin /etc/kapkan/kapkan.env
sudo install -m 0644 deploy/kapkan.service /etc/systemd/system/kapkan.service
sudo systemctl daemon-reload && sudo systemctl enable --now kapkan

The env file above seeds only KAPKAN_API_TOKEN, since an API token is recommended before you expose the listener (see Authentication). Seeding the env file does not by itself enable auth: the shipped config binds 127.0.0.1 with api.token_env commented out, so the token stays inert. Uncomment api.token_env (or add an api.tokens list) to turn authentication on. Every notification channel is optional — add KAPKAN_TG_TOKEN, SMTP or ClickHouse credentials only if you actually use those features. The full list of *_env keys is under Secrets below.

The unit runs as the kapkan user with ExecStart=/usr/local/bin/kapkan -config /etc/kapkan/config.yaml -log-format json -log-level info -pid-file /run/kapkan/kapkan.pid, restarts on failure, and is heavily sandboxed: NoNewPrivileges, ProtectSystem=strict, PrivateTmp, ReadOnlyPaths=/etc/kapkan, RestrictAddressFamilies=AF_INET AF_INET6, MemoryDenyWriteExecute and more. It also declares StateDirectory=kapkan (mode 0700), giving the daemon a writable /var/lib/kapkan for ban.state_file, and RuntimeDirectory=kapkan, giving it a writable /run/kapkan for the pid file — while ProtectSystem=strict keeps the rest of the filesystem read-only. A crash-loop guard (StartLimitIntervalSec=60, StartLimitBurst=4) sends the unit to the failed state if the binary fails to start four times within a minute, so a bad upgrade is observable rather than thrashing — the upgrade script leans on this together with the /healthz readiness probe.

iBinding BGP to port 179

Kapkan needs no special privileges in dry-run or when peering from a high local port. If you bind the BGP speaker (the component that announces blackhole routes to your router) to the well-known port 179, grant the capability by adding AmbientCapabilities=CAP_NET_BIND_SERVICE to the unit's [Service] section.

After it is running, check status and follow the logs:

sudo systemctl status kapkan
sudo journalctl -u kapkan -f

File layout

PathModePurpose
/usr/local/bin/kapkan0755The single static daemon binary.
/etc/kapkan/config.yaml0640The YAML configuration, owned by kapkan:kapkan.
/etc/kapkan/kapkan.env0600Secrets env file (tokens, SMTP credentials).
/etc/systemd/system/kapkan.service0644The hardened systemd unit. The .deb/.rpm install it to /usr/lib/systemd/system/kapkan.service (vendor location) instead.
/var/lib/kapkan/0700Writable state directory (StateDirectory=kapkan); holds ban.state_file when ban persistence is enabled.
/run/kapkan/0755Runtime directory (RuntimeDirectory=kapkan), created on start and removed on stop; holds kapkan.pid so kapkan -s reload can find the daemon.

The whole /etc/kapkan directory is mounted read-only to the process via ReadOnlyPaths=/etc/kapkan, so the daemon can read its config and secrets but cannot modify them.

Secrets

No credential belongs in config.yaml. The YAML references each secret by the name of an environment variable, and the unit loads those variables from the env file via EnvironmentFile=-/etc/kapkan/kapkan.env. Keep that file at mode 0600.

The config keys that read from the environment are the *_env keys:

  • notify.telegram.token_env — e.g. KAPKAN_TG_TOKEN, the Telegram bot token.
  • notify.email.username_env / notify.email.password_env — SMTP credentials.
  • api.token_env — the API bearer token (see Authentication).
  • storage.clickhouse.username_env / password_env — ClickHouse credentials, if you enable the optional history store.

For example, the YAML names the variable and the env file supplies its value:

notify:
  telegram:
    token_env: "KAPKAN_TG_TOKEN"   # name of the env var, never the token itself
    chat_id: "-1001234567890"
# /etc/kapkan/kapkan.env  (mode 0600)
KAPKAN_TG_TOKEN=123456:abcdef
KAPKAN_API_TOKEN=...   # a real random token, e.g. from: openssl rand -hex 32

Why this matters

Because secrets live only in process environment, your config.yaml is safe to keep in git and diff. The systemd sandbox keeps /etc/kapkan read-only and runs the daemon as an unprivileged user, so the env file is never writable by the service.

Config reload

Kapkan hot-reloads its configuration on SIGHUP (the Unix signal that tells the daemon to re-read its config without restarting) — no restart, no dropped flow state. The systemd unit wires this up with ExecReload=/bin/kill -HUP $MAINPID, so you reload with:

sudo systemctl reload kapkan

Off the box's service manager, the daemon also answers the nginx-style local control flag, which reads the pid file the unit writes to /run/kapkan/kapkan.pid:

sudo kapkan -s reload    # re-read the config (SIGHUP)
sudo kapkan -s stop      # graceful shutdown (SIGTERM)

See Controlling a running daemon for the full behaviour and exit codes. Equivalently, you can trigger the same reload over the API:

curl -s -X POST localhost:8080/api/v1/config/reload \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $KAPKAN_API_TOKEN"

Content-Type: application/json is mandatory on every mutating POST (the server returns 415 otherwise), and when auth is enabled the reload needs an unscoped (admin) Bearer token — a tenant-scoped token (one limited to a single customer's networks) gets 403.

Most keys reload live, including dry_run, thresholds, networks, hostgroups, baselines, notification settings and sampling.default_rate. Six things are fixed at startup, and a reload that changes any of them is rejected (restart required): the listen addresses, BGP identity (bgp.local_asn / router_id), api.listen, the samples settings, storage settings and geoip settings. See the configuration reference and, for changing the binary without dropping mitigation, Upgrading.

!A reverse proxy in front of the API needs a long read timeout

If you run scrubbing nodes and put a reverse proxy in front of api.listen, the scrub-node rule feed long-polls — a request is held up to 30 s. A short proxy read timeout severs that hold and the node reconnects every request instead of once per rule change. Raise it above the hold: nginx proxy_read_timeout 60s; (or higher); on Envoy, override the 15 s default route timeout for that path. The API itself deliberately sets no write timeout for the same reason.

Building from source

Building requires Go 1.26 or newer. The Makefile provides the standard targets:

make build  # static binary
make test   # go test -race ./...
make lint   # golangci-lint run
make bench  # engine hot-path benchmarks

make build produces the single static kapkan binary you install in the steps above. Tests use synthetic NetFlow/sFlow datagrams built by pkg/flowgen (real wire format) and an in-process GoBGP peer, so no real routers are ever contacted.

!Before you expose Kapkan

Keep dry_run: true until you have validated detection against live telemetry — see Going live. And set an API token before binding the listener beyond 127.0.0.1, or the API and dashboard are reachable unauthenticated — see Authentication.