Skip to content

Identity model

The hardest problem in a security product is naming things. Who is making this request? Is it a person? Which one?

kilasec treats this as a layered resolution problem instead of a fixed schema. The audit row stores raw facts (source_ip, mac, hostname at the moment of the event). A name is derived at display time by walking a chain of identity sources, strongest first.

This document is the canonical version of how that resolution works.

What the audit row holds

Three things, immutable:

request.metadata.source_ip       e.g. "10.0.3.42"
request.metadata.identity.mac    e.g. "3c:22:fb:a1:b2:c3"     (when known)
request.metadata.identity.hostname  e.g. "ops-mbp"             (when known, with provenance)

Plus the legacy request.agent field, which the addon sets to whatever the agent "wanted to be called" via x-agentfw-agent header or AGENTFW_AGENT env (default unknown_agent).

Everything else — the friendly name, the team, the principal type — is resolved by joining these facts against the identity sources below.

The resolution chain

In order, strongest signal first. The first signal that returns a name wins.

0. Verified identity token

An agent carrying a valid x-agentfw-agent-token (minted on Identity → Agent identity tokens, injected by the kilasec_agent package) resolves to the name the token was minted for — before anything else in this chain. Unlike every signal below, this one is verified: the collector strips the header and checks the token against the cloud, so a process can't get this name by claiming it. Provenance recorded: identity.agent_source = "token" plus the token fingerprint.

1. Manual admin override

If the admin has assigned a name to this source on the Agents page, that's what we use. The override is keyed by whichever of these is most stable: key_fp (API key fingerprint), mac, hostname, or ip. Persisted in the cloud identities table; tenant-scoped.

Manual overrides win retroactively — when you rename host-10.0.3.42 to mwilson-mbp, every audit row with source_ip=10.0.3.42 immediately displays the new name. Audit rows are not rewritten; the join just resolves to the current name.

2. DHCP-pushed identity

The customer's DHCP server stamps a vendor-class option containing (name, team, vlan) into the lease. The collector reads it via its IdentityResolver (agentfw/identity.py). Strongest possible signal because the customer's own infrastructure is the source of truth.

To use: configure your DHCP server to include kilasec's vendor option in its leases. Most enterprise DHCP (Infoblox, ISC dhcpd with the right snippets) supports this.

3. DHCP lease feed

If you can't or don't want to push identity via DHCP option, run the lease feeder (dev/dhcp-feeder.py) on your DHCP server. It POSTs the lease file to /api/v1/leases every minute. The cloud joins audit rows against it by source_ip at display time.

4. Reverse DNS at the collector

The NetworkNameResolver (agentfw/name_resolver.py) runs socket.gethostbyaddr(source_ip) with a 0.4 s timeout. Works whenever your DHCP server auto-populates PTR records — which is the default on most modern routers.

Provenance recorded: labels.hostname_source = "reverse_dns".

5. mDNS / Bonjour

Same resolver tries an mDNS scan on the LAN for _workstation._tcp.local.. Macs broadcast their hostname by default. Works without DHCP integration; great for mixed-OS networks.

Provenance: labels.hostname_source = "mdns".

6. x-agentfw-agent request header

An agent process can opt into self-identification by setting x-agentfw-agent: my-bot-name on every request. Useful for headless agents that don't get a DHCP-visible hostname (containers, CI jobs). The kilasec_agent package sets this automatically on all outbound HTTP; add a token (source 0) when you need the name to be verified rather than claimed. The collector strips the header before forwarding upstream.

7. AGENTFW_AGENT env var

Set on the agent host before launching the process:

bash
export AGENTFW_AGENT=qa_eval_loop
python my_agent.py

Default if unset: unknown_agent.

8. Fall through — IP-based cluster

When none of the above resolved a name, the cluster on the Agents page is keyed on source_ip. The card shows host-10.0.3.42 — better than unknown_agent because one host gets one card, not "everything unnamed collapses into one mega-cluster."

Provenance is visible

Every name on the Agents page carries a small badge telling you which source set it: from network (reverse_dns), from agent header, manual, from DHCP. This is critical for trust — an admin needs to know whether "support-bot-v2" came from the agent itself (low trust; the agent named itself) or from the network (high trust; physical layer attribution).

Screenshot: Agents card showing the provenance sublinedocs/public/screenshots/agents-provenance-close.png

What gets clustered together

The Agents page groups by the strongest stable identifier seen on the events. The priority for the cluster key:

real agent name (not "unknown_agent")  →  hostname  →  source_ip  →  "unknown"

That means if 50 events have source_ip 10.0.3.42 and 5 of them somehow carried x-agentfw-agent: dora-prod, you get one card called dora-prod containing all 55 events (because the agent name is the strongest signal). The other 50 still get attributed correctly because the cluster's source_ips field shows [10.0.3.42] regardless of which header they carried.

Renames apply retroactively — why

This is a deliberate design choice. The naive alternative would be to stamp the resolved name onto each audit row at ingest time — but then renames don't apply to old rows, and history gradually drifts.

By resolving at display time, two properties hold:

  1. A rename is one click. You don't backfill history; the join just picks up the new value.
  2. Renames are reversible. Drop the override and the original network-derived name comes back.

The cost is one extra index lookup per audit row read. SQLite handles that trivially at our event volumes. When we move to Postgres, the same pattern with a materialized view solves the higher-cardinality case.

What about API keys?

The collector also fingerprints provider API keys: it hashes the x-api-key / Bearer value locally (the raw key never leaves the collector) and stamps the short digest into request.metadata.identity.api_key_fp. When no other signal resolved a name, the fingerprint itself (fp:abc123…) becomes the agent name, so traffic from one key clusters on one card instead of collapsing into unknown_agent.

This matters for enterprise API traffic, where:

  • The auth header is the identity (the key is issued per-developer or per-team).
  • It's opaque (not a JWT — JWT decoding doesn't help with sk-ant-… or sk-proj-…).
  • A one-way fingerprint lets you attribute without storing the key.

Note the limits: the fingerprint identifies the key, not the agent — if several agents share one provider key they share one fingerprint, and if a stronger signal (like a parsed User-Agent) already named the flow, the fingerprint stays metadata-only. For per-agent attribution that holds regardless of key sharing, use identity tokens (source 0).

→ See Naming agents for the operational guide.

Documentation for kilasec — the AI Agent Firewall.