Kapkandocs
GitHub

Notifications

Kapkan fires a notification on every attack lifecycle event — when an attack is first detected (attack_started) and when it clears (attack_ended). Each event is delivered to every configured channel: Telegram, Slack, email, a generic JSON webhook and an exec hook.

Delivery is best-effort and asynchronous. A slow or failing channel never stalls detection or mitigation, and each channel is bounded by its own worker pool — under a carpet-bomb attack that emits one event per host, excess notifications are dropped and counted rather than piling up unbounded connections or hook processes.

iSecrets come from the environment

Bot tokens and SMTP credentials are read from named environment variables, never from the config file. The file holds only the name of the variable (for example token_env: "KAPKAN_TG_TOKEN"), so you can keep the config in git.

Channels

Telegram

Set notify.telegram.token_env to the name of the environment variable holding your bot token, and notify.telegram.chat_id to the target chat. The token is read from that variable at send time; it is never stored in the file. Messages are sent as HTML and include the attack target, classification, triggering metric and ban state.

Slack

Set notify.slack.webhook_url to a Slack incoming webhook URL. Kapkan POSTs a formatted message to it on each event. Leave it empty to disable the channel.

Email (SMTP)

Configure notify.email to send SMTP mail. Set smtp_host to host:port (an empty value disables the channel), from to the envelope sender, and to to a list of recipients. Credentials are read from the environment via username_env and password_env.

KeyMeaning
smtp_hostSMTP server as host:port; empty disables email.
fromEnvelope and header sender address.
toList of recipient addresses.
username_envEnv var holding the SMTP username (enables AUTH PLAIN).
password_envEnv var holding the SMTP password.
require_tlsForce STARTTLS even without credentials.

Kapkan uses STARTTLS whenever the server advertises it. STARTTLS is required when credentials are configured (a non-empty username_env) or when require_tls: true — in that case, a server that does not offer STARTTLS is refused with a loud error rather than falling back to cleartext, which turns an active downgrade attack into a visible failure. When TLS is neither offered nor required, delivery to a non-loopback host proceeds in plaintext and is loudly logged.

!Plaintext to remote hosts is logged, not blocked

If your SMTP server offers no STARTTLS and you set no credentials and no require_tls, Kapkan will send mail in the clear to a non-loopback host and emit a warning. Set require_tls: true to make that case fail instead.

Webhook

Set notify.webhook.url to any HTTPS endpoint. Kapkan sends an HTTP POST with a Content-Type: application/json body — the callback payload described below — on every attack start and end. Non-2xx responses are treated as a delivery error and counted. The request carries a bounded timeout, so a hung receiver cannot tie up a delivery slot.

POST /your/endpoint HTTP/1.1
Content-Type: application/json

Exec hook

Set notify.exec.command to the absolute path of an existing executable. On every event Kapkan runs it directly — no shell — with:

  • The callback payload JSON on stdin.
  • The event name (attack_started or attack_ended) as argv[1].
  • A minimal environment: only PATH, HOME, TZ, LANG, USER and TMPDIR are passed through. The daemon's secrets (bot tokens, SMTP credentials) are never inherited by the hook or its children.

The command must exist and be executable when the config loads. timeout_seconds (default 10) bounds each run; on timeout Kapkan kills the hook's whole process group so grandchildren cannot survive as orphans. The hook receives the same schema as the webhook, so a single script can serve both channels.

!The hook runs as the daemon user

The exec command runs with the privileges of the Kapkan process. Treat the script as part of your trusted base, keep it on a path only root can write, and never read untrusted attacker-controlled fields back into a shell.

Configuration

A complete notify block. Every channel is optional — an empty smtp_host, url, webhook_url or command disables that channel.

