Secrets
Records hold handles. Backends hold values. Syncropel enforces the separation at four independent layers — a deliberate "no plaintext secrets in records, ever" invariant baked into the protocol.
Overview
Every workspace eventually needs credentials. API keys for Anthropic and OpenAI. Database passwords. Webhook signing secrets. OAuth tokens. Syncropel has to coordinate work that depends on these values without ever turning records into a credential leak surface.
Syncropel resolves this with a clean separation: records hold handles, backends hold values. A record can name a credential — "the Anthropic prod key" — without ever embedding its bytes. The actual value lives in your operating system's keychain, in a vault you operate, or in a backend you wire in. When code needs the value, it asks Syncropel for the handle, Syncropel asks the backend, and the value flows directly back to the caller without ever crossing the record graph.
This is a structural invariant, not a convention. A record body, at any hash level, must never contain a plaintext secret value. Syncropel enforces this rule at four independent layers — a bypass of any one layer leaves the remaining three load-bearing.
Why this matters
Conventions leak under pressure. "Don't put secrets in records" is the kind of rule everyone agrees with at design time and breaks when a feature ships under deadline. Forty years of credential-leak post-mortems say the same thing: the only credential that doesn't leak is the one the system structurally refused to accept in the first place.
This rule makes the refusal structural. The record log is one of the most-read surfaces in the system — projections render records to UIs, federation replicates them across instances, fold rules read every body field — so every credential that lands in a body multiplies its leak surface across every reader. Removing values from records entirely collapses that surface to zero.
The four enforcement layers
Defense in depth is the design. Each layer catches a different class of leak that the others miss.
Layer 1 — Body-kind validator at HTTP ingest
When a record arrives at the instance's POST /v1/records endpoint, the body-kind validator inspects the declared body.kind against a registry of known shapes. Each kind can declare per-field sensitivity: Public (default), Sensitive (rejected at ingest with F16_VIOLATION_SENSITIVE_FIELD), or CiphertextPermitted (must be a wrapped envelope with an unwrap-key reference).
Today, no production kind declares a Sensitive field — the credential descriptor (core.credential.v1) and the audit envelopes (secret.access.v1, secret.rotation.v1, secret.value_invalidated.v1) are structurally handle-only. They reference values by handle and backend URI; they never embed bytes. The validator infrastructure ships so future kinds can declare and have it enforced without a code change at the call site.
Layer 2 — Secret<T> Rust type
The Secret<T> wrapper around any value type makes a Rust-level mistake impossible. Secret<T> refuses serde::Serialize (a compile error if you try), produces <redacted> for Debug and Display, and forces every read of the inner value through an explicit .expose_secret() call that grep can enumerate. Combined with compile-fail doctests, the type system blocks you from ever serializing a secret value into a record body.
This layer cannot be tested at runtime — it's a compile-time barrier. Either the build accepts a code path that emits a Secret<T> through serde, or it doesn't. The secret test module includes compile-fail doctests asserting the contract.
Layer 3 — Tracing redaction
Operators paste raw API keys into env vars, ad-hoc commands, error messages from third-party libraries, and HTTP error bodies — none of which the type system can intercept. The tracing layer scans every log line at the writer boundary and replaces credential-shaped substrings with <redacted:N-chars> before the bytes hit disk. Ten patterns ship today: Anthropic (sk-ant-...), OpenAI (sk-proj-..., generic sk-...), GitHub (gho_/ghp_/ghu_/ghs_), JWTs, Bearer auth, and AWS access keys.
The bias is toward false positives — a slightly mangled log line beats a leaked credential. Real-world test: paste an API key into an instance command's stderr, and the log file shows <redacted:48-chars>, not the key.
Layer 4 — Projection-time redaction
Records that legitimately carry sensitive fields (operator-only diagnostics, scoped traces) need a different mechanism: not "reject at emit", but "mask at read." The projection layer redacts per-field declared values at every read surface — GET /v1/records/:id, GET /v1/records, POST /v1/records/query, the MCP read_thread tool, and federation outbound — when the projecting actor's capability scope set lacks the required scope.
Today only the test.redaction.v1 fixture declares a redactable field. The infrastructure ships so future kinds can declare an operator-only diagnostic field and have it masked everywhere a non-admin actor reads it, without surfacing-by-surfacing audit work.
core.credential.v1 — the credential primitive
When you tell Syncropel about a credential, you emit a core.credential.v1 record:
{
"kind": "core.credential.v1",
"handle": "anthropic-prod",
"backend_ref": "keychain://syncropel/anthropic-prod",
"publisher_did": "did:sync:user:alice",
"lifecycle": "active"
}That's the entire record. No value field. No anywhere to put a value. The handle is the human-readable name, backend_ref is the URI pointing at where the value lives, publisher_did records who created the descriptor, and lifecycle tracks active, rotated, invalidated, or erased.
When code needs the value, it asks Syncropel by handle. Syncropel looks up the descriptor, reads the backend_ref, asks the backend, returns the value to the caller, and emits a secret.access.v1 audit record before the operation response returns. The value never touches the record graph.
The spl secret CLI
Operator-facing surface for managing credentials:
spl secret set anthropic-prod # reads value from a TTY (echo disabled) or stdin
spl secret get anthropic-prod # returns metadata (no value)
spl secret get anthropic-prod --reveal # returns value to stderr with a warning
spl secret list # all known handles
spl secret promote ANTHROPIC_API_KEY # graduates an env var into the OS keychain
spl secret delete anthropic-prod # writes a tombstone descriptor + audit recordTwo design rules apply to every command:
- Values are never CLI arguments.
setreads from a TTY (echo disabled viatcsetattr) or from piped stdin. There's no--value <plaintext>flag. Process-table snooping cannot leak. - Every operation emits a
secret.access.v1audit record before the operation response returns. The audit lands in the record log first; only then does the caller see success. This is the record-before-result invariant — auditability is structural, not an afterthought.
Where values live
The current release ships four backends in-binary:
- macOS Keychain — service
syncropel, account = handle. - Linux Secret Service — D-Bus / libsecret, schema attribute
org.syncropel.spl.credential. - Windows DPAPI — Credential Manager, target prefix
Syncropel:<handle>. env://VAR— read-only env-var resolver. Compiles + tests on every platform, useful for CI and containers.
Each adapter speaks the SecretBackend trait directly inside the instance. There is no subprocess, no external loading, no signing-chain enforcement yet — those ship in a future release along with the full adapter protocol (sandboxed subprocess, manifest-declared syscalls, Syncropic-DID-signed adapters for 1Password, Doppler, KMS, Vault). The current release is the in-binary subset.
What's audited
Three body kinds register the audit family:
secret.access.v1— DO record onth_secret_audit, emitted on everyset/get/list/delete/promote. Body fields:credential_ref(the descriptor's record id),actor_did,purpose,outcome(allowed | denied | backend_unavailable), plus a supplemental envelope withcredential_handle,workspace_did,operation,result, and the workload-identity token'sjti.secret.rotation.v1— LEARN record, emitted when a credential's backing value is replaced with a fresh one (key rolled at the backend).secret.value_invalidated.v1— LEARN record, emitted when the backing value is wiped without replacement.
Auditability is built in: the audit thread th_secret_audit is just another thread, content-addressed records, federation-replicable, queryable through the same POST /v1/records/query filter grammar that every other thread uses. SOC2 and HIPAA evidence-of-access patterns work without a separate audit database.
What's deferred
This release is the foundation. Future cycles extend the design with:
- External adapter loading —
core.secret_backend_registry.v1records pin third-party adapters with Syncropic-DID signing. - OS-level subprocess sandboxing —
sandbox-execon macOS,unshare+ cgroups on Linux, AppContainer on Windows. - Capability-scoped tokens — single-use 5-second-lifetime tokens per adapter invocation.
- Manifest-declared syscall enforcement — adapter declares its syscall set; Syncropel enforces.
- Federation integration of workload-identity tokens — instance A presents a short-TTL workload-identity token to instance B before invoking an adapter on the originating instance.
- Cascade invalidation —
spl secret invalidate-all-from-backend <backend>propagates across federated workspaces.
Cryptographic erasure (delete the wrap key in the backend → ciphertext blobs in records become opaque) is on the post-MLS roadmap. Identity recovery primitives (Shamir social recovery, escrowed paper key) follow alongside.
Acceptance criteria
The four-layer enforcement carries a dedicated acceptance gate. Each scenario disables one layer and verifies the remaining three still independently catch the leak. Green here is the contract: a regression on any single layer leaves the other three load-bearing, and the acceptance gate flags it before any release tag.
Related
- Records — the primitive that holds credential handles + audit metadata
- Governance — consent records, scope grants, capability discovery
- Federation — how audit records replicate across instances
Governance
Permissions live in the protocol, not bolted on. Every act produces a record. Without a matching allow, the answer is no.
The Engine
Four loops at four timescales — from sub-millisecond record ingest through minute-level drift detection to daily maintenance. This is the engine that turns records into action, action into evidence, and evidence into learning.