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
| Path | Mode | Purpose |
|---|---|---|
/usr/local/bin/kapkan | 0755 | The single static daemon binary. |
/etc/kapkan/config.yaml | 0640 | The YAML configuration, owned by kapkan:kapkan. |
/etc/kapkan/kapkan.env | 0600 | Secrets env file (tokens, SMTP credentials). |
/etc/systemd/system/kapkan.service | 0644 | The hardened systemd unit. The .deb/.rpm install it to /usr/lib/systemd/system/kapkan.service (vendor location) instead. |
/var/lib/kapkan/ | 0700 | Writable state directory (StateDirectory=kapkan); holds ban.state_file when ban persistence is enabled. |
/run/kapkan/ | 0755 | Runtime 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.
Related
- Configuration reference — every key in the YAML file.
- Authentication — set a bearer token before exposing the API.
- Going live — validate detection, then turn off dry-run.