Skip to content

Privilege separation stays here; the gate to upstream is secretspec.provider/1 (PR #362) #2

Description

@djbclark

The division of labour, settled 2026-08-18

This repo keeps privilege separation only: vault files owned by a dedicated
service user, unreadable and unwritable by the calling user, mediated through
sudo. The sqlite provider — including its opt-in history feature — goes
upstream via djbclark/secretspec-sqlite, not here.

Privilege separation is not a provider concern, and this repo's own code
already says so:

Confidentiality comes entirely from filesystem permissions on the database
file — the schema and connection carry no credential of their own. That is
what lets identical code serve both an ordinary user-owned store and a
privilege-boundary vault file guarded by a 0600 service-user-owned path:
the privilege model stays external to the provider.
secretspec/src/provider/sqlite.rs:7-11

The Provider trait is get/set/delete/reflect/preflight. It has no notion of
caller UID or daemon lifecycle, and adding one would force every provider to
implement a security model it does not need.

The gate between the two halves: upstream PR cachix#362

This was already worked out on 2026-08-16 and then went untracked — hence this
issue.
See docs/design/upstream-ipc-v1-and-the-fork.md.

cachix/secretspec PR #362
("SecretSpec IPC v1 for 0.20+", authored by the maintainer) defines two
protocols over one framed JSON-RPC layer:

boundary protocol
app / SDK → secretspec broker secretspec.client/1
secretspec → external provider endpoint secretspec.provider/1

secretspec.provider/1 is the gate: this fork becomes an out-of-tree
privileged provider endpoint, with no patching of upstream provider
internals.

Load-bearing distinction — upstream's "broker" is not a privilege boundary.
It is a private child process of the caller, in the caller's own trust domain.
Forwarding secret authority is a stated upstream non-goal. Upstream built the
plumbing and explicitly fenced off this fork's territory, so cachix#362 grants the
out-of-tree mechanism cachix#345 asked for.

Already engaged: a supportive comment was posted to cachix#362 on 2026-08-17
(cachix#362 (comment), text at
docs/design/pr362-comment.md), announcing intent to ship the first out-of-tree
privileged endpoint, and offering three verified gaps in upstream's
external.rs trust checks as review feedback — POSIX-mode checks blind to macOS
ACLs, std::fs::metadata following symlinks, and trust stopping at the
immediate parent. This fork's drift.rs check_ancestor_chain walks the resolved
chain to / and rejects all three.

Work items

  • Prototype the privileged secretspec.provider/1 endpoint against
    feat/ipc-v1
    in a scratch worktree. Small shim; it proves the "no
    upstream patching" claim instead of asserting it. DONE 2026-08-18
    ss-ipc-proto at a204898, provider.lifecycle passes, git diff in
    the upstream checkout is empty. See the comment below for the four
    findings.
  • Wire the endpoint's --client path to the live vault end to end. It
    is implemented, cancellation kills the privileged child, but it has never
    been run against the live boundary. This is the next action.
  • Offer the macOS sigtimedwait portability fix upstream on feat: add versioned client and provider IPC cachix/secretspec#362.
    libsecretspec-ipc/src/process_posix.c:237 makes the conformance suite
    unbuildable on Darwin; a portable sigpending + sigwait fix is ready as
    a patch file in the prototype crate.
  • Watch for a maintainer reply on feat: add versioned client and provider IPC cachix/secretspec#362.
  • Plan the 0.20 rebase: adopt secretspec-ipc types for this fork's own
    client↔broker hop, keeping sudo as the authority mechanism.
  • Position the README against the trust-domain distinction — it currently
    does not address it at all. Risk: "SecretSpec has a broker, with audit and
    reasons" will sound to the ecosystem like it already covers the local-AI-
    agent threat model.
  • Convergence watch: if upstream later specifies the deferred socket
    transport with peer credentials, it genuinely enters this fork's
    space. Counter is to be the incumbent reference privileged deployment
    shaping that spec — which argues for engaging now, not after 0.20 ships.

Superseded by the above

  • The two-database file merge (secrets.db | broker.sqlite3) — the
    "cut by owner" rationale that broke a 2–2 reviewer tie assumed the provider
    and the broker are permanently separate owners. Under the plan above the
    provider goes upstream and this repo keeps only privilege separation, so that
    boundary is not the one to design around. Do not build it without revisiting.
  • Its transactions FK follow-up, for the same reason. Note if it is ever
    revisited: SQLite cannot add a FK to an existing table and FKs cannot span
    files (both verified), so splitting the FK from the merge means rewriting the
    audit trail twice.

Still live, independent of all of the above

  • drift/doctor never verifies the manifest chain at rest.
    drift.rs:1036 verifies only audit::verify_read_only; there is no history
    equivalent, and every history::verify caller is test-only (broker.rs:1805,
    1832, 1855, all after #[cfg(test)] at broker.rs:1560). The chain is
    verified on every capture (history.rs:387), so this is a detection-at-rest
    gap, not silent rot. Worth closing: a deliberate integrity check should not be
    asymmetric between two ledgers of equal standing.

Retracted, do not re-report

"No busy_timeout anywhere in the CLI crate" is FALSE. rusqlite 0.31.0
calls sqlite3_busy_timeout(db, 5000) unconditionally on every
Connection::open (rusqlite-0.31.0/src/inner_connection.rs:121). Two
independent reviewers reported it as a P0; a fix was written, mutation-tested,
found to be a no-op, and reverted. grep finding no explicit call is not
evidence the behavior is absent when a library sets a default. See handoff
7a1c.

Related

Companion issue in the upstreaming fork: djbclark#1 — djbclark/secretspec-sqlite (upstreaming the provider)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions