Overview
Daevix is split into two kinds of component. The split exists for data sovereignty: any operation on sovereign data happens inside an enclave and never transits the controlplane.
┌──────────────────────────────┐
│ controlplane │ central management plane
│ identity · policy · audit │ (SaaS or self-hosted)
│ · operator API │ never sees sovereign data
└──────────────┬───────────────┘
│ metadata / control only
│ (never sovereign payload)
┌────────────────────┴────────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ enclave (eu-sov-1) │ │ enclave (us-sov-1) │ sovereign data plane
│ K8s + Kata + Cilium │ │ K8s + Kata + Cilium │ in the customer boundary
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ agent container │ │ │ │ agent container │ │
│ │ └─ mounts ─┐ │ │ │ │ ... │ │
│ └────────────┼─────┘ │ │ └──────────────────┘ │
│ IMDS unix sock │ │ │
└───────────────────────┘ └───────────────────────┘
Components
controlplane (dvx serve controlplane)
Central management: identity, policy, audit aggregation, operator API. Deployable as SaaS or self-hosted. Owns its own database, which never holds sovereign data.
Package: internal/controlplane. It must not import internal/enclave.
enclave (deployment unit)
The sovereign data plane — a deployment unit, not a single process: a
collection of Daevix-owned processes co-located inside the customer’s
sovereignty boundary, a Kubernetes cluster using Kata containers (VM-isolated
pods) and Cilium (identity-aware network policy). Every operation on
sovereign data happens here, and each enclave owns its own database holding
only its tenant’s data. Its members are the broker (the control node and
sole controlplane egress), the imds, the network gateway, and the
llmproxy — described below and in enclave-broker.md.
Package namespace: internal/enclave/**. Nothing under it may import
internal/controlplane (lint-enforced).
Deployment shapes = placement drivers. The K8s cluster (Kata + Cilium) is
the first shape; the local micro-VM shape (mission bd_2_0-cwnz M3,
internal/enclave/vmdriver) runs the SAME satellites as bare host processes
on the operator’s own machine, with each AGENT as its own cloud-hypervisor
micro-VM: rootfs derived from the agent’s container image (per-agent qcow2
overlay), identity delivered over a per-agent vsock channel bridged to
localhost by the injected static init (cmd/dvx-init), a file-backed
AgentMapping registry under the enclave state dir, and a vm-only isolation
class. Agent authoring is driver-agnostic (same image + command on both
shapes); one enclave runs exactly one driver; the K8s and VM drivers are
verified against one conformance suite (internal/enclave/cpbroker/driver/drivertest).
broker (dvx serve broker)
The enclave’s central control node and its sole controlplane egress
(enclave-broker.md). The broker core
(internal/enclave/cpbroker) owns the level-triggered desired-vs-actual
loop that turns the operator’s desired agent set into running workloads and
garbage-collects withdrawn ones; the platform touchpoints it drives sit behind
the driver seam (internal/enclave/cpbroker/driver): small interfaces — a
Provisioner (Ensure/Delete/State/List verbs), an ArtifactPublisher
(enclave-scoped credential + policy delivery), an IsolationGate
(fail-closed capability check, ADR 046), and an Attributor (runtime → agent
identity) — assembled into one driver value and built per reconcile pass by
a factory (so rotated platform credentials are picked up). Kubernetes is the
first driver implementation (k8sDriver over the stdlib-only k8sWriter); a
local micro-VM driver is planned on the same seam. The seam crosses no K8s
types: the driver receives a workload-neutral struct and builds its own
platform objects. The Provisioner’s State verb makes runtime drift visible
(a CrashLoopBackOff pod or an externally-deleted one is reported in broker logs
and the operator status API, never silently invisible). The local-issuer path
stays off the seam (issuance is driver-invariant; only delivery varies). One
driver per enclave; the AgentMapping registry is broker-core-owned, not a seam
surface. Every driver implementation passes the interface-level conformance
suite (internal/enclave/cpbroker/driver/drivertest, bead decision 8; the
drivertest.Harness + MemDriver reference fake precedent
kmstest.MemProvider): Ensure idempotence, two-phase teardown with terminating
guards, GC List shape, observe/State verb semantics, and per-agent home
scoping, run against every driver (fake + K8s today; the VM driver joins in M3)
so these semantics cannot drift per driver.
Driver-neutral placement + one driver per enclave, enforced (bd_2_0-cwnz,
cwnz.5): every AgentMapping carries a typed Placement{Driver, ID} plus
per-driver metadata instead of K8s-specific node/namespace fields. The broker
never authors a mapping whose Placement.Driver differs from the enclave’s
configured driver - a mismatched Put is refused loudly (fail-closed, logged as
an error, never applied), and the K8s driver likewise refuses EnsureRuntime
for a foreign-driver placement. The mapping’s Placement.ID is the node the
broker pinned at creation (RuntimeClass-constraint-filtered; see
internal/enclave/registry/provider.go and the drivertest conformance suite).
imds (dvx serve imds)
The node-local IMDS daemon: an enclave satellite that hosts the only interface an in-enclave agent talks to. Modeled on a cloud Instance Metadata Service, it listens on a unix domain socket mounted into the agent’s container and serves the agent its identity, short-lived tokens, and credentials. Agents never reach the controlplane and never reach a remote enclave.
Placement matching is fail-closed (bd_2_0-cwnz, cwnz.5): the daemon
serves only mappings whose Placement names the driver it implements AND whose
Placement.ID positively matches this instance (K8s: the node name). Empty or
mismatched placement fields serve nothing - the old
serve-everywhere-on-empty semantics are gone. A mapping matching no local
placement is surfaced by a rate-limited warn (once per 10m per mapping), so
broker-authored vs node-served drift is observable without spamming every
reconcile poll; the signal never widens the match.
Package: internal/enclave/imdsnode (the daemon), which serves the metadata
service implemented in internal/enclave/imds. It must not import
internal/controlplane.
relay (dvx serve relay)
The enclave’s edge ingress and single publicly-exposable surface
(bd_2_0-ib4.1). The broker is dial-out-only and in-cluster-private, so an
external operator cannot reach its operator API (secret authoring, audit query)
directly. The relay terminates the external operator connection and
reverse-proxies an allow-listed subset of the broker’s operator API to it —
so the broker never has to be exposed. The proxy is default-deny: only the
explicitly listed method+path routes are forwarded; everything else is refused at
the relay and never touches the broker.
Two trust boundaries meet here. The operator-facing side is server-TLS only:
the operator presents a Zitadel bearer over TLS (no client cert). The relay
independently validates that bearer at the edge (defence in depth — it rejects
unauthenticated/expired requests before they reach the private broker) and then
forwards it untouched; the broker re-validates and enforces org scope, so it
stays the authorization authority. The relay→broker hop is mTLS: the relay
presents a broker-minted client identity (dvx-relay-broker-client) and the
broker’s operator listener requires it, so the relay is the only caller that can
reach it — the broker stays private (mutual-TLS gate, plus a CiliumNetworkPolicy
that admits port 8451 only from the relay). The reverse-proxy core is deliberately
generic: the future WS agent-terminal forward and WireGuard relay to agents
(bd_2_0-kzb) slot in as further allow-listed routes.
Package: internal/enclave/relay. It must not import internal/controlplane.
LLM inference gateway (llmproxy) (dvx serve llmproxy)
A node-local enclave satellite that governs and forwards agents’ LLM
inference calls (epic bd_2_0-b06). It is a sovereign-plane process: it sees the
agent’s plaintext prompt bytes (the customer’s most sensitive data) but, like
every enclave component, never dials the controlplane (invariants 1/2). Its
only ingress is the network gateway, over the agent’s attested SVID.
The datapath has two stages:
- Classification + force-route (network gateway). The gateway already
MITM-terminates every redirected egress. When a decrypted call is recognized as
an LLM inference request — a known provider host (
api.anthropic.com,api.openai.com,api.factory.ai,inference.hetzner.com; the host→provider set is the sharedinternal/enclave/llmregregistry) and an exact inference method+path — the gateway force-routes it to llmproxy instead of the real upstream (bd_2_0-b06.1). Classification runs only after destination policy has already allowed the flow, and is fail-closed (bd_2_0-b06, decision B): an unattributed agent, an agent with no held SVID, or an unreachable llmproxy is denied (mediation:llm-route), never sent direct. Non-inference calls to the same host (token counting, model list) pass through unchanged. - Verify + forward (llmproxy). The gateway dials llmproxy over mTLS
presenting the agent’s own attested SVID (reusing the assert-identity tunnel;
the signing key stays in the gateway, outside the agent VM — agent-identity
invariant 6). llmproxy verifies that peer cert into agent/org/enclave
(
agentid.Verifier, the echoverify pattern) and trusts nothing in the HTTP bytes. The gateway conveys the observed original destination and its verified SNI out-of-band as a PROXY-v2 preamble (proxyproto.EncodeV2→Parse; the SNI rides a custom TLV,bd_2_0-izh), so a lyingHostheader cannot redirect egress. llmproxy resolves the provider from the attested SNI (cross-checked against the agent’sHost— a mismatch fails closed), maps provider→canonical upstream via the sharedinternal/enclave/llmregregistry (optional governance override, operator startup default;https-only), and uses the request path only to detect the wire format (never the provider — a path cannot tell apart two providers that share a wire, e.g. Hetzner serves OpenAI’s/v1/...paths). It then applies governance (credential mode, model allow/blocklist) and forwards the request byte-faithfully — including SSE responses, streamed per-chunk (bd_2_0-b06.3/.4/.5/.6). This byte-faithful passthrough is the default mode only: when the cross-format translation flag (--translate/DVX_TRANSLATE) is enabled, a request whose client wire format differs from the upstream’s expected format is translated (request, response, and SSE stream) instead of forwarded byte-faithfully — seedocs/threat-model.md(“Cross-format translation”) for the documented shift this makes to the passthrough property.
Direct egress breaks the classify→route→classify loop (decision C). llmproxy
egresses directly to the real provider, not back through the gateway. This
is deliberate: llmproxy’s own egress attributes to llmproxy, not to an agent,
so the gateway would have nothing to attribute and no reason to re-classify it —
routing it back would loop. The trade-off is a second enclave egress path
distinct from the agent gateway, governed by llmproxy’s own upstream-resolution +
model policy rather than the gateway’s destination policy; the threat model tracks
this as a residual (docs/threat-model.md §4.1).
Package: internal/enclave/llmproxy. It must not import internal/controlplane
(the enclave import fence in .golangci.yml covers all of
internal/enclave/**, so no llmproxy-specific fence is needed).
Agent identity
Agents are arbitrary, untrusted code, so they never hold a credential and never
authenticate. The platform attributes every action to an agent from signals
below the agent (the per-agent socket mount, Cilium identity, the Kata TEE
measurement) and forces all egress through a TLS-terminating gateway. See
agent-identity.md for the full model.
Invariants
- Sovereign data never transits the controlplane. Operations on sovereign data go directly to an enclave.
- Sovereign data never goes to the controlplane; the enclave never depends
on it to serve the data plane. This is a rule about data, not about who
dials whom. An enclave is free to initiate a connection to the controlplane
— hold an outbound SSE channel, poll, pull — and that is the normal, expected
shape (it lets a NAT’d customer-cluster enclave receive nudges without any
inbound reachability). What must never happen is sovereign data — anything not
meant to be stored in the controlplane — flowing to or through it. Only
platform-owned metadata (attestation policy, governance policy) moves
CP→enclave, and it may be delivered out-of-band (file at rest) or pulled by
the enclave. The sovereign payload path is always enclave-local, and a
controlplane outage never stalls it — the enclave serves from last-known
metadata. The pull path
is one reusable primitive, the CP→enclave metadata channel (SSE nudge +
authenticated per-enclave pull, keep-cache-on-error):
internal/cpchannel(server) +internal/enclave/cpsync(client), with governance policy and attestation reference values as its two consumers. It works across the CP/enclave cluster boundary, where a shared ConfigMap cannot reach a customer-cluster enclave. - Agents talk only to their local enclave, via the IMDS socket. Agents hold no controlplane credential, ever: egress toward the controlplane is terminated at the enclave gateway (total mediation, like all agent egress) and carries enclave-asserted agent identity — the gateway relays the agent’s SVID through the broker and presents the controlplane-minted 60s agent-operator token itself on the re-originated hop (bd_2_0-bvw); the token never enters the agent’s view. The agent’s own TCP to a controlplane-bound host is redirected into the gateway listener like any other egress, never delivered direct.
- controlplane and enclave are separate packages. They share the
dvxbinary for packaging convenience only; adepguardlint rule (.golangci.yml) forbids cross-imports so the boundary cannot erode. - Identity minting is a controlplane-only authority; enclaves never mint
controlplane-trusted identities. The controlplane is the only party that
mints identities the controlplane trusts (Zitadel service accounts / keys). It
mints exactly one such identity per enclave — the enclave’s broker — so
the controlplane’s per-enclave trust surface is one identity, not many. The
broker may issue only local credentials to the other enclave services (it is
a local issuer, never an issuer of controlplane-trusted identities), and those
services reach controlplane-owned data only through the broker, never with a
credential the controlplane trusts. Handing an enclave the power to mint
controlplane-trusted identities would push IdP authority across the sovereignty
boundary and multiply that trust surface — so it never happens. The
depguardfence enforces the structural half: aninternal/enclavepackage cannot import the Zitadel management client, which lives underinternal/controlplane. Seeenclave-broker.md. - An agent authenticates to trust-domain services with exactly one identity:
a single attestation-bound mTLS credential the agent never holds. There is
one auth method for an agent reaching any service inside (or explicitly
federated with) the dvx trust domain — mutual TLS presenting a SPIFFE-style
agent-identity SVID whose subject is the attested agent (UID / org /
image-digest). No second scheme (bearer tokens, signed headers as their own
trust root, per-service API keys, WireGuard peer keys) is introduced for this
purpose; alternatives were evaluated and rejected (see
bd_2_0-bwc). The credential is originated by the gateway, which runs outside the agent’s Kata VM: the agent makes a plain connection, the egress redirect sends it to the gateway, and the gateway holds the key and presents the SVID. The agent image is never reconfigured and the signing key never enters the untrusted guest — a compromised agent can only have traffic signed as itself, never forge or steal the identity. This is the inward-facing half of a two-sided rule: inside/federated → assert the agent’s attested identity; genuine third-party → inject the destination’s credential (creds.Source), because an outside party does not trust the enclave CA. Both halves keep the BYO agent holding no credentials. This invariant is the design law; it is realized by the attestation-bound credential (bd_2_0-hbk, minting on attest) and the identity-asserting tunnel (bd_2_0-bwc, the gateway wrapping agent traffic), which build onto the vsock IMDS seam and the real-attestation gate (bd_2_0-che). Containment is enforced in two layers (the same strategy as invariants 1-5). Deterministic (compiler):internal/enclave/agentidsplits the mint input (Identity, openly constructable - a signature request proves nothing until the enclave CA signs it) from the verify output (VerifiedAgentIdentity, unexported fields, produced only byagentid.Verifier), so holding a verified identity is itself proof a credential was checked, and it is a distinct type from the gateway’s enclosure attribution (registry.AgentIdentity) - the two cannot be interchanged. Judgment (drift-review skill): flags any agent-facing service that trusts a different root (bearer token, header, API key, or attribution-as-auth). - A resource is addressed on two orthogonal axes, and placement never confers
authority. Every dvx resource has a tenancy coordinate — who owns it / who
may act on it:
platform → org → group → agent— and a placement coordinate — where it physically lives:enclave(sovereign region) + namespace + resource. Authorization keys on tenancy only. The authz target ispolicy.Target{OrgID, GroupID, AgentID}(internal/policy); enclave is a placement target the operator API takes as a publish destination, never a scope that grants authority (internal/controlplane/iam/agents.go: “the authz coordinate comes from the route path, the enclave is placement”). This is load-bearing security, not taxonomy: if placement conferred authority, “move an agent into enclave X to inherit X’s power” would be an escalation. The same agent is nameable on both axes — one says where it runs, the other who it is — and they must never be conflated. dvx is not AWS: policy is scope-based, not resource-ARN-based, so a unified coordinate is wanted for audit / targeting / cross-reference, not for policy authoring. Realization is deliberately deferred (bd_2_0-ihz): today the coordinate primitives are ad-hoc per subsystem (grant principalsuser:/group::, theurn:dvx:orgtoken claim, value-crypt AAD,--enclave-idaudit/secret targeting). A singleurn:dvx:grammar (urn:dvx:enclave:…placement,urn:dvx:org:…tenancy) would subsume all of them — too large to smuggle into any one change — so it is not built yet; the enclave id was kept URN-embeddable (an opaque random token, not a human coordinate; invariant realized bybd_2_0-49z) so the scheme can be adopted later with zero rework, homed ininternal/naming.
Data at rest & key custody
Encryption at rest and key ownership follow the same sovereignty logic as everything else: key custody never crosses a boundary the data itself may not cross. A regulated customer who asks for “BYOK” almost always means “we do not trust you to hold it” — so Daevix answers the hard tiers by moving the data and the deployment into the customer’s boundary, not by holding an encrypted blob under a key we must still reach to use.
The BYOK spectrum
There are three answers, escalating with how little the customer will let the SaaS operator hold:
| Customer stance | Answer |
|---|---|
| “operator must never possess our sovereign data” | BYO-enclave — run your own enclave. Nothing sovereign ever reaches the controlplane; there is no ciphertext to compel and no key to compel. Stronger than BYOK. |
| “operator may hold our control-plane metadata, under a managed key” | Controlplane metadata encrypted at rest under the operator’s KMS (operator CMK). Honest disclosure: the operator holds that key. |
| “operator must not hold the key to anything of ours” | BYO-controlplane — self-host the whole controlplane, its operator-CMK pointing at your own KMS. The symmetric top answer; the controlplane is deployable SaaS or self-hosted (see self-hosted-controlplane.md, bd_2_0-k5t.10). |
The shape to keep in mind: BYO-key → BYO-enclave → BYO-controlplane. The two hard tiers are answered structurally (the data/deployment lives in the customer boundary), not by per-tenant key escrow on our side.
Two KMS trust domains, never crossing
- Enclave KMS — the customer/enclave-operator’s (Vault in-cluster first; their own AWS/GCP KMS later). Guards sovereign rows and the attestation signing key. Only the enclave dials it. Its binding (endpoint, auth, key ring/transit key) is local deployment config the operator sets when standing the enclave up, exactly like the DB DSN — the controlplane never learns it, never holds a credential to it, never performs a crypto op against it. This is the direct consequence of invariants 1/2/5: a controlplane that could unwrap an enclave’s DEKs would hold key-custody authority over sovereign data, which is forbidden the same way identity-minting authority is (invariant 5). If a feature seems to need controlplane→customer-KMS access, that feature belongs in the enclave, driven by policy the enclave pulls.
- Controlplane KMS — the operator’s own. Guards controlplane metadata at rest. Only the controlplane dials it. A cloud enclave KMS must be the customer’s own account/region, or key material crosses the sovereignty boundary.
Envelope encryption (both planes)
The mechanism is standard envelope encryption, ported from the prior daevix
internal/kms: a random per-value DEK encrypts the plaintext locally with
AES-256-GCM; the KEK (in the KMS) only wraps the DEK — the KMS never sees
plaintext. A KeyRef stored beside the ciphertext (provider:orgID:name:version)
is the read-routing handle, and an AAD binds each ciphertext to its logical
identity (org, agent, secret) so a stolen (ciphertext, key_ref) pair cannot be
replayed onto another row or org. Decrypt is fail-closed.
One provider backs two operations: envelope (rows) and sign (the SVID CA,
via a crypto.Signer through the agentid.SVIDIssuer seam, bd_2_0-d5n). So the
attestation signing key and the data-at-rest KEK share a provider and its
auth/config, differing only in primitive. The KMS-backed SVID CA (bd_2_0-k5t.6,
agentid.KMSMinter) self-signs its CA cert over the provider’s key and mints
every leaf through the provider — the CA private key never leaves the KMS and is
never written to disk; only the public ca.crt trust bundle is persisted. When no
KMS is configured the dev-start node-local CA holds the key on tmpfs; both plug in
behind the same seam, so agentid.Verifier is unchanged.
Per-org keys without per-org custody
Within a single enclave (or the controlplane), keys are partitioned per org by
key name (org-{id}-secrets) inside the one provider that plane owns — not
per-org providers reaching into per-org KMSs. That deliberately drops the prior
daevix central org_kms_config registry, which had the operator reaching into
each org’s KMS — precisely the controlplane→customer-KMS coupling this design
forbids. An enclave serves exactly one org (see “One enclave, one org” below), so
the per-org key name is effectively the enclave’s own key; the naming is retained
(forward-compat and free) and still buys:
- Crypto-shredding — destroy an org’s key and its rows become unreadable (GDPR erasure / clean offboarding), a concrete compliance win the operator can offer with no customer-KMS coupling.
Isolation between orgs is topological, not a shared-plane key partition: two orgs never share an enclave, so their sovereign data lives in separate clusters/KMSs — operator-proof isolation, stronger than the blast-radius framing a shared plane could offer.
One enclave, one org
Invariant (bd_2_0-j9r, decision bd_2_0-qyk): an enclave serves exactly one
org. enclave.owning_org_id is required (a NOT NULL FK to the org coordinate);
there is no shared / multi-tenant / platform-only enclave, and no runtime
served-org set the controlplane scopes snapshots to. An org may own many
enclaves (org 1→N enclaves; e.g. one per region or per sensitivity tier), but no
enclave is ever shared across orgs. The controlplane fans a per-enclave policy /
config snapshot out scoped to that enclave’s single owning org plus the platform
floor (Snapshot.ForOrg, internal/policy); the enclave needs no request-time
cross-org gate, because a foreign-org agent’s rows are simply absent from its
snapshot and default-deny downstream. This removes the shared-enclave co-tenant
adversary class entirely (docs/threat-model.md §4.6/§6). Hardware density, when
needed, comes from multi-enclave-per-node (bd_2_0-6l2), which is orthogonal
and unaffected.
The rule that keeps controlplane BYOK from breaking the boundary
Classic per-tenant BYOK on the controlplane is deliberately not built: the controlplane must read most of its metadata to function (evaluate policy, resolve identity), so a customer-only key would force it to dial the customer’s KMS on every read — a controlplane→customer-boundary dependency (breaks invariant 2) and an availability inversion (customer KMS down ⇒ their control plane down). Hence the design rule:
The controlplane may apply a customer-held key only to fields it stores and returns but never interprets. Anything it computes on stays under operator custody (or the customer self-hosts the controlplane).
The strongest lever remains data minimization: the less org-sensitive
metadata the controlplane holds authoritatively (push sensitive policy content
down to the enclave; keep only references/hashes centrally), the smaller this
whole question gets — invariant-2 thinking applied to sensitivity, not just
sovereignty. The column-by-column classification that operationalizes this — which
controlplane fields are public / operational / org-sensitive, and per sensitive
column whether to encrypt-at-rest, keep a reference only, or relocate — is in
controlplane-data-minimization.md
(bd_2_0-k5t.9); it feeds tier-2 controlplane at-rest encryption (bd_2_0-k5t.7).
Client interaction
How clients (the CLI, a web console, operator tools) talk to the controlplane vs.
directly to an enclave — the endpoint split, enclave discovery, control-intent
delivery, the direct sovereign-data path, and enclave identity — has one governing
model: client-interaction.md. In one line: metadata and
control go to the controlplane and reach an enclave only by the enclave pulling +
reconciling; sovereign-data operations go client→enclave directly and never
through the controlplane.
Two log streams
Operational logging and audit logging are separate first-class streams, not
two uses of one logger. Application logs go to stdout; audit logs are a
store, not a stdout line — when an audit store sink is registered
(audit.AddSink) the record goes to the store instead of stdout. Both are told
apart by the top-level stream key:
| application log | audit log | |
|---|---|---|
| package | internal/applog | internal/audit |
| sink | stdout | ClickHouse audit store (internal/auditstore); stdout only when no store is wired |
| top-level key | stream=app | stream=audit (+ audit_schema) |
| purpose | diagnostics for operators | durable security-relevant record |
| may be dropped/reworded | yes | no |
Audit is stored in ClickHouse, per plane: each enclave has its own sovereign
audit store (agent signals — network flows, etc.), and the controlplane has
its own platform-audit store (operator actions; no sovereign data). The two never
share a store (invariants 1/2). A component with no --audit-dsn configured falls
back to stdout. Split streams downstream with one field match, e.g.
jq 'select(.stream=="audit")'.
Record classes are classified into evidence tiers (authority /
completion / telemetry), each with a named durability behavior at the store’s
WAL bound — the classification bd_2_0-4p8.3’s capture-or-deny extension scopes
against. See audit-evidence-tiers.md.
In an enclave the audit stream is the primary evidence that sovereign-data operations stayed within the enclave. Treat it as an API, not convenience logging.
Durability. The store buffers writes through a per-table batcher whose
back-pressure drops rather than blocks — audit must never stall a component’s
hot path. By default the buffer is in-memory (lost on a ClickHouse outage or a
restart). Setting --audit-dsn’s companion --audit-wal-dir upgrades it to a
crash-safe on-disk WAL (bd_2_0-8kl.10): each batch is fsync’d + atomically
renamed before flushing, removed only once it lands in ClickHouse, and replayed
on start, so a ClickHouse outage or a component restart loses nothing. The
spool is bounded (MaxWALBytes, default 512 MiB); past the bound it drops the
newest batch and raises producer back-pressure (Store.Stats().WALDepth).
Why ClickHouse, hand-written. The audit/telemetry store speaks the
clickhouse-go driver directly rather than through sqlc, because sqlc has no
ClickHouse codegen (sqlc-dev#1628);
the rest of the tree is sqlc+goose over Postgres. This split is deliberate and
fenced (see Preventing drift): internal/auditstore is the one-stop-shop home
for ClickHouse access, and a depguard rule confines the raw driver to it so it
cannot leak into Postgres-backed code.
Preventing drift
Cross-cutting concerns each have exactly one home package, so a change happens in one place. We keep it that way with two complementary layers:
Deterministic (CI, every commit) - .golangci.yml:
depguardenforces the controlplane <-> enclave import boundary, keepsinternal/kmsboundary-safe (it may import neither plane, so both can use it), and confines the rawclickhouse-godriver tointernal/auditstore(the ClickHouse-vs-sqlc split,bd_2_0-8kl.8).forbidigofences each raw primitive to its owning package: build a logger anywhere butinternal/applog/internal/audit, write a JSON response outsideinternal/httpx/internal/problem, stand up an HTTP server outsideinternal/server, or encrypt at rest with raw AES-GCM (or construct a KMS backend directly) outsideinternal/kms, andmake lintfails.make drift-duplis an advisory (non-blocking) token-level clone scan.
The one-stop-shop packages: internal/server, internal/httpx,
internal/problem, internal/applog, internal/audit, internal/auditstore
(ClickHouse access), internal/obs, internal/authn, internal/kms,
internal/config.
Judgment (periodic / on review) - the drift-review skill
(.claude/skills/drift-review). It finds what fences cannot: a new
cross-cutting concern with no home yet, and semantic duplication that clone
detection misses. Every finding it confirms proposes the forbidigo/depguard
fence to add, so that class of drift becomes deterministic from then on. The
skill is the discovery mechanism; the fences are the ratchet.
A third surface has its own uniformity contract: every component’s HTTP API
follows one shape — /api/v1/* paths, apispec.Route tables that drive both the
mux and the OpenAPI spec, RFC 9457 errors, bearer + path-derived capability, and
a self-describing origin. See http-api-conventions.md:
this is what a daevix HTTP API looks like; do not diverge from it.
The threat-model lens (change discipline)
The same judgment philosophy applies to a second kind of drift: the gap between
the code and its threat model (threat-model.md). That
document is a Daevix-wide, boundary-by-boundary security model, and it is only
useful if it stays true - a fail-closed default that quietly goes fail-open, a
new trust dependency nobody wrote down, or a “designed-but-unbuilt” note that is
now stale because the mechanism shipped, all silently invalidate it.
So it is a standing discipline - not just a doc to read - that every security-relevant change is viewed through one question: how does this affect the threat model? The rule:
A change that materially affects the threat model must (a) be recognized as such, (b) get a careful review or an alternative approach when the effect is negative (it weakens a boundary), and (c) leave
threat-model.mdstill true - reconciled in the same change, or with a bead filed for any gap it knowingly opens.
“Material” means it alters a trust boundary (§4.1-4.7 of the doc), an invariant (1-6 above; agent-identity invariants 1-6), or one of the doc’s §5 assumptions / §6 out-of-scope items - typically by weakening a fail-closed default, expanding the TCB / adding a trust dependency, crossing a sovereignty invariant (data, identity-minting, or dial direction), introducing a standing agent secret, adding a second agent-auth trust root, or opening a new reachable surface. A behavior- preserving refactor in a security package is not material.
This is the security sibling of drift-review, with the same two layers:
- Judgment - the
threat-model-reviewskill (.claude/skills/threat-model-review) applies the lens to a diff / PR or a proposed design: it locates the affected boundary, computes the concrete threat delta, flags negative deltas for careful review or an alternative, and says exactly how the doc must be reconciled. - The ratchet - here the ratchet is not a lint fence but the doc itself: a
material change that lands without reconciling
threat-model.md(or filing a bead for a known gap, asbd_2_0-a9ewas filed for the last-admin race) is incomplete. The model is kept honest by the same adversarial-verification discipline that built it - a mitigation is real only if the code enforces it.