Open Source · Alpha

Keep your secrets
out of reach.

Subumbra is a split-trust secret broker. Your apps talk to Subumbra; Subumbra talks to OpenAI, GitHub, npm, and more. Real API keys, SSH keys, and registry tokens never sit in plaintext on the machines you operate — and even a compromised app never sees them.

API keys SSH signing npm publish Locked by default Janus approvals
LiteLLM · Open WebUI · LibreChat · n8n · git/ssh · npm — any HTTP client. Holds only a narrow, revocable consumer token. Never the real key.
subumbra-keys stores ciphertext and wrapped DEKs only — useless without the private key, which lives in Cloudflare.
ciphertext + wrapped DEK only
The Worker checks session, consumer scope, target host, and key fingerprint — fail-closed — before anything decrypts.
private key in SubumbraVault DO
An isolated Durable Object unwraps the DEK, decrypts the key, and calls the provider. Plaintext exists in memory for about 100 ms.
~100ms memory then destroyed
Anthropic · OpenAI · GitHub (SSH) · npm registry · 10+ more. The response streams straight back to your app.
zero at-rest exposure

Tested providers

Anthropic OpenAI Groq DeepSeek Gemini Mistral Cerebras OpenRouter xAI Together

Built-in adapters

One broker. Three kinds of secrets.

The same split-trust vault, session lockdown, policies, and audit trail apply whether the secret signs an SSH challenge, publishes a package, or calls an LLM.

API keys
LLM & HTTP APIs without plaintext keys
Point LiteLLM, Open WebUI, LibreChat, n8n, or any HTTP client at the local transparent proxy. Apps hold a narrow, revocable consumer token — the real provider key is decrypted per request inside Cloudflare and discarded in roughly 100 ms.
api_base: http://subumbra-proxy:8090/t/openai_prod
Anthropic, OpenAI, Groq, DeepSeek, Gemini, Mistral, and more.
SSH keys
git push without a key on disk
Subumbra holds ed25519 SSH private keys and exposes signature-only access through a local agent socket. OpenSSH, git, scp, and rsync work unchanged — the private key itself never leaves the Cloudflare vault, and signing can be host-bound and quota-limited.
IdentityAgent ${XDG_RUNTIME_DIR}/subumbra/ssh-agent.sock
Per-session sign quotas · host fingerprint binding · optional per-sign approval.
npm tokens
Publish without a token in .npmrc
Broker npm publish and installs so your registry token never lands in .npmrc, a .env, or a CI secret. Tarballs are inspected before publish — scope checks, size caps, and secret-pattern scanning, all fail-closed.
//127.0.0.1:10199/t/npm_publish/:_authToken=${SUBUMBRA_TOKEN_NPM}
Operation allowlists (publish, query, dist-tag…) · pre-publish content scanning.

Download, configure, bootstrap, point your apps

A one-shot ./bootstrap.sh encrypts provider material and deploys enforcement to Cloudflare. After that, subumbra-keys on your side only stores ciphertext envelopes and wrapped DEKs — never usable upstream secrets at rest.

01 / Get the code
Download or clone
Grab a release tarball from GitHub Releases, or clone the repo (fork first if you plan to carry patches).
git clone https://github.com/polysemic/Subumbra.git
cd Subumbra
02 / Configure
Manifest (and optional automation)
Copy the minimal template to manifest.yaml and edit providers, routing, and consumer IDs. For CI or unattended runs, copy .env.bootstrap.example to .env.bootstrap and add Cloudflare credentials plus the same secret_ref values referenced in the manifest.
cp manifest.minimal.yaml manifest.yaml
cp .env.bootstrap.example .env.bootstrap
03 / Bootstrap
Encrypt and deploy

Run ./bootstrap.sh on the host. Think of it as the one “setup” button: it starts the bootstrap container, deploys or updates the Worker in Cloudflare, encrypts your provider keys into the local store, and tells you when to bring the stack up — without leaving a trail of plaintext secrets in the repo as part of that run.

If you use interactive mode, you answer prompts for Cloudflare credentials and each provider secret; those answers stay in process memory for the session and are not written back into the project as cleartext.

If you already copied .env.bootstrap.example to .env.bootstrap and filled it in, the same script runs headlessly, then securely shreds that file after a successful pass so it does not linger on disk.

When the script exits cleanly, your .env contains the consumer tokens the stack expects, encrypted material is on the volume, and the core containers can start normally.

./bootstrap.sh
04 / Integrate
Transparent proxy URL + consumer token

