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 │
│ │
┌────────────────┐ HTTPS_PROXY ┌──────────────────────┐ │
│ agent host │ ────────────────► │ collector (LAN) │ │
│ laptop / │ http://lan:8080 │ │ │
│ container / │ + mitm CA │ mitmproxy :8080 │ │
│ pod │ │ PDP :8000 (loopback)│ │
└────────────────┘ │ PAC :9443 │ │
│ └──────┬───────────────┘ │
│ │ decisions only │
│ │ (no raw payload) │
└─────────────────────────────────┼───────────────────────────┘
│
▼ HTTPS outbound
┌─────────────────────┐
│ kilasec.com │
│ │
│ Caddy (TLS, route) │
│ cloud.app :8000 │
│ /api/v1/ingest/* │
│ /api/audit /me… │
│ │
│ SQLite (per-tenant)│
└─────────────────────┘
▲
│ HTTPS, session cookie
│
┌─────────────────────┐
│ admin browser │
│ /app/ (React SPA) │
└─────────────────────┘Collector (on-prem)
- mitmproxy on
:8080— terminates TLS for agent traffic. The first time a client connects, mitmproxy mints a per-connection cert signed by a CA the collector generated on first run. That CA must be trusted on every agent host that goes through the proxy. - PDP (Policy Decision Point) on
127.0.0.1:8000— a small FastAPI app that loadspolicies.yamland answersPOST /evaluatewith{verdict, matched_rule, reason}. Bound to loopback only — never exposed to the LAN. - PAC server on
:9443— serves the proxy auto-config to DHCP clients. Cert signed by the collector's own mitmproxy 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). One-way: cloud does not call into the collector.
Cloud (kilasec.com)
- Caddy — TLS termination, request routing. Routes
/api/*tolocalhost:8000,/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,dhcp_leases). 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, and nowdest_ip:dest_portsince369f3d2) - Method + URL (path only; no query strings with secrets)
- For redact: the names of redacted fields (
body.card_number), never the values - Token counts + estimated cost (when collector emits usage)
Never crosses:
- Authorization / x-api-key / Bearer values — scrubbed in
_classify_requestbefore the event is shipped. - Request bodies (with the exception of fields the policy author explicitly chose to ship for audit).
- mitmproxy CA private key — the collector generates and keeps it. Cloud never sees it.
- DHCP lease MAC addresses, unless the customer opts into the lease feeder.
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. For the original content, you must SSH into the collector.
→ 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, stored on the cloud VM | The "default" credential before collectors are enrolled. Carried by the legacy ingest path. |
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 (kilasec_session=) | Browser, set by /invite/{code} | All SPA → API calls. Cookie value equals the tenant token; httpOnly so JS can't read it. |
Cloud get_tenant_id() resolves any of these to the same tenant_id, in order: cookie → Authorization: Bearer → x-api-token. Cookie wins so upstream-proxy-injected headers can't override a real session.
Identity model
Names for sources are derived from the network, not from a database we maintain. Priority (strongest first):
- DHCP-pushed identity (vendor option)
- DHCP lease feed (the customer's DHCP server POSTs leases to
/api/v1/leases) - Reverse DNS at the collector
- mDNS / Bonjour on the LAN
x-agentfw-agentrequest header (SDK opt-in)AGENTFW_AGENTenv var- Manual admin override (saved in cloud
identitiestable)
The display name is resolved at query time, so renames apply retroactively. Audit rows store the raw facts (source_ip, mac, 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 (yet). Each tenant's
policies.yamllives on their collectors. 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.