Encryption
Orbseal uses end-to-end encryption for secrets. The server stores only ciphertext — it never receives or holds the plaintext value of any secret.
Key model
Every app generates a keypair:
Private key (orbsk-...) |
Stays on your infrastructure. Never sent to Orbseal. |
Public key (orbpk-...) |
Registered with Orbseal when you create an app key. |
Keys use X25519 Diffie-Hellman for key exchange and XSalsa20-Poly1305 for symmetric encryption, implemented via libsodium's crypto_box_seal.
Generating a keypair
npx orbseal keygen
Recovery phrase — write this down and store safely:
witch collapse practice feed shame open despair creek road again ice least
public key orbpk-6VqoWhXczDR1ydCHVFxhNhv8uQaWLicCHRiXQz5fXRet
private key orbsk-F1NdfoJULGa8qdaWr6xAbWeYz7bc5uu6EFwZ3AznGHua
The recovery phrase uses BIP-39 and can regenerate the keypair if you lose the private key file:
npx orbseal recover "witch collapse practice feed shame open despair creek road again ice least"
Sealing a secret
# Save your public key to context once
npx orbseal use --public-key "orbpk-6VqoWhXczDR1..."
# Seal a secret — prompts for plaintext, never logged
npx orbseal seal taskflow:database_url --scope environment --ref production
# Secret value: [hidden]
What happens client-side:
- Plaintext value entered in terminal (hidden input)
crypto_box_seal(plaintext, recipient_public_key)runs locally in Node.js- Only the ciphertext is sent to the API — Orbseal never sees the plaintext
PUT /v1/workspaces/:ws/projects/:proj/secrets/:plugin/:key
{
"ciphertext": "wM2pSljqMNJNrbBAgTo9...",
"public_key": "m1pJN0blB7vdOtYlYn9d...",
"scope": "environment",
"scope_ref": "production"
}
Resolving secrets at runtime
When your app calls /v1/config/resolve, Orbseal returns the ciphertext sealed for your app's public key. Your app decrypts it locally using the private key:
import _sodium from 'libsodium-wrappers';
const { config, secrets } = await fetch(
'https://api.orbseal.com/v1/config/resolve',
{ headers: { Authorization: `Bearer ${process.env.ORBSEAL_TOKEN}` } }
).then(r => r.json());
await _sodium.ready;
const s = _sodium;
const pk = s.from_base64(process.env.ORBSEAL_PUBLIC_KEY, s.base64_variants.ORIGINAL);
const sk = s.from_base64(process.env.ORBSEAL_PRIVATE_KEY, s.base64_variants.ORIGINAL);
const dbUrl = s.to_string(
s.crypto_box_seal_open(
s.from_base64(secrets['taskflow:database_url'], s.base64_variants.ORIGINAL),
pk, sk
)
);
Threat model
| Threat | Mitigation |
|---|---|
| Orbseal server compromised | Attacker gets ciphertext only — useless without app's private key |
| Network interception | TLS + ciphertext in transit |
| Admin token leaked | Attacker can set new ciphertext, but cannot read existing secrets |
| Private key lost | Recover from BIP-39 phrase |
| Private key compromised | Rotate: generate new keypair, re-seal all secrets |
Rotating secrets
If a secret is compromised or a key is rotated:
# Generate new keypair
npx orbseal keygen
# Update the app key with the new public key
npx orbseal keys create "prod-worker-v2" \
--public-key "orbpk-<new>" \
--env production
# Re-seal all secrets with the new public key
npx orbseal use --public-key "orbpk-<new>"
npx orbseal seal taskflow:database_url --scope environment --ref production
npx orbseal seal taskflow:stripe_secret --scope environment --ref production
# Deploy new app with new private key
# Revoke the old app key
npx orbseal keys revoke <old-key-id>