Dashboard
Open http://localhost:8080 in a browser running on the Kapkan host. When you are under attack, look first at the Live mode banner (is Kapkan actually announcing, or only watching?) and the Active attacks list (what is being hit, and how hard).
The web UI is built into the kapkan binary itself, so there is nothing extra to install or
deploy — the same process serves the dashboard, the JSON API and Prometheus metrics.
iOne binary, one listener
The dashboard, the JSON API and /metrics are all served by the same process on the same
address. There is no separate web service to deploy or keep in sync.
Where it lives
The dashboard is served at / on the same address as the REST API — the api.listen
value from your config. With the default of 127.0.0.1:8080, you open it at
http://localhost:8080.
Because the default binds 127.0.0.1, the dashboard is reachable only from the Kapkan host
itself — opening localhost:8080 on your own laptop will show nothing. To view it from your
workstation, forward the port over SSH rather than rebinding api.listen to a public address:
ssh -L 8080:127.0.0.1:8080 user@kapkan-host
Then open http://localhost:8080 on your workstation. (Rebinding the listener to a public address needs an API token first — see the Authentication caveat below.)
To serve only the JSON API and metrics without the UI, disable it:
api:
listen: "127.0.0.1:8080"
dashboard: false # serve the JSON API and /metrics only
With api.dashboard: false, the /api/v1 endpoints and /metrics keep working — only the
embedded UI shell stops being served.
What it shows
The dashboard polls the JSON API and renders everything Kapkan tracks in memory:
-
Live mode — whether the instance is running in dry-run or live, so you can see at a glance whether a detected attack would actually be announced.
-
Active and recent attacks — currently firing attacks plus the recently ended ones, each with its classification of the attack type (for example, NTP/DNS amplification or a SYN flood — see the Detection page for what each type means) and the dominant sources, ports and protocols captured from the flow samples.
-
Top talkers — the tracked-host snapshot with per-direction rates and learned baselines, so you can see who is loudest and how it compares to normal.
-
Hostgroups — your named prefix groups and their state.
-
Scrubbing nodes — shown only when you run managed scrubbing nodes: each node's liveness (it is alive because it keeps polling for rules), how many bans divert to it, and its own reported load, drops, version and dry-run flag — the reported columns are labelled as such, because a node's self-report is advisory, never the source of its liveness.
-
Edge — shown only when edge nodes are configured: every zone of the zones file, with its request rate across the fleet, the nodes it is watch-only on, and a challenge column — off, preview (the rung would ask but only watches, on every node), or the zone's mode with the nodes it bites on (a lever shows as that mode), and a zone-wide challenge with its reason (the node's
zone-rpstrigger) — plus a Who would be challenged panel: the sources the nodes previewed a challenge or a deny for, the busiest first, with the nodes that saw each. Read it before a zone's rung goes live; the view says partial when a node had to cut part of that set, and warns when nodes cut whole zones from their reports. An HTTP/3 column reads the zone's HTTP/3 state: off when the zone neither asks for it nor is served over it; on · 42% · 3/3 when every node reporting the zone serves it, with the share of the last window that arrived over HTTP/3 (the percentage is dropped when the window had no requests); on · 2/3 ready in the watch-only treatment when the zone asks and a node cannot serve it — hover the cell to see which nodes serve it over TCP and why (built without the HTTP/3 module, HTTP/3 switched off on the node, or the readiness probe failed), together with any advisory that node's probe raised; and off · 2 nodes still serving when the zones file no longer asks for HTTP/3 but a node still holds a QUIC listener, because the file and the fleet disagree until the next generation lands. Click a zone's row to open its stored history under the table — see Zone history below.The Nodes column reads the zone's placement, not the count of nodes that happen to be reporting it:
2/2is two of the two nodes this zone is placed on are alive,0/1plus a red UNSERVED badge is the alarm that a placed zone has no live node behind it, and a dash is a zone no node's scope covers at all (akapkan -check-configwarning, not an outage). Hover the cell for the node names; under it, for an unscoped token, sits the zone's hostgroup. Rows the file seeds but no node reports show dashes instead of zeros — a measured zero and nothing measured are different answers — and say why in the challenge column, reading the placement rather than the report count: proxy only for a zone inpolicy.mode: none, not served by any alive node for a deciding zone whose placed nodes are all down or that no node's scope covers, and no report yet for one whose placed node is alive but has not mentioned the zone — a fresh start, a report older than the window, or a zone the node cut from its report.The challenge column is also where the lever is pulled. An operator sees a Challenge… button on every zone in
policy.mode: decide; it opens a small panel that asks for the rung (manual — "Every request without a valid clearance is sent to the clearance page" — or auto), how long to hold it (15 minutes, 1 hour, 6 hours or 24 hours), and an optional reason for the audit row and the nodes' logs. Before you pull it, the panel says what would stop it biting: "Every node reporting this zone only previews its rung, so the challenge will be counted and nobody will be sent to the page" when the rung is watch-only across the fleet, and "No live node serves this zone, so nothing applies the lever until one comes back" for an unserved zone. A running lever then shows in the cell as lever · manual · 14m 58s left — a longer one reads lever · manual · 5h 42m left — counting down to the moment the zone follows its zones file again, with the operator's reason on hover; the same button becomes End, which retires it at once. The countdown is read against the brain's own clock, taken from the response the zone status arrived in, and End stands for as long as the brain reports the lever, whatever this browser's clock makes of the time left: a machine running fast never hides the control for something the fleet is still applying. A refusal is shown in the panel rather than thrown away — including the one answer that deliberately covers two cases, "This zone is not in the zones file, or this token cannot reach it", because the brain answers a zone outside a tenant's reach exactly as it answers a zone that does not exist. The lever is an operator's action: aviewertoken sees the state and no control. A console talking to a Kapkan older than the zones-file merge is offered no Challenge… either — that table is the one it always was, plus, when a lever is in force, the badge for it and the End that retires it.With multi-tenancy, an unscoped token also sees each zone's
tenantunder its name and a row of tenant chips above the table that narrows both the zones and the Who would be challenged panel to one customer (remembered for the browser session; a choice that no longer names a tenant on screen is forgotten rather than left to switch itself back on). A tenant-scoped token sees exactly its own zones with no chips and no banner — the visible to unscoped tokens only notice is reserved for a brain too old to answer it at all. -
Edge nodes — the fleet's own inventory beside its zones, shown under the same condition. One row per configured node: whether it is alive or LOST (the zones poll is the only liveness signal), the agent tokens bound to it and which token last polled as it, its placement scope and how many zones that scope puts on it, and — from the node's own report, labelled (reported) like the scrubbing-node columns — its Kapkan version, the terminator it orchestrates with the live generation, that terminator's HTTP/3 readiness and the certificates it holds, coloured amber inside 30 days of expiry and red inside seven. A node with no token of its own carries an amber shared token badge while any agent token is unbound, and a banner at the top names those tokens: an unbound agent token may poll and report as any node in the fleet. The inventory names tokens and scopes, so it is visible to unscoped tokens only; a scoped one sees the same visible to unscoped tokens only notice the scrubbing-node inventory shows in its place.
-
Ban table — every ban, active and historical (with the scrubbing node column when nodes are configured).
-
Traffic / Reports — live ingress/egress charts built from the in-memory rate buffers, plus a Historical reports section that reads the persisted ClickHouse tables when storage is enabled.
-
Controls — the same actions the API offers: manual ban / unban of an address and config reload (the equivalent of sending
SIGHUP).
Two parts of the dashboard need storage: the Historical reports section on the Traffic / Reports view, and the Edge view's Zone history card with its sources table and the Fleet events card. Both are populated only when ClickHouse storage is enabled, and otherwise render a labeled placeholder explaining how to turn it on.
✓No database required
The dashboard is built entirely from the engine's in-memory state. Historical flow storage is a separate, optional feature — it is not needed to see live attacks, top talkers or bans.
Zone history
Everything else on the Edge view is the last ten seconds. Clicking a zone's row opens that zone's stored history under the table, over 1 h, 24 h or 7 d — the same question asked of a period instead of a window. It reads the edge history, so it needs ClickHouse storage; with storage off the card shows the same labelled placeholder the Traffic view does, never an error. Clicking the row again, or Close, shuts it.
The card carries two charts, and a third when there is HTTP/3 to draw:
- Requests / s — the nodes' windows summed into buckets (one minute, ten minutes or an hour, matching the range) and divided by the real length of the windows that fell in each.
- Refused or would be — denied plus challenged plus would-deny plus would-challenge, so a rung that only previews and one that bites are counted on the same axis. The preview tag says the rung bites on no node now; the chart is titled Would be refused only when the period itself also holds no real refusal, because a rung switched to watch-only an hour ago leaves real denials in the hours behind it.
- HTTP/3 share, % — shown only when some bucket actually saw HTTP/3 requests, because a flat zero line under a zone nobody reaches over QUIC would read as a measurement rather than as an absence.
Under the charts, the period's totals: the most nodes seen reporting in any one bucket, the requests, the refusals (real or previewed), the 4xx/5xx count and the bucket width the engine actually used — it raises the step it was asked for when a range would otherwise hold more than 5 000 buckets, so the figure names the buckets you are looking at.
Below the card, Who would have been challenged — over 1 h (or 24 h / 7 d, following the range you picked) lists the period's telling sources, busiest first: the strongest thing any node did to each (denied, challenged, would deny, would challenge, with the same badges the live table uses), its requests, how many windows and nodes saw it, and when it was first and last seen. This is the question to ask before a zone's rung goes live, over a day rather than over ten seconds.
A Fleet events card closes the view for unscoped tokens: what the brain saw change in the last 24 hours, newest first — a node lost or back, a version, a dry-run flag flipping, a document rendered, a generation installed or refused, a terminator's liveness, an HTTP/3 state, a certificate issued, renewed or gone, a zone-wide challenge starting or ending, a node's clock drifting out of the gate, a report that had to be cut. A tenant-scoped token does not see the card at all: the events name nodes. The history and its sources, by contrast, are readable by a tenant for its own zones — another tenant's zone is refused and the card simply does not appear.
iRead on demand, not on the poll
The zone's history and its sources are read when you open a zone or switch its range; the fleet's events are read whenever the Edge view is on screen for an unscoped token, zone open or not. All three at most every ten seconds, and none of them on the console's three-second poll — they query ClickHouse, and the windows behind them are ten seconds long.
Attack detail
Click an attack in the Active and recent attacks list to open its detail drawer. Alongside the classification, the "why it fired" breakdown, the escalation ladder and the mitigation route, two sections attribute the traffic:
- Captured sample — the dominant Top sources (relabeled Top destinations for an
outgoing attack), Protocols and Top source / dest ports distilled from the flow sample,
plus the total packets sampled. With GeoIP enabled it also shows a Top ASNs row (Top dest
ASNs for outgoing attacks): the busiest autonomous systems by sampling-corrected packets,
listed as
AS<number> <organization>. - Raw flows — a table of individual captured flows (source, protocol, destination, flags,
fragmentation, packets). With a country database loaded, each source carries a small
country-code badge such as
USorDE; hover it to see the source's network organization.
The Top ASNs row and the per-flow country badges appear only when GeoIP attribution is enabled.
Point geoip.asn_database at a MaxMind GeoLite2/GeoIP2 ASN database for the ASNs and organization
names, and geoip.country_database at a country database for the country badges — see the geoip
block in Configuration. Without them the drawer still renders, just without
the ASN row and country tags.
Authentication
By default the API and dashboard are unauthenticated, which is safe only because the default
api.listen binds to 127.0.0.1. When you set an API token, the dashboard adapts: the
static UI shell still loads, but the data behind it does not until you supply the token.
When auth is enabled, the dashboard prompts for the token and keeps it in the browser's
sessionStorage, attaching it as a bearer header on every API request it makes. Each
/api/v1 request is authorized with Authorization: Bearer <token>; /metrics and the
static shell stay open, but the live data and the ban / reload controls do not.
!Set a token before exposing the listener
Before binding api.listen to anything beyond 127.0.0.1, configure an API token. See
Authentication for how to set api.token_env and how requests are
verified.
Related
- REST API — the endpoints the dashboard polls and the control actions it calls.
- Authentication — how to protect the API and dashboard with a token.