Multi-tenancy
One Kapkan instance can serve many customers (a managed service provider (MSP) or internet data center (IDC) use case) and give each a token that sees and touches only their own attacks, bans and hosts. A tenant is just an optional label on a hostgroup — there is no new top-level object, no separate tenant registry, and the detection hot path is untouched.
tenant: "house" # optional: label the global/fallback group
hostgroups:
- name: custA-web
tenant: "customerA" # this group belongs to customerA
networks: ["203.0.113.0/26"]
- name: custA-dns
tenant: "customerA" # a tenant can span several groups
networks: ["203.0.113.64/26"]
- name: custB
tenant: "customerB"
networks: ["198.51.100.0/24"]
- name: shared-infra # no tenant → visible only to admin tokens
networks: ["192.0.2.0/24"]
api:
tokens:
- { name: admin, token_env: KAPKAN_ADMIN, role: operator }
- { name: a-portal, token_env: KAPKAN_A, role: viewer, tenant: "customerA" }
- { name: b-ops, token_env: KAPKAN_B, role: operator, tenant: "customerB" }
How to set it up
Three steps map directly to the YAML above:
- Tag each customer's hostgroup(s) with
tenant: "customerA"(the same label can repeat across several groups — a customer's web and DNS groups, say). - Add an API token under
api.tokenscarrying that sametenant:plus arole:—vieweris read-only,operatorcan also ban/unban. - Hand the customer that token. They now see and touch only their own prefixes; everything else (other tenants, untagged groups) is invisible to them.
That's the whole workflow. The rest of this page explains how ownership is resolved and exactly what a scoped token can and can't do.
The model
"Which tenant owns this IP" is answered by the same longest-prefix hostgroup lookup the engine and mitigator already use to attribute traffic — when prefixes overlap, the most specific matching hostgroup wins (longest-prefix match). A tenant can span several hostgroups (a customer with separate web and DNS groups).
Any IP that falls outside every named hostgroup is handled by the implicit global/fallback
group (the catch-all). That global group can carry a top-level tenant so this catch-all
traffic is attributed to a "house" tenant; a named hostgroup with no tenant is unlabeled
and visible only to admin tokens.
A tenant label is not a unit of mitigation policy — thresholds, BGP attributes and escalation stay per-hostgroup. It is purely an ownership tag used to scope the API.
Token scope
An API token gains an optional tenant, building on its role: a
viewer can read but not change anything; an operator can also ban/unban.
| Token | Sees | May mutate |
|---|---|---|
unscoped (no tenant) | all tenants (admin) | everything its role allows (a viewer reads all tenants; an operator may ban/unban in all tenants) |
tenant: customerA, role: viewer | only customerA | nothing |
tenant: customerA, role: operator | only customerA | only customerA |
An unscoped token is an admin — the default, fully backward-compatible behavior. A token scoped to a tenant that no hostgroup uses is rejected at config load (a typo would otherwise silently see nothing).
iThe agent role sits outside this axis
A scrub node's agent token is not a lesser
viewer with a tenant — it is off the role ladder entirely, and it cannot be tenant-scoped
(the combination is a config error). The rule feed and node inventory span every
tenant by design: a scrub node filters for the whole deployment, so per-node tenant scoping is a
fleet concern, not a token one. Treat an agent token as deployment-wide, and
keep it off boxes that hold customer-facing data.
Enforcement
Scoping is default-deny: a scoped token only ever sees its own tenant. The tenant is derived once during authentication and applied to every row, so a scoped token can never fall through to another tenant's data.
- Reads —
/api/v1/status,/attacks,/hosts,/bansand/auditreturn only rows whose owning group carries the caller's tenant./statusis rebuilt per scope: a scoped token gets only its own hostgroups and counts, never the global protected networks, the global thresholds, or the deployment-wide fallback group's config row — so a tenant never learns another's prefixes or BGP posture. A house tenant (the fallback group labeled with atenant) still sees its catch-all attacks, bans and hosts; only that group's config row is hidden. The audit log is bound to the caller's tenant server-side, and atargetfilter naming an out-of-tenant address is refused — no cross-tenant existence oracle. - Mutations — a scoped operator may
ban/unbanonly within its own prefixes. A target outside the tenant returns a uniform403whether or not a ban exists, so a tenant cannot probe another's ban state.POST /api/v1/config/reloadis admin-only — a reload rewrites every tenant's policy and the token set itself. - Ambiguity fails closed — if one bearer secret matches tokens of different role or tenant (a reused secret), the request is refused rather than resolved to the wider scope.
Verify isolation
Hand the scoped token to curl and confirm it only sees its own tenant. /api/v1/status
returns just that tenant's hostgroups:
curl -H "Authorization: Bearer $KAPKAN_A" http://127.0.0.1:8080/api/v1/status
# → only customerA's hostgroups and counts; no other tenant, no global config
Then confirm a cross-tenant ban is refused — banning an address outside the token's tenant
returns 403:
curl -X POST -H "Authorization: Bearer $KAPKAN_A" \
-d '{"ip":"198.51.100.5"}' http://127.0.0.1:8080/api/v1/ban
# → 403 {"error":"target is outside your tenant"}
i/metrics and the dashboard
/metrics is not tenant-scoped — it stays open for Prometheus scraping and requires no token,
even when API auth is configured. The dashboard
shell is shared (it holds no data of its own); every data call it makes is filtered by the
token pasted into it, so a tenant's viewer token shows only that tenant's data.
!Ownership follows the current config
A scoped token's visibility is computed against the live config: re-tenanting a prefix (an admin-only reload) hands its in-flight attacks and bans to the new owner immediately. This is deliberate — control follows current ownership — but it means a mistaken reload can briefly move rows between tenants.
Backward compatibility
No tenant configured anywhere = single-tenant behavior, byte-for-byte: every group is unlabeled, every token is an unscoped admin, and the API returns everything as before. Tenancy is opt-in and you cannot get isolation on an open (token-less) API — there is no principal to scope.
Related
- Hostgroups — the prefix groups a tenant labels.
- Authentication — token roles the tenant scope builds on.
- Audit log — the per-tenant trail of operator actions.
- REST API — the endpoints that are filtered per tenant.
- Configuration reference