Encryption & secrets
Several settings carry sensitive values β pool passwords, API connector auth tokens, the OIDC client secret. The framework offers a simple primitive to keep them safe: any secret field in the Settings UI has a π toggle that switches the input between plain text and encrypted. Encrypted values are stored as opaque blobs that can only be decrypted by the running process; the master key that does the decryption sits in the environment of the host, never on disk.
This page covers the toggle in the UI, the master key, how to rotate keys and the convention for what to encrypt vs what to keep in the environment.
At a glanceβ
JWT signing key. License key. AI API key.
Never on disk.
Safe to commit to git.
Cached for the process lifetime.
The master key never lands on disk; the encrypted blob never lands in the environment. The two only meet at decryption time, inside the running process.
The π toggle in the Settings UIβ
Any field flagged as a secret β Pool Password, Connector Auth token, OIDC Client secret, Slack Webhook URL, etc. β shows a π icon next to the input. Click it to switch the field between plain and encrypted mode.
| Mode | What happens on save |
|---|---|
| Plain | The literal value is stored. Use only for non-secret defaults (e.g. an empty password on a local SQLite pool). |
| π Encrypted | The framework encrypts the value with the current master key before saving. The field displays encrypted + a masked dot row. Editing the field overwrites the previous value (no reveal happens automatically). |
A field already in encrypted mode shows a Reveal button (visible only to operators carrying settings:reveal-secrets). Clicking it asks for the master-key fingerprint as confirmation, then displays the decrypted plaintext for 10 seconds. The reveal action is audit-logged.
Some fields β the OIDC Client secret, the Slack Webhook URL β have the π toggle locked on. Plain mode isn't offered because the value is unambiguously sensitive.
The master keyβ
The master key is a 32-byte AES-256-GCM key that the framework loads from the environment at startup. The Settings UI's Framework β Encryption section configures where the key comes from, not what its value is:
| Field | Effect |
|---|---|
| Master key | Name of the environment variable holding the key. Default ${LIBERTY_MASTER_KEY}. |
| Legacy keys | List of older keys, used for decryption only during a rotation. New encryptions always use Master key. Click + Add legacy key to add an entry β each entry is an env var name. |
Generate a fresh key with the liberty-crypto CLI:
.venv/bin/liberty-crypto genkey
# 7c4f1c2d8e3a6b9f0c1d4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c
Export it under the env var name configured above, restart the framework, and every new save through the π toggle uses the new key.
A master key fingerprint (SHA-256 of the key) is surfaced on the Settings β Framework β Encryption section β useful to confirm two replicas share the same key without exposing the key itself.
Rotating the master keyβ
When the master key needs to change β annual policy, suspected leak, change of operator β the rotation is online:
| Phase | Action |
|---|---|
| 1. Generate a new key | liberty-crypto genkey. Export it under a fresh env var name (e.g. LIBERTY_MASTER_KEY_NEW). |
| 2. Add the old key as legacy | In Settings β Framework β Encryption, set Master key to the new env var name and add the old env var to Legacy keys. Save β the framework now decrypts using either key (tries master first, then each legacy in order). |
| 3. Restart | Required because the master key itself is read at startup, not on save-reload. |
| 4. Re-encrypt every secret | Run liberty-crypto rewrap β see below β to re-encrypt every stored blob with the new master. Idempotent; safe to re-run. |
| 5. Drop the legacy entry | Once Rewrap reports zero remaining blobs encrypted with the old key, remove the legacy entry from the Settings form and unset the old env var. Restart. |
The rewrap command:
.venv/bin/liberty-crypto rewrap
# 4 encrypted fields re-wrapped with the current master key
# 0 still using legacy keys
The command crawls every storage location, decrypts each blob with whichever key works, and re-encrypts with the current master. Idempotent β re-running produces a different ciphertext (new random nonce) but the plaintext is unchanged.
What to encrypt vs. what to keep in the environmentβ
The split is deliberate and worth following:
| Secret | Where it lives | Why |
|---|---|---|
| Pool password | π Encrypted in the pool definition. | Lives next to the pool β one storage change updates both. |
| API connector auth token | π Encrypted on the connector. | Same reason. |
| OIDC client secret | π Encrypted on the OIDC sub-form. | Always encrypted (toggle locked on). |
| Slack webhook | π Encrypted on the Notifications sub-form. | Same. |
| Per-app API key for an HTTP connector | π Encrypted on the connector. | Same as OIDC. |
| Master key | Environment only. | The whole scheme depends on this never landing on disk. |
| JWT signing key | Environment only. | Rotated independently of the master key. |
| License key | Environment only. | RS256-signed JWT, public-key-verifiable; not sensitive to disclose, but kept in env to be replaceable without a save. |
| AI provider API key | Environment only. | Convention in the Anthropic ecosystem. |
A safe rule of thumb: anything that has a "scope" tied to a single connector / settings entry goes through the π toggle; anything that is global to the framework goes in the environment.
Permissionsβ
| Code | Effect |
|---|---|
settings:framework | View the Framework β Encryption section. |
settings:reveal-secrets | See the Reveal button next to encrypted fields. Audit-logged. |
The Master key fingerprint read-out is visible to anyone with settings:read β it's not sensitive on its own.
Failure modesβ
| Symptom | Cause | Recovery |
|---|---|---|
connector loaded, but password rejected by database | The encrypted blob was wrapped with a key that's no longer in Legacy keys. | Add the old key back as legacy, restart, run liberty-crypto rewrap, then drop the old key. |
crypto: master key not set at startup | The env var configured on Master key is unset and at least one encrypted blob exists. | Export the key under the right name; or temporarily flip the field back to plain to rescue the install. |
crypto: authentication tag mismatch | A blob was edited by hand, or the wrong key is loaded. | Re-encrypt the value from the plaintext source; never edit ciphertext by hand. |
| Reveal button greyed out | The caller lacks settings:reveal-secrets. | Grant the permission (or refuse β auditors often shouldn't reveal). |
Tips & best practicesβ
- Generate the master key with
liberty-crypto genkey. Hand-crafted keys are a footgun β 32 random bytes from the framework's PRNG is the simplest correct path. - Use the same env-var name across replicas. Settings β Framework β Encryption stores the name, not the value β the same form works on every replica as long as the env var resolves to the same key.
- Run
liberty-crypto rewrapafter every rotation. Otherwise old blobs sit on legacy keys and the rotation isn't really finished. - Don't disable the π toggle on a value that's already encrypted. Switching back to plain reveals the value on disk; the operator has to confirm and the action is audit-logged.
- Keep legacy keys only as long as needed. Past one rotation cycle, every still-relevant blob has been rewrapped β the legacy entry can be dropped.
Under the hoodβ
Encrypted values are stored as opaque ENC:-prefixed blobs inside the per-section TOML files. Operators do not edit these blobs by hand; the π toggle is the only safe path. The master key fingerprint is also stored on disk so the framework can detect a mismatched key on the wrong host without exposing the key itself.
The liberty-crypto CLI is the only scripted path for advanced operations (rewrap, fingerprint inspection, hand-encryption for a script-driven setup).
What's nextβ
- Environment variables β including the master key env var.
- Framework settings β the Encryption sub-section.
- CLI reference β liberty-crypto β every subcommand.
- Authentication β where the OIDC client secret lives.