Skip to content

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:

Terminal window
sb put .env
# Stored encrypted (secret) .env in webapp at webapp/secrets/env

Any single file can be stored as a secret with --type secret:

Terminal window
sb put service-account.json --type secret
# Stored encrypted (secret) service-account.json in webapp at webapp/secrets/service-account-json

sb get decrypts the secret on the server and restores it under its original filename with owner-only permissions (0600):

Terminal window
sb get env
# Downloaded .env (secret, decrypted) to .env

The slug is derived from the filename (.envenv, .env.productionenv-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:

Terminal window
# Generate a key
openssl rand -base64 32
# docker-compose / .env
SB_SECRETS_KEY=q83vGm…your-generated-key…=

Notes:

  • If SB_SECRETS_KEY is unset, sb put --type secret (and any secret read) fails with SECRETS_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.json manifest (source_filename), not in the storage path.
  • Secret contents are not searchable; sb search matches only name, slug, and description.