seekrit
Docs/Export & break-glass recovery

Export & break-glass recovery

Zero-knowledge means seekrit cannot read your secrets. It should also mean you don't need us to. seekrit archive exports your whole organization as one signed file, and opens that file offline — with your passphrase, a service token, or a quorum of recovery custodians. No network, no account, no seekrit.

Use it for a durable backup, for a compliance or due-diligence snapshot, or to prove to yourself (or a security reviewer) that leaving is a file copy rather than a migration project.

What's in an archive

One JSON file, seekrit-archive/v1, holding every row we store for the organization:

  • Structure — applications, groups, environments (including branches and their parents), and which groups each environment composes.
  • Secrets — every current ciphertext, and every historical version, exactly as stored.
  • Key grants — each environment's data key wrapped to each principal (wd1. blobs), for every principal, plus managed-key (KMS) grants and the wrapped recovery shares.
  • People and credentials — members and roles, service-token and machine-client metadata, outstanding invites.
  • Operations — rotation policies, sync connections and bindings, lease targets, and the append-only audit log.
  • Agent access policy — every agent identity and the full version history of its published, signed policy bundles, so what an agent was allowed to reach stays verifiable offline, for past versions as well as the current one.
  • Your own key — the requesting user's passphrase-encrypted private key (pk1.), so the archive plus your passphrase is self-sufficient.

Ciphertext stays ciphertext: exporting decrypts nothing, and the archive is exactly as unreadable to us as the database it came from.

note

An archive carries your wrapped private key and nobody else's. Every member's key stays where it is; each person exports their own. That's why the manifest records who asked for it.

Three columns are deliberately left out, because including them would either hand you an offline cracking target or an undecryptable blob: service-token and session hashes (authentication verifiers, useless to you), the admin credentials on lease targets, and the destination API credentials on sync connections. The last two are encrypted to a server-side key that no key of yours can open — the row is in the archive, the credential column isn't.

A few whole tables are out too, and for one of them the reason is worth knowing: honey tokens are never in an archive. A decoy works because nobody knows where it's planted, and an archive is a file that travels — so the one place a decoy's placement must not appear is a portable copy of your organization. Every trip is still there, in the audit log. Also omitted: expired lease records, sync run history, in-flight recovery ceremonies, the log-sink credential, and billing rows — each either duplicated by the audit log or not your data. The full list, with reasoning, is in the design notes shipped with the code.

Create one

Admins only, from the CLI:

seekrit archive create --out acme-2026-08-19.json

It prints a row count per section, verifies the file it just wrote, and names the signing key. Options: --no-versions and --no-audit to shrink it, --audit-limit <n> to cap audit history (the default keeps the newest 25,000 rows). Or straight from the API:

curl -X POST https://api.seekrit.dev/v1/orgs/$ORG/export \
  -H "authorization: Bearer $SEEKRIT_TOKEN" \
  -H "content-type: application/json" -d '{}' \
  -o acme-2026-08-19.json

Every export writes an org.exported audit row — who, when, how many rows, and the archive's digest — so an export is as visible as any other privileged action. Export is never gated on your plan; the ability to leave isn't a paid feature.

Verify it

The manifest carries a SHA-256 digest per section, and the whole manifest is signed with Ed25519. So an archive is tamper-evident years later, offline:

seekrit archive info    acme-2026-08-19.json   # summary + verification
seekrit archive verify  acme-2026-08-19.json   # exits non-zero if anything fails

verify reports which section broke rather than just "bad file". To check provenance as well as integrity, pin the signer — the public key is published unauthenticated, so you can fetch it once and keep it:

curl -s https://api.seekrit.dev/.well-known/seekrit-export-signing-key
# → { "algorithm": "ed25519", "publicKey": "…", "keyId": "1ab3bd8b98170773" }

seekrit archive verify acme-2026-08-19.json --key-id 1ab3bd8b98170773

Open it offline

Two ways in, both with no network.

With the CLI, which needs no credentials and never builds a client for these subcommands:

seekrit archive decrypt acme-2026-08-19.json --out ./plaintext
# ./plaintext/apps/web/production.env
# ./plaintext/groups/shared/staging.env

You'll be prompted for your passphrase (or set SEEKRIT_PASSPHRASE). Other key sources: --token skt_… for a service token, --key-file for a raw private-key JWK. Add --format json|shell, --env web/production to narrow it, or --stdout to print instead of writing files. Environments your key holds no grant on are skipped, not failed — an archive spans the whole organization, and no single principal is expected to open all of it.

With no install at all, using the standalone decryptor: a single HTML file you keep alongside your archives.

seekrit archive decryptor --out seekrit-decrypt.html

Open it in any browser — including on an air-gapped machine, straight from file:// — drop the archive in, enter your passphrase, and read your secrets. It verifies the digests and signature first, and shows values masked until you reveal them.

tip

The page declares Content-Security-Policy: default-src 'none'. Your browser will refuse every outbound request it could make, so the page that sees your passphrase and your plaintext cannot send them anywhere. It's one line at the top of the file, and you can read it before you trust it.

Break glass: opening an archive nobody holds the key to

The hard case isn't losing the file, it's losing the person. If the only key-holder for an environment is gone, their passphrase is gone with them — and we never had it.

That's what customer-controlled recovery is for, and an archive carries everything the ceremony needs: each environment's data key wrapped to the org recovery key, plus every custodian's wrapped Shamir share. So the ceremony also runs offline, from the archive alone.

Each custodian unwraps their own share from their own archive:

seekrit archive share acme-2026-08-19.json --out share-ana.json
# (or --token skt_… for a service-token custodian)

Then any threshold of shares reconstructs the recovery key and opens everything:

seekrit archive decrypt acme-2026-08-19.json \
  --share share-ana.json --share share-bo.json \
  --out ./recovered

Below the threshold it fails outright rather than producing plausible garbage. The standalone HTML decryptor accepts pasted shares too, under custodian shares (recovery quorum).

caution

An unwrapped share is sensitive: a threshold of them is the recovery key, and the recovery key opens every environment. Move them as you would the secrets themselves, and delete them when the ceremony is done.

Notes and limits

  • It's an exit and audit artifact, not a restore point. There is no import: re-establishing access to a live organization means re-granting keys, which is the recovery ceremony above, not a file upload.
  • Values are as stored. A ${REFERENCE} in a secret is expanded when an app reads it (see references); an archive shows the literal stored value.
  • Truncation is always declared. If an audit cap trims history, the section says so and verify repeats it. A quiet trim would make an incomplete archive look complete.
  • Two exports of unchanged data are byte-identical, so you can diff yesterday's archive against today's.
  • Keep a decryptor with your archives. seekrit archive decryptor is the half that has no dependencies at all; an archive plus that file plus your passphrase is the whole recovery path.