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_fractionrefuses 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/32at a time, even while each ban stays under the count cap. Example: withnetworkstotaling a/22(1024 IPv4 addresses) andmax_banned_fraction: 0.25, Kapkan refuses new IPv4 bans once about 256 addresses are blackholed. A carpet/24ban counts as its full 256 addresses toward this fraction, not as one.max_bans_per_window(withban_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 request | Result |
|---|---|
Target is in protected_whitelist | 409 — never announced |
Target is outside the configured networks scope | 409 — outside configured networks |
Banning the target would exceed max_active_bans | 409 — cap reached |
Banning the target would exceed max_banned_fraction | 409 — when enabled |
Banning the target would exceed max_bans_per_window | 409 — 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.
Related
- Mitigation & RTBH — how announcements, TTLs, hysteresis and the cap work end to end.
- Going live — validate detection in dry-run, then turn it off safely.
- Configuration reference — the
dry_run,ban,protected_whitelistandnetworkskeys. - Under attack right now — the step-by-step runbook for a flood in progress, including manual bans.
- Troubleshooting — what to check when a ban is refused (
409) or routes aren't landing.