Architecture
kilasec is a network-layer firewall for AI traffic. It sits between the agents that want to make API calls and the providers that serve them, decides what's allowed, and forwards the decision (not the payload) to a cloud control plane.
The two halves
CUSTOMER NETWORK
─────────────────────────────────────────────────────────────────────────
agent host / laptop / container / pod
┌──────────────────────────────────────┐
│ Kilasec Connect (endpoints) │ two lanes, ONE policy engine:
│ • proxy lane loopback :3128 │──┐ mitm, tenant CA on device
│ • gateway lane loopback :3129 │──┤ CA-free, 1 upstream TLS conn
└──────────────────────────────────────┘ │
browsers / OS-proxied apps ─────────────────┤ HTTPS_PROXY = lan:8080 (CONNECT)
▼
┌─ COLLECTOR (one container, on-prem) ──────────────────────────
│
│ kilasec-dp (Rust) :8080 ← CONNECT + TLS-terminate,
│ forge per-host leaf from tenant CA (this IS the mitm)
│ classify → evaluate → enforce, in-process
│ device-TLS hop :8443
│ │ unix socket: events + slow path
│ ▼
│ mgmt plane 127.0.0.1:8000 (loopback, bearer-auth'd #527)
│ /evaluate: approval holds · cloud who-is · token verify
│ · uplink · rule/policy/scope CRUD
│ PAC over TLS :9443
└──────────────────────────────────────┬───────────────────────
│ decisions only — no raw payload
─────────────────────────────────────────────┼─────────────────────────────
▼ HTTPS outbound (uplink, one-way)
┌────────────────────────────┐
│ kilasec.com (CLOUD) │
│ Caddy (TLS, route) │
│ cloud.app :8000 FastAPI │
│ /api/v1/ingest/* │
│ /api/audit /me … │
│ SQLite (per-tenant) │
└─────────────┬──────────────┘
▲ HTTPS, httpOnly cookie
┌─────────────┴──────────────┐
│ admin browser │
│ /app/ (React SPA) │
└────────────────────────────┘On the ports:
HTTPS_PROXY = lan:8080is the proxy address (http-scheme, as every forward proxy is named). The agent's actual traffic to the provider is HTTPS, tunneled viaCONNECTand TLS-terminated bykilasec-dp— nothing runs in plaintext on the wire. The cloud uplink and the admin browser are both TLS.On "mitm": the collector still man-in-the-middles every in-scope TLS flow (forge a per-host leaf from the tenant CA, terminate, inspect, re-originate) — that is the product. What changed is the implementation: the old mitmproxy (Python) was replaced by
kilasec-dp(compiled Rust) with the policy engine ported to thekilasec-policycrate. There is no mitmproxy process anymore; the name survives only in the on-disk CA filename (mitmproxy-ca.pem, kept for compat) and legacy comments.
The collector is one container (entrypoint.py supervises the processes):
kilasec-dp(Rust) on:8080— the data path. Terminates TLS for agent traffic by forging a per-host leaf cert signed by the tenant CA (set up on first run), then classifies the flow, resolves identity, and evaluates the rulebase in-process via thekilasec-policycrate and enforces the verdict itself. That CA must be trusted on every agent host that goes through the proxy lane. Also binds a device-TLS hop on:8443for Kilasec Connect endpoints. (This replaced the old mitmproxy addon — seeDESIGN-collector-dataplane-rearch.md.)- Management plane on
127.0.0.1:8000— a FastAPI app (agentfw.proxy:app, uvicorn). It is the dp's slow path:POST /evaluatefor the three things the dp can't decide locally (approval holds, cloud who-is, agent-token verify — once per session), plus the dp's event-socket consumer, thecost.jsonfeed, rule/policy/scope CRUD, and the cloud uplink. Bound to loopback only, and bearer-auth'd whenever a cloud/mgmt token is set (AGENTFW_MGMT_TOKEN/AGENTFW_CLOUD_TOKEN;/healthz+ PAC exempt;AGENTFW_MGMT_AUTH=0restores open behavior). - PAC server (TLS) on
:9443— serves the proxy auto-config. Cert signed by the collector's own tenant CA so the customer never needs a separate certificate authority. - Cloud uplink — POSTs decisions to
/api/v1/ingest/events(and/api/v1/ingest/usage,/api/v1/ingest/agents) and pulls policy/scope/pricing (guarding a last-known-good). One-way: the cloud never calls into the collector.
Two lanes onto the collector (devices running Kilasec Connect)
On endpoint devices, the Kilasec Connect client gives traffic two lanes into the same collector and the same policy engine — which lane a request takes changes how it's carried, never whether it's inspected:
| Proxy lane (mitm) | AI-gateway lane (CA-free) | |
|---|---|---|
| Who uses it | Browsers, desktop apps, anything steered by the OS proxy | AI CLIs / SDKs that honor ANTHROPIC_BASE_URL / OPENAI_BASE_URL |
| Transport | HTTPS via local proxy :3128; TLS re-terminated with the tenant CA | Plain HTTP on loopback :3129 to the local gateway, which holds ONE upstream TLS connection |
| CA required on device | Yes (tenant inspection CA) | No — no forged certs, nothing in any trust store |
| Typical overhead | ~2 extra TLS handshakes per fresh connection | ~single-digit ms |
The gateway is an optimization, not the enforcement boundary: a tool that misses the gateway environment (a shell opened before the agent, a GUI app) falls back to the proxy lane and is inspected identically. Connect's shell hook self-heals stale terminals onto the gateway at their next prompt, and the menu-bar app surfaces when recent AI traffic took the slow lane.
Cloud (kilasec.com)
- Caddy — TLS termination, request routing. Routes
/api/*tolocalhost:8000,/auth/invite/*for onboarding,/installfor the collector installer,/app/*for the SPA, root for marketing. - cloud.app on
:8000— FastAPI. Receives ingest, serves the read API, handles invite redemption and session cookies. - SQLite — per-tenant tables (
events,usage_records,agent_profiles,tenants,collectors,invites,identities, …). Tenant isolation is enforced at every endpoint viaDepends(get_tenant_id). - SPA — React app at
/app/. Cookie-authed, talks to the same-origin API viafetch("/api/…").
What crosses the on-prem / cloud boundary
This boundary matters more than any other architectural detail.
Crosses (collector → cloud):
- Verdict (
allow / deny / redact / require_approval / log) - Matched rule name + reason
- Agent identity (
name, optionalhostname,principal_type, source IP) - Destination (
host/tool/model) - Method + URL
- For redact: the paths that were masked, never the values
- Workload attribution (process / container / pod) when the eBPF host sensor is running
- Token counts + estimated cost (when collector emits usage)
Never crosses:
- Authorization / x-api-key / Bearer values — scrubbed by the collector before the event is shipped (only a fingerprint may remain).
- Request bodies (prompt/tool content is replaced with a size stub unless the operator explicitly opts in via
AGENTFW_SHIP_PROMPT_CONTENT). - The TLS-inspection CA private key — held tenant-side, not reconstructable from what the cloud stores.
This means the cloud UI cannot reconstruct the raw conversation between an agent and a provider. It can only tell you that a request happened, what kind, to where, with what verdict.
→ See On-prem / cloud boundary for the full list and why it's drawn where it's drawn.
Auth model
Two distinct credential types. They are not interchangeable.
| Token | Who holds it | Used for |
|---|---|---|
Tenant API token (agt_…) | One per tenant; created and rotated by an admin on API Access, stored only as a hash | Scripts, SDKs, connectors and the host sensor. Scoped to read / policy / connector; CA, collector and device lifecycle stay admin-session only. Expiring per-assistant keys for Connect Claude are the same format. |
Per-collector token (agt_collector_…) | Each collector, in /etc/kilasec/collector.env | All ingest after enrollment. Lets you revoke one collector without rotating the others. |
| Session cookie | Browser, set when a user signs in via /auth/invite/{code} | All SPA → API calls. httpOnly so JS can't read it. |
Cloud get_tenant_id() resolves any of these to the same tenant_id, checking a token first (Authorization: Bearer, then X-Api-Token) and falling back to the session cookie when no token is present.
Identity model
A source's name is derived by walking a chain of signals, strongest first:
- Verified agent token — the
kilasec_agentpackage sends a minted token the collector verifies against the cloud. The only authenticated name in the chain. - Manual admin override (saved in the cloud
identitiestable) - Directory/DHCP-pushed identity
- Reverse DNS at the collector
- mDNS / Bonjour on the LAN
x-agentfw-agentrequest header (self-declared)- Parsed User-Agent (
claude-cli,openai-python,browser, …) AGENTFW_AGENTenv var (defaultunknown_agent)
The display name is resolved at query time, so renames apply retroactively. Audit rows store the raw facts (source_ip, hostname at event time) immutably.
→ Full details: Identity model.
What the system does not do
- It doesn't terminate connections at the cloud — only at the on-prem collector. The cloud sees post-fact decisions, never live traffic.
- It doesn't run the policy engine in the cloud. Each tenant's policy is authored in the cloud and pulled by their collectors, but evaluation happens on the collector, inline. Cloud only knows what got matched.
- It doesn't hold customer secrets. Provider API keys stay on the customer's host; only fingerprints / decisions / redaction trails reach the cloud.
Those are deliberate. They let kilasec ship a security product without becoming a new high-value target on the customer's network.