SSyncropel Docs

Secrets

Records hold handles. Backends hold values. The substrate 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. The substrate 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 the substrate for the handle, the substrate 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. Frozen Foundation F16 says a record body, at any hash level, must not contain a plaintext secret value. The substrate enforces F16 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.

F16 makes the refusal structural. The record substrate 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 the substrate 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 daemon'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 kernel type

The kernel's 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 the kernel's 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 kernel's 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 a daemon 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 substrate primitive

When you tell the substrate 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 the substrate by handle. The substrate 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 record

Two design rules apply to every command:

  1. Values are never CLI arguments. set reads from a TTY (echo disabled via tcsetattr) or from piped stdin. There's no --value <plaintext> flag. Process-table snooping cannot leak.
  2. Every operation emits a secret.access.v1 audit record before the operation response returns. The audit lands in the substrate first; only then does the caller see success. This is the record-before-result invariant — auditability is structural, not aspirational.

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 daemon. 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 on th_secret_audit, emitted on every set/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 with credential_handle, workspace_did, operation, result, and the workload-identity token's jti.
  • 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 substrate-native: 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 loadingcore.secret_backend_registry.v1 records pin third-party adapters with Syncropic-DID signing.
  • OS-level subprocess sandboxingsandbox-exec on 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; the substrate 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 invalidationspl 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.

  • Records — the substrate primitive that holds credential handles + audit metadata
  • Governance — consent records, scope grants, capability discovery
  • Federation — how audit records replicate across instances

On this page