notify:
  telegram:
    token_env: "KAPKAN_TG_TOKEN"   # token is read from this env var, never the file
    chat_id: "-1001234567890"
  webhook:
    url: ""                        # generic JSON POST on every attack start/end
    # payload schema: docs/callback-schema.json (check schema_version)
  slack:
    webhook_url: ""                # Slack incoming webhook
  email:                           # SMTP; credentials from env vars
    smtp_host: ""                  # "mail.example.com:587"; empty disables
    from: ""
    to: []                         # ["ops@example.com"]
    username_env: "KAPKAN_SMTP_USER"
    password_env: "KAPKAN_SMTP_PASS"
    require_tls: false             # STARTTLS is auto-required when credentials are set
  exec:                            # hook run on every attack event (no shell)
    command: ""                    # absolute path to an existing executable
    timeout_seconds: 10

Callback payload schema

The webhook body and the exec hook's stdin carry the same JSON object. It is versioned by schema_version, currently 1. The authoritative definition lives in docs/callback-schema.json.

iForward compatibility

Consumers MUST check schema_version and SHOULD ignore unknown fields. The version is bumped only on breaking changes, so new fields can be added without breaking you.

FieldTypeNotes
schema_versionintegerAlways 1 in this release.
eventstringattack_started or attack_ended.
scopestringhost (a single target is under attack) or group (a calculation: total hostgroup's summed traffic; group set, target absent).
targetstringAttacked host — or, for outgoing attacks, the attacking host. Absent for group-scoped events.
groupstringOwning hostgroup name; global for the implicit fallback group.
directionstringincoming, or outgoing when the target originates the attack (likely compromised).
metricstringThe threshold that tripped (for example pps, mbps, udp_pps, tcp_syn_pps, frag_pps).
ratenumberObserved value of the triggering metric, sampling-corrected.
thresholdnumberConfigured limit of that metric at attack start.
ppsnumberSampling-corrected packets per second.
mbpsnumberSampling-corrected megabits per second.
flows_per_secnumberSampling-corrected flows per second.
ban_statestringactive, withdrawn or rejected, when mitigation was consulted.
routestringThe blackhole route announcement (the would-be route in dry-run).
dry_runbooleanWhether mitigation was in dry-run for this event.
atstringRFC 3339 timestamp of the event.
classificationobjectInferred attack vector — attack_started only.
sampleobjectFlow sample captured before the threshold tripped — attack_started only, when the traffic buffer is enabled.

The classification object carries the inferred type (for example ntp_amplification, syn_flood, udp_flood, or mixed when no signature matched), a confidence share from 0 to 1, and src_port for amplification vectors. The sample object carries truncated flows plus top-K top_sources, top_src_ports, top_dst_ports and protocols counters and a total_packets denominator.

Sample payload

A realistic attack_started event for an NTP-amplification flood, detected in dry-run:

{
  "schema_version": 1,
  "event": "attack_started",
  "scope": "host",
  "target": "203.0.113.45",
  "group": "customers",
  "direction": "incoming",
  "metric": "mbps",
  "rate": 8421.5,
  "threshold": 500,
  "pps": 612000,
  "mbps": 8421.5,
  "flows_per_sec": 9400,
  "ban_state": "active",
  "route": "203.0.113.45/32",
  "dry_run": true,
  "at": "2026-06-13T14:22:07Z",
  "classification": {
    "type": "ntp_amplification",
    "confidence": 0.94,
    "src_port": 123
  },
  "sample": {
    "flows": [
      {
        "src": "198.51.100.7",
        "dst": "203.0.113.45",
        "src_port": 123,
        "dst_port": 51344,
        "proto": "udp",
        "fragment": false,
        "bytes": 4800,
        "packets": 10,
        "sampling_rate": 1024
      }
    ],
    "top_sources": [
      { "key": "198.51.100.7", "packets": 184320, "bytes": 88473600 }
    ],
    "top_src_ports": [
      { "key": "123", "packets": 611000, "bytes": 293280000 }
    ],
    "top_dst_ports": [
      { "key": "51344", "packets": 611000, "bytes": 293280000 }
    ],
    "protocols": [
      { "key": "udp", "packets": 611500, "bytes": 293520000 }
    ],
    "total_packets": 612000
  }
}