Secrets
Secrets are a dedicated item type for sensitive files — API keys, .env files, credentials. Unlike every other item type, secret content is encrypted before it is written to GitHub, so the directory repo only ever stores ciphertext.
Storing and retrieving secrets
.env-style filenames (.env, .env.production, local.env, …) are detected automatically:
sb put .env# Stored encrypted (secret) .env in webapp at webapp/secrets/envAny single file can be stored as a secret with --type secret:
sb put service-account.json --type secret# Stored encrypted (secret) service-account.json in webapp at webapp/secrets/service-account-jsonsb get decrypts the secret on the server and restores it under its original filename with owner-only permissions (0600):
sb get env# Downloaded .env (secret, decrypted) to .envThe slug is derived from the filename (.env → env, .env.production → env-production), so use sb get <project>/<slug> to pick the right one when several projects each have an env secret.
How it works
Secret content is encrypted with AES-256-GCM under a server-side master key before it leaves the SkillsBin application. What lands in your GitHub directory repo is an armored JSON envelope stored as secret.enc:
{ "format": "skillsbin/secret-envelope", "v": 1, "alg": "A256GCM", "nonce": "…base64…", "data": "…base64…"}A fresh random nonce is used for every write, and GCM authentication means any tampering with the stored ciphertext is detected at decrypt time.
Threat model
What this protects against, and what it doesn’t:
- GitHub never sees plaintext. The directory repo is private anyway, but for secrets that’s not the only line of defense: repo clones, GitHub staff access, leaked backups, or an accidentally broadened repo permission only ever expose ciphertext.
- The SkillsBin server holds the key. Encryption and decryption happen server-side with the master key (
SB_SECRETS_KEY). This is server-side encryption at rest, not end-to-end encryption — the server operator can decrypt secrets. If you don’t want that, self-host (see below). - Authorized users get plaintext via the API. Anyone who can read the item through SkillsBin — you, members of the org it lives in, or a recipient of a share — receives decrypted content. Access control is enforced by the API, not by the ciphertext.
- Sharing re-encrypts. When a shared secret is saved by a recipient, the copy in their inbox is encrypted the same way; it never lands in their repo as plaintext.
Self-hosting: SB_SECRETS_KEY
Self-hosted servers must configure the master key before secret storage works. It’s a base64-encoded 32-byte key, passed via the SB_SECRETS_KEY environment variable:
# Generate a keyopenssl rand -base64 32
# docker-compose / .envSB_SECRETS_KEY=q83vGm…your-generated-key…=Notes:
- If
SB_SECRETS_KEYis unset,sb put --type secret(and any secret read) fails withSECRETS_NOT_CONFIGURED. Non-secret item types are unaffected. - Back the key up. Losing it makes every stored secret permanently unrecoverable — the repo only holds ciphertext.
- Changing the key does not re-encrypt existing secrets; previously stored secrets will fail to decrypt (
SECRET_DECRYPT_FAILED) under a new key.
Limits
- Single-file only. A secret is exactly one file — directories and extra files are rejected. Store multiple secrets as multiple items.
- The original filename is preserved in the item’s
sb.jsonmanifest (source_filename), not in the storage path. - Secret contents are not searchable;
sb searchmatches only name, slug, and description.