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:
- The
denyis explicit, not just absent. The operator can list inbound metadata, so the console can show "fulfilled" or "pending". The data path carries an explicitdeny. - Vault evaluates
denybeforeallow. No stacking of policies, token wrapping or combining of capabilities overrides an explicitdeny. A policy added later that wrongly grantedreadon 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:
- 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. - 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.