Verify it yourself

If a security claim can't be checked, it's marketing. This page shows the exact policy Sepulchre writes to Vault for the operator, and the two checks that prove a running Vault enforces it.

The published policy

This is operator.hcl.tmpl, included here directly from the repository when the site is built. Sepulchre renders it for each tenant, replacing {{TENANT}} with the tenant id, and writes it to Vault as operator-<tenant>.

# operator.hcl.tmpl — the PUBLISHED zero-knowledge proof (N-011, F-025), per tenant.
# Rendered per tenant by replacing {{TENANT}} → the tenant id, then written to Vault as the
# policy `operator-<tenant>`. tools/verify-policy.sh renders this for a tenant and diffs it
# against the live policy, and proves an operator-scoped token is denied on inbound.
#
# The explicit `deny` on the tenant's inbound data is the core guarantee: the operator cannot
# read submitted credentials. Vault evaluates deny before allow; no stacking overrides it. The
# policy only ever references THIS tenant's paths, so an operator of one tenant has no grant
# on another tenant's paths (#46 isolation).

# ── Inbound: operator can see metadata, NEVER the secret data ──
path "sepulchre/data/{{TENANT}}/inbound/*" {
  capabilities = ["deny"]
}
path "sepulchre/metadata/{{TENANT}}/inbound/*" {
  capabilities = ["list", "read"]
}

# ── Outbound: operator manages this tenant's shares ──
path "sepulchre/data/{{TENANT}}/shares/*" {
  capabilities = ["create", "update", "delete"]
}
path "sepulchre/metadata/{{TENANT}}/shares/*" {
  capabilities = ["list", "read", "delete"]
}

# ── Response wrapping for one-time shares ──
path "sys/wrapping/wrap" {
  capabilities = ["update"]
}
path "sys/wrapping/lookup" {
  capabilities = ["update"]
}

Two properties of Vault make this hold:

  1. The deny is explicit, not just absent. The operator can list inbound metadata, so the console can show "fulfilled" or "pending". The data path carries an explicit deny.
  2. Vault evaluates deny before allow. No stacking of policies, token wrapping or combining of capabilities overrides an explicit deny. A policy added later that wrongly granted read on the same path would still lose.

The two checks

A published policy is worthless if the running Vault enforces something looser. tools/verify-policy.sh closes that gap with two independent checks:

  1. Policy diff. It reads the live operator-<tenant> policy from Vault, normalizes it, and diffs it against the rendered template. Any drift makes it exit 1 and print the diff.
  2. Behavioural check. It creates a real operator-scoped token and tries to read an inbound secret. The guarantee is proven by the read being denied, not by reading the policy text.

These steps run from a checkout of the repository, which is not public yet. Ask for access and you can run them against your own Vault.

pnpm install && cp .env.example .env    # set ADMIN_PASSWORD and JWT_SIGNING_KEY
pnpm infra:up                           # Vault, Postgres, PgBouncer, Mailpit, Traefik
pnpm vault:provision                    # audit device, mounts, AppRole, per-tenant policies
bash tools/verify-policy.sh
==> [t_default] comparing live 'operator-t_default' policy to the rendered published template
OK: live policy matches the published artifact (tenant t_default)
==> [t_default] asserting an operator-scoped token is DENIED reading inbound data
OK: operator-t_default denied on sepulchre/data/t_default/inbound/* (zero-knowledge holds)
==> zero-knowledge property verified for tenant 't_default'.

Watch it fail

You shouldn't trust a check you have only ever seen pass. Give the operator read on the inbound path and run the script again:

docker compose -f deploy/docker-compose/docker-compose.dev.yml exec -T \
  -e VAULT_TOKEN=sepulchre-dev-root vault \
  sh -c 'printf "path \"sepulchre/data/t_default/inbound/*\" { capabilities = [\"read\"] }\n" > /tmp/bad.hcl
         vault policy write operator-t_default /tmp/bad.hcl'

bash tools/verify-policy.sh   # → FAIL + diff, exit 1

Re-run pnpm vault:provision to restore the published policy.

On every change, not once

The zero-knowledge-proof CI job does all of this on every push and pull request. It stands up a fresh Vault, provisions it from the published policies, runs the proof, and then loosens the live policy on purpose to confirm the check still catches it. Alongside it, secret-leak-scan fails the build if any code path could carry a secret to a log, and tenant-isolation provisions two tenants and proves neither can reach the other's data, both in Postgres (row-level security) and in Vault.

Every acceptance criterion, in one command

With the stack running, pnpm verify:poc runs the whole lifecycle against the live API and prints a pass or fail for each criterion, without ever printing a secret. The lifecycle covers an intake request, a submission, metadata the operator can see but not read, a service-account read, a one-time reveal, audit attribution, a match against the published policy, and a stack health check.

POC acceptance: 7/7 criteria passed.
ALL POC ACCEPTANCE CRITERIA PASS

The full write-up, including where this fits in the threat model, is in Zero-knowledge proof.