Update your apps wherever they read provider settings — .env files, YAML or JSON config, admin UIs, or secrets in your orchestrator. Where you used to paste the real vendor key and call the provider directly, aim traffic at Subumbra and put the narrow consumer token in the credential field instead. The key_id from your manifest still goes in the URL path right after /t/ (below, anthropic_prod is only an example).

Docker apps using the subumbra network
api_base: http://subumbra-proxy:8090/t/anthropic_prod
Apps not using docker or the network
api_base: http://127.0.0.1:10199/t/anthropic_prod
Both use the consumer token
api_key: ${SUBUMBRA_TOKEN_YOUR_ADAPTER}
Use your apps as usual
Same client libraries and workflows — swap base URL and substitute the narrow adapter token from .env for the real provider secret.
Common bootstrap.sh commands
./bootstrap.sh --helpFull option list (also -h)
./bootstrap.sh --session start --ttl 8h --consumers <ids>Open a scoped, time-boxed unlock window
./bootstrap.sh --session end --allClose every session — vault locks immediately
./bootstrap.sh --show <consumer_id>Paste-ready config snippet for an app or .npmrc
./bootstrap.sh --add-ssh-keyAdd an SSH signing key (also --rotate-ssh-key, --revoke-ssh-key)
./bootstrap.sh --rotate-npm-token <key_id>Swap the upstream npm token; CI config unchanged
./bootstrap.sh --rotateOffline per-key re-encryption with the on-disk public key
./bootstrap.sh --upgradeRebuild images and recreate containers (no volume wipe)
./bootstrap.sh --provision <key_id>Targeted repair for one record
./bootstrap.sh --revoke-key <key_id>Revoke a key (optional --offline for local-only)
./bootstrap.sh --add-consumer <key_id> <consumer_id>Bind an adapter to an existing key
./bootstrap.sh --revoke-consumer <key_id> <consumer_id>Remove a consumer binding
./bootstrap.sh --publish-policy <key_id>Republish policy metadata to KV
./bootstrap.sh --update-janusRefresh Janus approval rules after a policy change
./bootstrap.sh --push-registryPush local endpoint.json state to Cloudflare KV
./bootstrap.sh --list-key-idsList manifest key IDs
./bootstrap.sh --list-consumersList distinct consumer IDs from the manifest
./bootstrap.sh --statusCompare manifest authority to deployed record state
./bootstrap.sh --nukeDestructive: reset vault keypairs and re-bootstrap from scratch

Locked by default. Unlocked on your terms.

Think of a time-lock safe: after bootstrap, nothing is released until you open a bounded session — and the riskiest actions can additionally wait for a human tap.

Scoped sessions

A stolen consumer token is useless while the vault is locked. Open a session for exactly the consumers and keys you need, with a TTL and optional request or signing quotas. Close it early, or let it expire.

  • No session → system_locked. API, SSH, and npm paths all fail closed.
  • Least privilege: unlock this consumer for these keys, not everything.
  • Sessions live in Cloudflare KV — closing your terminal doesn't end them; --session end does.
Janus — per-request human approval

For the actions that really matter — an npm release, an SSH deploy signature, a sensitive API path — Janus holds the request and pushes an approval card to your browser or phone. Nothing decrypts until you say so.

  • Gates /proxy calls, SSH signatures, and npm publishes — reads pass through untouched.
  • Browser push with one-time, HMAC-protected approve/deny links; unanswered holds time out and deny.
  • Approval state lives in its own Durable Object, separate from key custody.

Everything a self-hoster or a team needs to keep secrets out of reach.

Transparent proxy
Drop-in sidecar at :8090/t. No app code changes — swap the base URL and use a narrow consumer token instead of a real key.
Session lockdown
Locked by default after bootstrap. --session start --ttl 8h opens a time-boxed window for chosen consumers and keys; everything else stays system_locked.
👁
Janus approvals
Per-request human approval in front of selected API calls, SSH signatures, and npm publishes. Push notification to your browser; one tap to approve or deny.
🔑
SSH signing agent
A local agent socket your tools already understand. Private keys stay in vault custody; only 64-byte signatures come back — optionally host-bound and quota-limited per session.
📦
npm publish guard
Operation allowlists, scope and identity checks, tarball size caps, and secret-pattern scanning before anything reaches the registry. Rotate the upstream token without touching CI config.
🔄
Offline key rotation
Rotate individual keys using the public key on disk. No Cloudflare interaction, no Worker redeploy, no service restart. Zero downtime.
🧩
Live policy registry
You declare providers and policy in manifest.yaml; bootstrap publishes structured entries to Cloudflare KV so the Worker validates every upstream target fail-closed.
📊
Management console
Vault posture, sessions, consumers, policies, Janus approvals, and a live structured audit log across API, SSH, and npm paths — without ever displaying key values.
🛡
Deploy integrity verification
Bootstrap records the deployed Worker bundle's SHA-256; subumbra-verify-deploy detects drift so a silently replaced Worker doesn't go unnoticed.

