Don't trust us. Verify us.
Sepulchre is a credential broker for the secrets that pass between people. The operator running it cannot read what flows through. This script proves it against the live system.
Why “Sepulchre”? A sepulchre is a sealed tomb. What's placed inside stays there, and whoever keeps the tomb doesn't get to open it.
Ask for a secret. Hand one over, once.
- 01Operator opens an intake
Name the credential, list the fields, pick the recipient.
- 02One-time link, emailed
A scoped, single-use token. Nothing else can redeem it.
- 03Submitted straight to Vault
The values land at the path the operator is denied.
- 04Operator sees “fulfilled”Deny
Status and timestamps. Never the values.
- 05A machine reads it, audited
A service account uses the one gated endpoint.
- 01Operator enters a secret
Typed once. Up to five views and a hard expiry.
- 02Written and wrapped in Vault
Postgres counts views against an opaque accessor.
- 03Recipient gets a reveal link
The accessor is the capability, and it does nothing without Vault.
- 04Revealed, then destroyed
The last allowed view deletes the underlying secret.
- 05Second visit trips the wireAlert
They get a generic error. You get an interception alert.
What each side actually sees.
Screenshots from a local deployment with invented demo data: the recipient's pages, then the operator's console for the same requests.
The recipient
The operator
Three layers, each one blind.
Select a component to see what it holds and what it can never hold. The full architecture →
script-src 'none': not one byte of JavaScript- Native HTML validation replaces client code
- Every stylesheet is forced external, and the rendered bytes are checked
connect-src 'self': it only talks to its own origin- The secret is fetched on an explicit click and held only in the page
- A refresh loads a page with no secret in it
- Shows intake status, from pending to fulfilled
- Signs in with OIDC single sign-on; auditors get a read-only role
- Can't display a value it could never read
- It reads as the caller's token, so Vault is the authority
- It emits a typed audit event for every operation
- Even fully compromised, it still can't read a submission
- Operator policy: an explicit deny on inbound data
- Bound service accounts are the only read path
- Per-tenant Transit keys for BYOK; revoke one and that tenant's data goes dark
- No secret values, Vault tokens or wrap tokens
- Only argon2id hashes and opaque accessors
- Stolen in full, it yields zero credentials
- Each event is hash-chained to the last and signed with a per-tenant key
- Copied to WORM object storage and to your SIEM
audit:verifyfails on any edit, deletion or forgery
Machines get credentials. People approve agents.
A submitted credential usually exists for a machine to use. Sepulchre gives applications and pipelines a read path that needs no stored secret. AI agents use the same path, but a person has to approve each request.
- 01Bind the credential to a reader
The operator names which workloads may read an intake, such as a deploy job or a billing service. Nobody else can.
- 02The job proves who it isNo stored secret
A CI job exchanges its own OIDC token for a short-lived Vault login. There is no secret stored to fetch the secret.
- 03Read with its own token
The broker reads with the caller's token, so Vault enforces the binding, not the broker.
- 04Every read on the record
Each fetch is audited with the reader's identity and OIDC provenance, and shows up on the console's Runtime page.
# GitHub / Gitea Actions
permissions:
id-token: write
steps:
- uses: ./actions/get-secret
with:
intake: ${{ vars.SEPULCHRE_INTAKE_ID }}
role: sepulchre-reader-t_acme-deploy
# plus broker-url and vault-addr
- 01The agent asks
get_credentialis refused without approval, so the agent callsrequest_credentialwith a reason. - 02A human decidesHuman approval
The request appears in the console's Approvals queue. An operator approves or denies it.
- 03A time-limited window
Approval opens one intake to that one agent for a set time: 60 minutes by default, 24 hours at most.
- 04The value bypasses the modelNever in context
The credential is written to a file on disk for the agent's runtime. The tool returns only a masked confirmation, so the value never enters the model's context.
get_credential(intake) → approval_required
request_credential(intake, reason)
check_approval(request) → approved · 60 min
get_credential(intake) → written to sink file
Also available as a CLI with certificate pinning and as SDKs for TypeScript, Python and Go. Readers authenticate with OIDC, AppRole or a Vault token.
The guarantee came first. Then everything else.
The two flows were built first, because the guarantee had to be right before anything else. This is what has been built on top of them since.
Operators sign in through your OIDC provider, which also enforces MFA. Auditors get a role that can read the audit trail and nothing else.
Each tenant gets its own envelope encryption. Revoke the tenant's key and its credentials become unreadable to everyone, operator included.
Events are hash-chained, signed per tenant, and copied to WORM storage and your SIEM. One command walks the chain.
Optionally, the recipient's browser encrypts values before submitting them, so the broker never handles plaintext at all.
A three-node Raft cluster with Transit auto-unseal, plus deployment modules for Helm, Terraform (AWS, GCP, Hetzner) and Ansible.
Emergency access needs a second approver and raises an alert. Contractor access revokes itself when it expires. Cross-tenant sharing needs both tenants' policies to agree.
Also built: tenant isolation proven in CI, a SOC 2 evidence pack, and Slack notifications.
Questions your auditor will ask.
Answered with mechanisms, not assurances.
01Can your operators read our credentials?
No. Operator tokens carry an explicit Vault deny on inbound data, and Vault evaluates deny before allow. Run tools/verify-policy.sh against the live system to confirm it yourself.
02What if the broker is compromised?
An attacker could mint links or create bogus shares. They still can't read submissions, because the broker never held that capability. It reads only with the calling service's token.
03What if the database leaks?
The attacker gets a list of who asked whom for what, and when. There are no secret values, Vault tokens or wrap tokens in it, only argon2id hashes and opaque accessors.
04How would we know a link was intercepted?
Reveal links are single-use by design. Any attempt after the allowed views are spent returns a generic error and writes a flagged interception event to the audit log.
05Can you prove who read what?
Every operation emits an event with a typed actor: human, recipient, service or system. Events are hash-chained, signed and copied to WORM storage, so the record can't be quietly edited. For a clean tenant, the list of human reads of inbound data is empty, and audit:verify shows it hasn't been tampered with.
06Can an AI agent get at our credentials?
Only through a human. An agent is a reader like any other, but each read needs an operator's approval, which opens one intake for a limited time. The value is written to a file for the agent's runtime rather than returned into the conversation, and every request, approval and read is audited.
07What if we don't trust whoever hosts Vault?
Use BYOK. Each tenant's values are encrypted under its own key before they are stored, and revoking that key makes them unreadable to everyone, the operator included.
Honest about scope.
- This is zero-knowledge by policy, not the cryptographic protocol. No operator-scoped identity holds a Vault capability to read inbound data. A Vault root token can read anything, which is why the policy is published and why BYOK exists.
- A designated service account can read credentials. That's the point, since a machine needs them. Reads are bound to specific readers, short-lived, and audited every time.
- It has had an internal security review, not an independent audit. The threat model goes adversary by adversary.
- Leased and dynamic secrets are designed, not built. Today's credentials are standing and are rotated by re-submission. See credential lifecycle.
Trust that doesn't have to be extended.
We're onboarding a small group of security teams. Sepulchre is Apache 2.0 and runs on your own infrastructure. Email us and we'll set you up to run the proof against your own Vault.




