Agent Security Model
The KnoxCall agent (both the Go Client Agent and the self-hostedknoxcall/proxy container) handles your credentials. Our job is to make every theft scenario harder than it looks — not just the obvious ones. This page enumerates the threats we defend against and the layers that mitigate them.
Threat Model
| Threat | Mitigation |
|---|---|
| Attacker copies the agent binary to a new machine | Machine-fingerprint binding refuses sessions from foreign hosts |
| Attacker modifies the agent binary | Tamper seal fails signature verification; control plane refuses sessions |
| Attacker captures the TLS-decrypted session request | Nonce + timestamp makes it single-use |
| Attacker steals the disk cache | Encrypted persistence without the session key inside |
| Attacker dumps agent memory | Session rotation limits usable secret lifetime to the current hour |
| Attacker compromises agent credentials | Admin revocation with ≤5-minute propagation |
| Rogue administrator on a stolen host | Single-tenant guard prevents cross-tenant access |
Credential Hierarchy
Three tiers of key material, each with a different blast radius:- MASTER_KEY_B64 never leaves the control plane. Stealing an agent gives you zero progress toward decrypting it.
- Session keys are HKDF-derived per
(agent_id, window_hour). The derivation is stateless — the control plane re-computes on each fetch without storing the key anywhere. - Window rotation means a stolen session key from 14:00 UTC is worthless at 15:00 UTC, even without revocation.
Machine-Fingerprint Binding
Every/agent/v1/session request includes a machine_fingerprint — a SHA-256 of stable host signals:
/etc/machine-id(systemd) or/var/lib/dbus/machine-id(fallback)- Primary non-loopback MAC address
- OS hostname
- A random first-run salt persisted alongside the cache
machine_fingerprint_locked=true. Every subsequent fetch must send the same fingerprint — otherwise the control plane:
- Refuses the session with
403 fingerprint_mismatch - Logs a tamper event visible on the agent detail page
- Keeps the agent’s existing binding intact (the thief’s fingerprint is not recorded)
POST /admin/agents/:id/reset-fingerprint. The next fetch rebinds.
Override the fingerprint with KNOXCALL_MACHINE_FINGERPRINT=<custom> when the agent legitimately runs on ephemeral instances that share an identity (e.g. auto-scaling groups behind a warm-pool image).
Tamper Seal
Every agent binary carries abuild_sig = HMAC-SHA256(AGENT_BUILD_SECRET, hash:version). Two-pass build flow:
- Compile with placeholder sig → hash the compiled artefact
- HMAC with the CI-only secret → re-compile with real sig baked in
agent_versions:
- Known sig → session issued normally.
- Unknown sig → tamper event logged. Default mode continues with a warning; strict mode (
KNOXCALL_STRICT_TAMPER_CHECK=true) denies the session.
Replay Protection
Session fetches carry:nonce— 16 random bytes, base64-hextimestamp— seconds since epoch
- Rejects requests with timestamp drift > 5 min (clock-sync check)
- Hashes the nonce and inserts into
agent_session_nonceswith a unique index on(agent_id, nonce_hash)→ a replay hits the unique constraint and is refused - Cleans up nonces older than 1 hour via the log-cleanup cron
Encrypted Disk Cache
The proxy persists a disk cache at/var/lib/knoxcall/session.json so a container restart can serve traffic during the first sync. Security properties:
- Never contains the session key.
session_key_b64is scrubbed before write. - Encrypted at rest with AES-256-GCM, key derived from
MASTER_KEY_B64via HKDF-SHA256 with a per-write random salt. - File mode
0600(owner read/write only). - Magic-prefix marks the cache format version; old plaintext caches are discarded on upgrade.
- On load without a live session key, the proxy serves routes + environments + api_keys from the cache but returns
503for requests that need secret injection until the next live sync repopulatesdecryptedSecrets.
KNOXCALL_PROXY_BUNDLE_CACHE_PATH=off.
Session Rotation
- Session duration: 1 hour
- Renewal trigger: 55 minutes (5-minute grace before expiry)
- Stale-session deadman: at
expires_at, the proxy flips/healthzto 503 and refuses new requests until a successful refresh
Memory Hygiene
On graceful shutdown (SIGTERM, SIGINT):
- Every entry in the decrypted-secrets map is overwritten with empty string before the map is dropped
- The in-memory session key is zeroed
- The disk cache is updated one last time (without the session key)
Single-Tenant Enforcement
WhenDEPLOYMENT_MODE=self_hosted, every request goes through a tenant guard in the proxy router. If the resolved tenant ID differs from KNOXCALL_TENANT_ID:
- Request returns
404(not403— we don’t reveal whether the other tenant exists) - Control plane refuses to issue session bundles containing any other tenant’s data
Revocation
Admin UI → Automation → Agents → select agent → Revoke.- Sets
agent_registrations.status = 'revoked'immediately - Existing in-memory session remains valid until
expires_at(≤ 55 minutes) - Next renewal attempt fails with
403 agent_revoked
MASTER_KEY_B64 — the session key derivation depends on it, so a key rotation invalidates every agent session system-wide within minutes.
What We Don’t Protect Against
Being honest about the limits:- Full root on the agent host — if the attacker owns the kernel, they can read the live session key out of process memory. This is true of every secrets-management tool. Mitigate with host hardening (SELinux, AppArmor, minimal-surface containers).
- Compromised control plane — if knoxcall.com itself is compromised, the attacker has the
AGENT_BUILD_SECRETandMASTER_KEY_B64and can forge anything. Our defence is that the control plane lives behind our security perimeter with a small attack surface and audited operator access. - Insider with database access — a KnoxCall operator with prod-DB access can read any tenant’s re-encrypted secrets. Mitigated by break-glass audit logging on all prod access; true air-gap requires the Phase-1B fully self-hosted architecture.