Secrets¶
What is encrypted¶
Three secret keys live in the dashboard dashboard_config table:
minio_access_keyminio_secret_keygrafana_api_key
Their values are stored in the secret_value column as Fernet ciphertext
(AES-128-CBC + HMAC-SHA256). The plaintext never leaves the backend except
when injected into a proxied request (Grafana).
How keys move¶
Admin types value in UI
│
▼
PUT /api/config {grafana_api_key: "g-key"}
│
▼ secret_store.encrypt("g-key")
│
▼
Postgres dashboard_config.secret_value (bytea, Fernet token)
│
▼ secret_store.decrypt(...) on read
│
▼
Used by inject_grafana_bearer in routers/proxy.py
The Fernet key¶
DASHBOARD_SECRET_KEY is a 44-character base64-urlsafe Fernet key.
Generate one:
Lose this key and the stored secrets are unrecoverable. They can only be re-entered through the UI by an admin. The backend fails fast on startup if the key is unset, so you'll find out immediately rather than at the first secret write.
Masking on read¶
GET /api/config returns "***" for any secret key that has a value, and
null for unset secret keys. Plaintext is never returned to the browser.
PUT semantics for secret keys¶
| User intent | Send in JSON body | Backend action |
|---|---|---|
| Leave as-is | omit key entirely | nothing |
| Type a new value | string non-empty | encrypt + store; audit set |
| Clear the secret | null |
NULL the column; audit clear |
Anything else (e.g. blank "") |
rejected | 422 Unprocessable Entity |
Manual key rotation¶
DASHBOARD_SECRET_KEY rotation is out of scope for this iteration.
If you must rotate (e.g. suspected key leak):
- As admin, log in and clear all three secret keys via the Config UI
(the × button). Verify with
GET /api/configthat each showsnull. - Stop the dashboard.
- Replace
DASHBOARD_SECRET_KEYin.envwith a freshly generated Fernet key. - Start the dashboard.
- As admin, re-enter the three secret values via the Config UI.
Do not skip step 1. If you change the Fernet key while ciphertext rows still
exist, every read of those rows raises cryptography.fernet.InvalidToken and
the affected proxied requests will fail.
Audit¶
Every secret write produces a row in dashboard_audit:
| column | content |
|---|---|
id |
autoincrement |
at |
timestamp UTC |
role |
always admin (only role that writes) |
action |
set / clear |
key |
which config key was touched |
No values — neither plaintext nor ciphertext — are stored in the audit table.
Read recent rows from the UI (admin → Audit page) or the API: