GitHub

Safety model

Blackholing a route is a blunt instrument: a wrong or runaway announcement can take your own infrastructure offline faster than any attack. Kapkan is built so that cannot happen by accident. The six core guarantees below are enforced in code and covered by tests — they are not configuration options you can forget to set, and they are non-negotiable. On top of them sits an optional blast-radius layer you can switch on for extra containment.

Kapkan tracks each mitigation decision as a ban; for the default method a ban is a blackhole route (RTBH) announced over BGP. This page uses "ban" for the tracked object and "blackhole route" for the BGP announcement.

Every one of them applies to automatic detection-driven bans and to manual bans requested through the API alike. There is no path that skips them.

1. Dry-run by default

Kapkan announces a blackhole route only when the config explicitly sets dry_run: false. An absent dry_run key is treated as true. In dry-run, every would-be announcement is logged and exposed through the API and metrics — but never sent to your routers. You can run Kapkan against live production telemetry, watch detection fire, and inspect every route it would announce before it can touch a single prefix.

!You opt in to mitigation, never out

Because a missing dry_run key means dry-run, the only way to start announcing routes is to deliberately write dry_run: false and reload. A typo, a truncated file, or a forgotten key all fail safe. See Going live for the validation checklist before you flip it.

Editing the config under attack is also safe: validate the file first with kapkan -check-config /etc/kapkan/config.yaml (exit 0 = valid, 1 = invalid). A live reload that fails validation is refused — Kapkan logs config reload failed; keeping previous config and keeps running the last good config, so a typo can't take you down mid-attack.

2. No permanent bans

Every announcement carries a TTL (ban.ttl_seconds, a required setting with no default). While an attack is still being detected, Kapkan re-reports it every detection window and the ban's TTL is refreshed — so the route stays up for as long as the flood lasts, however long that is. The moment detection stops, the TTL is no longer refreshed and the ban is withdrawn automatically within one TTL window. There is no code path that produces a route without an expiry: a stuck process, a lost notification, or an operator who walks away can never leave a host blackholed forever.

iYou don't have to size the TTL to the attack

Because a sustained attack refreshes its own TTL, ban.ttl_seconds is not a bet on how long an attack will last — it is how long a ban lingers after the traffic is already gone. A short value (e.g. 1800) frees a recovered host quickly and still never cuts protection short during the flood.

3. Unban hysteresis

A ban is withdrawn only after the target's traffic has stayed below the threshold for ban.unban_hysteresis_seconds, not the instant a single window dips. This prevents the announce/withdraw flapping that would otherwise happen at the edge of an attack, where traffic oscillates just above and below the limit and would churn BGP on every window.

4. Hard ban cap

ban.max_active_bans is an absolute ceiling on the number of simultaneous bans. Once it is reached, new bans are refused and the rejection is alerted loudly rather than announced. This is the circuit breaker against a misconfiguration or a detection storm: Kapkan will never blackhole half your network because a threshold was set too low or an attacker spread across thousands of destinations. A manual ban that would exceed the cap is also refused (see below).

5. Whitelist is absolute

Addresses listed in protected_whitelist — your routers, name servers, and other critical infrastructure — are never announced, regardless of how much traffic they attract and regardless of who asks. The whitelist overrides detection and it overrides manual ban requests. There is no flag that bypasses it.

For carpet (whole-prefix) mitigation the guarantee is even stronger: a prefix that contains a single whitelisted address has its entire mitigation refused — Kapkan will not blackhole or filter a /24 to catch an attack if one protected host lives inside it, whichever carpet.mitigation method is configured. This holds even mid-attack: if a reload adds a whitelisted address into a prefix that is already under a live carpet ban, the ban is withdrawn within about a second rather than waiting out its TTL. When the carpet method is dataplane, the protected host is safe immediately rather than a second later — the kernel checks the protected destination list before it evaluates any installed rule, so the prefix rule cannot reach it even while it is still installed.

6. Scoped detection

Kapkan acts only on destinations inside the configured networks prefixes. Traffic to addresses outside your protected ranges is still counted in metrics, but it can never trigger a ban. You cannot accidentally blackhole a prefix you do not own, even if a flood happens to traverse your network on the way somewhere else.

Optional: blast-radius guards

These guards sit on top of the six core guarantees and are off by default — they are the one part of the safety model you opt into. The hard cap (#4) bounds the number of bans, not how much of your network they cover or how fast they accrue. Two optional ban guards tighten that further:

  • max_banned_fraction refuses new bans once more than that share (per address family) of your protected space is already blackholed — so a poisoned baseline or a spoofed-source storm cannot null-route a large fraction of your own network one /32 at a time, even while each ban stays under the count cap. Example: with networks totaling a /22 (1024 IPv4 addresses) and max_banned_fraction: 0.25, Kapkan refuses new IPv4 bans once about 256 addresses are blackholed. A carpet /24 ban counts as its full 256 addresses toward this fraction, not as one.
  • max_bans_per_window (with ban_window_seconds) bounds how fast new bans accrue, catching a runaway storm before it reaches the count cap.

Both default off (0). A refusal is alerted with a distinct blast_radius_fraction or blast_radius_rate reason on the kapkan_mitigate_bans_rejected_total metric.

How manual bans inherit every rule

The POST /api/v1/ban endpoint is not an escape hatch. A manual ban request is checked against the same guards as automatic detection, and a request that violates one is rejected with HTTP 409 rather than silently announced:

Manual ban requestResult
Target is in protected_whitelist409 — never announced
Target is outside the configured networks scope409 — outside configured networks
Banning the target would exceed max_active_bans409 — cap reached
Banning the target would exceed max_banned_fraction409 — when enabled
Banning the target would exceed max_bans_per_window409 — when enabled

With tenant-scoped API tokens there is one earlier check: a target belonging to another tenant returns 403 ("target is outside your tenant") before any of these guards run. Single-operator deployments (the common case) never hit this.

If the optional blast-radius guards are enabled, a manual ban is checked against them too, and is rejected with the same 409 when it would push past max_banned_fraction or max_bans_per_window. A manual ban that clears every one of these checks still respects the TTL and hysteresis rules: it auto-expires like any other announcement. The only rule manual bans bypass is the detection threshold itself — that is the entire point of a manual ban — but never the safety guards around the announcement.