Designed so a partial compromise yields nothing useful.

Split trust means ciphertext and wrapped DEKs on your infrastructure are useless without the matching RSA private key held in a Cloudflare Durable Object. A complete break requires defeating both sides — plus your manifest-defined adapter and policy controls at the Worker boundary. Cloudflare and your deploy credentials remain explicit trust boundaries.

  • V3 hybrid envelope (RSA-4096 + AES-256-GCM)
    Per-record DEKs are wrapped with RSA-OAEP. Payloads are AES-256-GCM with AAD subumbra:v3:<key_id>:<policy_hash>, binding ciphertext to the policy in effect at encryption time and blocking transplant or stale-policy replay.
  • Private key in SubumbraVault DO custody
    RSA private material is generated and stored in a Durable Object vault; your host only receives ciphertext envelopes, wrapped DEKs, and an exportable public key for offline rotation. Neither side alone can recover a provider secret.
  • Per-request decrypt in the vault DO
    For each allowed upstream call, the vault unwraps the DEK, decrypts the provider material, applies the configured auth semantics to the outbound HTTPS request, and discards plaintext from memory after the response path completes. This is a short-lived in-memory window — not a durable copy on your servers.
  • Default lockdown with scoped sessions
    No secret is released outside an operator-opened session. Sessions bind specific consumers to specific keys with a TTL and optional request or signing quotas; everything else returns system_locked.
  • Adapter allowlists and capability gates
    Manifest allow.consumers binds each consumer token to specific records; policy can scope methods, paths, hosts, and capability class so a compromised chat app cannot silently pivot to unrelated APIs.
  • HMAC-signed ciphertext fetches
    subumbra-proxy requests envelopes from subumbra-keys with per-request HMAC integrity and freshness controls so replayed or forged fetches fail closed.
  • Fail-closed upstream validation
    The Worker resolves live registry metadata (from KV) for the requested record, verifies hostname, provider label, and fingerprint alignment, and rejects mismatches before unwrap or decrypt.
  • Structured audit trail (local)
    The keys service records access attempts, denials, and lifecycle events to a local SQLite-backed audit store — useful for operators without ever logging provider key material.
 Threat matrix
Server filesystem compromiseBlocked
endpoint.json stolenBlocked
Ciphertext replay attackBlocked
Ciphertext transplantBlocked
Network interception (TLS)Blocked
Skeleton-key adapter misuse (leaked token)Mitigated
Undetected malicious Worker deployPartial
Host OS compromisePartial

Built for homelabs. Shaped for teams.

Self-hosters
Paste your keys once, then forget where they live.
  • One setup command. ./bootstrap.sh walks you through it interactively; secrets stay in RAM during setup and never land in the repo.
  • Your existing tools work. Open WebUI, LiteLLM, n8n, git, npm — swap a base URL or socket path, keep everything else.
  • Work-hours unlocking. Open an 8-hour session when you sit down, close it when you're done — or run a 30-day session for always-on services.
  • Approve releases from your phone. Janus pushes an approval card for npm publishes and SSH deploys, wherever you are.
Teams & enterprises
Blast-radius controls that policy documents only promise.
  • Non-exportable custody. Private keys live in Durable Object vaults and are never returned — there is no "reveal secret" button to misuse.
  • Least-privilege sessions. Unlock one pipeline's consumer for one key with a TTL and quota — not the whole vault.
  • Per-call approval. A human gate in front of decrypt and sign operations, not just access grants after the fact.
  • Unified audit. One structured trail across API, SSH, and npm paths — denials included, key material never logged.
  • Verifiable deploys. Worker bundle hashes recorded at bootstrap; drift detection catches a silently replaced enforcement layer.

Ready to stop trusting your servers with secrets?

Self-hosted, open source, deployable in minutes. API keys, SSH keys, and npm tokens — locked by default, released one request at a time, never in plaintext on your servers after bootstrap.

./bootstrap.sh