Kapkandocs
GitHub

REST API

Kapkan exposes a small JSON REST API for observing detection and driving manual mitigation. It is served on the address in api.listen, which defaults to 127.0.0.1:8080. The same listener also serves the embedded dashboard and the Prometheus /metrics endpoint.

The API is read-mostly: status, active and recent attacks, the tracked-host snapshot, and the ban table are all GETs. Three POST endpoints mutate state — manual ban, manual unban, and config reload.

!Authenticate before exposing it

The default api.listen binds to 127.0.0.1, so the API is safe unauthenticated only on localhost. Before binding beyond loopback, set a bearer token. See Authentication.

All /api/v1 routes pass through the bearer-token check when api.token_env is set; once configured, every request must carry Authorization: Bearer <token>. The POST endpoints additionally require a JSON content type — send Content-Type: application/json. Note that /metrics and the static dashboard shell are served without the token; the data the dashboard loads is fetched through the guarded API.

Endpoints

MethodPathDescription
GET/api/v1/statusMode, uptime, protected networks, thresholds, hostgroups, active attack/ban counts.
GET/api/v1/attacksActive attacks plus the last 100 that ended, with samples and classification.
GET/api/v1/hostsTracked-host snapshot: per-direction rates, learned baselines, attack state.
GET/api/v1/bansAll bans, active and historical.
POST/api/v1/banManually ban an address.
POST/api/v1/unbanManually withdraw a ban.
POST/api/v1/config/reloadRe-read the config file (same as SIGHUP).
GET/metricsPrometheus metrics.

All responses are JSON. Errors return an object of the form {"error": "..."} with an appropriate HTTP status.

GET /api/v1/status

Returns the current operating mode and a summary of what Kapkan is protecting.

{
  "dry_run": true,
  "uptime_seconds": 8123,
  "networks": ["203.0.113.0/24", "198.51.100.0/24"],
  "active_attacks": 1,
  "active_bans": 1,
  "thresholds": {
    "pps": 80000,
    "mbps": 1000,
    "flows_per_sec": 20000
  },
  "hostgroups": [
    { "name": "web", "calculation": "per_host", "mitigation": "blackhole", "ban": true }
  ]
}

dry_run reports the global mode: when true, no blackhole is announced to your routers. thresholds and hostgroups mirror the active configuration; see Detection and Hostgroups for their full shape.

GET /api/v1/attacks

Returns currently active attacks plus the last 100 that ended (newest first). Both arrays hold Attack objects.

FieldTypeNotes
scopestringhost or group. Group-scoped attacks carry no target.
targetstringThe attacked address (host scope).
groupstringHostgroup name (group scope, or the host's group).
directionstringincoming or outgoing.
metricstringThe metric that tripped, e.g. pps, mbps, tcp_syn_pps.
ratenumberThe observed rate at detection, in the metric's unit.
thresholdnumberThe threshold that was crossed.
ratesobjectFull per-protocol rate breakdown (see below).
activebooltrue while ongoing.
ban_statestringactive, withdrawn, or rejected. Omitted when no ban.
methodstringMitigation method: blackhole or flowspec. Omitted when no ban.
routestringThe RTBH route string, or a flowspec: ... summary, when a ban exists.
flowspecarrayThe generated FlowSpec rules, when method is flowspec.
dry_runboolWhether the ban was virtual.
started_atstringRFC 3339 timestamp.
ended_atstringRFC 3339 timestamp; omitted while active.
sampleobjectFlow sample captured at detection; omitted when sampling is off.
classificationobjectInferred attack vector; omitted when unclassified.
{
  "active": [
    {
      "scope": "host",
      "target": "203.0.113.66",
      "group": "web",
      "direction": "incoming",
      "metric": "pps",
      "rate": 412000,
      "threshold": 80000,
      "rates": {
        "pps": 412000,
        "mbps": 3100,
        "flows_per_sec": 9800,
        "udp_pps": 405000,
        "udp_mbps": 3080
      },
      "active": true,
      "ban_state": "active",
      "route": "203.0.113.66/32 next-hop 192.0.2.1 community 65000:666",
      "dry_run": false,
      "started_at": "2026-06-13T09:41:07Z",
      "sample": {
        "top_sources": [
          { "key": "198.51.100.23", "packets": 1240000, "bytes": 1612000000 }
        ],
        "top_src_ports": [
          { "key": "123", "packets": 1180000, "bytes": 1534000000 }
        ],
        "top_dst_ports": [
          { "key": "443", "packets": 1240000, "bytes": 1612000000 }
        ],
        "protocols": [
          { "key": "udp", "packets": 1240000, "bytes": 1612000000 }
        ],
        "total_packets": 1240000
      },
      "classification": {
        "type": "ntp_amplification",
        "confidence": 0.95,
        "src_port": 123
      }
    }
  ],
  "recent": []
}

The rates object carries the base trio (pps, mbps, flows_per_sec) plus the per-protocol fields that are nonzero: tcp_pps, tcp_mbps, udp_pps, udp_mbps, icmp_pps, icmp_mbps, tcp_syn_pps, tcp_syn_mbps, frag_pps, frag_mbps. The sample object summarizes the buffered flows behind the detection — top_sources, top_src_ports, top_dst_ports, protocols (each a list of {key, packets, bytes} counters), an optional raw flows list, and total_packets. The classification type is one of the vectors documented in Detection; confidence is the share (0..1) of attack traffic matching the winning signature, and src_port is the reflected service port for amplification vectors.

GET /api/v1/hosts

Returns a snapshot of every tracked host — the top-talkers data. Each entry is a HostStat.

FieldTypeNotes
targetstringThe host address.
groupstringThe host's hostgroup name.
ratesobjectCurrent incoming windowed rates.
rates_outobjectOutgoing rates; only nonzero when outgoing detection is on.
in_attackboolWhether the host is in any active attack.
metricstringThe metric of the active attack; omitted when not in attack.
directionstringThe active attack's direction; omitted when not in attack.
baselineobjectLearned incoming baseline; present when baselines are configured.
baseline_outobjectLearned outgoing baseline; present when baselines are configured.
{
  "hosts": [
    {
      "target": "203.0.113.66",
      "group": "web",
      "rates": {
        "pps": 412000,
        "mbps": 3100,
        "flows_per_sec": 9800,
        "udp_pps": 405000
      },
      "rates_out": {
        "pps": 120,
        "mbps": 2
      },
      "in_attack": true,
      "metric": "pps",
      "direction": "incoming",
      "baseline": {
        "pps": 950,
        "mbps": 7,
        "flows_per_sec": 60
      },
      "baseline_out": {
        "pps": 110,
        "mbps": 2,
        "flows_per_sec": 14
      }
    }
  ]
}

The baseline and baseline_out objects carry the learned-normal base trio (pps, mbps, flows_per_sec) and appear only while EWMA baselines are configured for the host's group. See Baselines.

GET /api/v1/bans

Returns the full ban table — active and historical. Each entry is a Ban.

FieldTypeNotes
targetstringThe banned address.
prefixstringThe blackhole prefix (/32 or /128).
metricstringThe metric that triggered the ban; omitted for manual bans.
ratenumberObserved rate at ban time; omitted when zero.
thresholdnumberThreshold crossed; omitted when zero.
next_hopstringThe discard next-hop.
communitystringThe RTBH community.
local_prefnumberThe LOCAL_PREF attached to the route; omitted when zero.
routestringThe full route string, or a flowspec: ... summary for FlowSpec bans.
statestringactive, withdrawn, or rejected.
dry_runboolWhether the ban was virtual.
manualbooltrue for operator-requested bans.
started_atstringRFC 3339 timestamp.
expires_atstringTTL expiry; bans are never permanent.
withdrawn_atstringWhen the route was withdrawn; omitted while active.
reasonstringWhy a ban was rejected or withdrawn; omitted otherwise.
methodstringMitigation method: blackhole or flowspec.
flowspecarrayThe generated FlowSpec rules, when method is flowspec.
escalationarrayThe configured escalation ladder, when one is set.
escalation_stepnumberIndex of the ladder's current rung.
{
  "bans": [
    {
      "target": "203.0.113.66",
      "prefix": "203.0.113.66/32",
      "metric": "pps",
      "rate": 412000,
      "threshold": 80000,
      "next_hop": "192.0.2.1",
      "community": "65000:666",
      "route": "203.0.113.66/32 next-hop 192.0.2.1 community 65000:666",
      "state": "active",
      "dry_run": false,
      "manual": false,
      "started_at": "2026-06-13T09:41:07Z",
      "expires_at": "2026-06-13T10:41:07Z"
    }
  ]
}

See Mitigation for how TTL, hysteresis and the ban cap shape this lifecycle.

POST /api/v1/ban

Manually blackholes an address. The body is a single IP:

POST /api/v1/ban
Content-Type: application/json

{"ip": "203.0.113.66"}

The response is the resulting Ban object. A manual ban honors every safety rule that an automatic ban does:

  • A whitelisted target is refused with HTTP 409 Conflict and reason: "whitelisted"; it is never announced.
  • A target outside the configured networks is refused with HTTP 409 and reason: "outside configured networks".
  • A request that would exceed max_active_bans is refused with HTTP 409 and reason: "max_active_bans reached".

In each 409 case the body is still a Ban object with state: "rejected" and the reason field set. An invalid or unparseable IP returns HTTP 400.

!Dry-run still applies

A manual ban respects the global mode. While dry_run is true, the ban is recorded and returned with dry_run: true but no route is announced. See the Safety model.

POST /api/v1/unban

Withdraws an active ban for the given address. The body is the same {"ip": "..."} shape and the same JSON content type is required:

POST /api/v1/unban
Content-Type: application/json

{"ip": "203.0.113.66"}

On success it returns the withdrawn Ban. If there is no active ban for the address, it returns HTTP 404.

POST /api/v1/config/reload

Re-reads the config file from disk and applies it — the same effect as sending SIGHUP. The body is empty; the JSON content type is still required.

{
  "reloaded": true,
  "dry_run": false,
  "thresholds": {
    "pps": 80000,
    "mbps": 1000,
    "flows_per_sec": 20000
  }
}

If the new config fails to parse or validate, the running config is left untouched and the endpoint returns HTTP 400 with the error. See Configuration.

GET /metrics

Serves Prometheus metrics in the standard text exposition format. This endpoint is not under /api/v1 and is served without the bearer token so a scraper can reach it. See Metrics for the full metric list.

  • Authentication — set a bearer token before exposing the listener.
  • Dashboard — the embedded web UI served on the same address.
  • Metrics — the Prometheus /metrics endpoint.