Credential management & rotation
Status: Accepted · Audience: Operator · Area: A — Getting started
Purpose
Section titled “Purpose”The stack accumulates a substantial set of secrets: a VPN private key, Usenet provider passwords, indexer API keys, and the API keys each service generates for itself. They live on disk, they get shared between services, and they need changing occasionally.
Without a deliberate design they end up scattered across a plaintext .env, a
dozen service config files, and — worst — pasted into logs and screenshots when
the operator asks for help. The operator has no single answer to “where are my
secrets, and how do I change one?”
This feature makes credentials locatable, changeable, and hard to leak.
Behaviour
Section titled “Behaviour”One inventory
Section titled “One inventory”The operator can see every credential the stack holds: what it is, which service uses it, when it was last validated, and where it’s stored — without any of the values being displayed.
| Credential | Used by | Origin |
|---|---|---|
| VPN private key | Gluetun | Operator, from their provider |
| Usenet provider password | SABnzbd | Operator |
| Indexer API keys | Prowlarr | Operator |
| Service API keys | Every *arr, Homepage, Seerr | Generated by the services themselves |
| qBittorrent WebUI password | qBittorrent, the forwarded-port push, the dashboard, and each *arr’s download-client registration | Generated by lemonfiber (A7-R14) |
The last two rows matter: most secrets in the stack are ones the services minted or lemonfiber minted, not ones the operator supplied. Those are lemonfiber’s to manage entirely, and the operator should never need to see or copy them — that copying is precisely the manual labour D1 seeding exists to eliminate.
The qBittorrent row is the one credential lemonfiber generates rather than reads.
qBittorrent mints a temporary WebUI password on each start and asks for it to be
replaced, so there is nothing stable to read; and it has a consumer that is not a
service at all — the VPN’s forwarded-port push, which authenticates against the
WebUI API on every connect and release. A rotation that reaches qBittorrent but not
that consumer leaves the tunnel silently unable to apply the port, which is why
A7-R6 names every consumer rather than every service.
That row has four consumers, and the two beyond the obvious pair are reached differently. The forwarded-port push and the dashboard each read the password out of their own container’s environment, fixed at the moment that container was created — so recording a replacement is only half of reaching them, and the other half is re-creating the container. Each *arr holds its own copy in the download-client registration lemonfiber wrote there, which is reached over that service’s API rather than through the environment at all. Three ways of holding one credential, and a report that counted only the first would say a rotation had finished while three consumers were still authenticating with the old value.
Rotation is a supported operation, not a reinstall
Section titled “Rotation is a supported operation, not a reinstall”Changing a credential is a first-class action:
- New value supplied.
- Validated against the live service (A3).
- Only on success, propagated to every consumer and the old value destroyed.
- Affected services restarted.
Validating before discarding is the essential ordering — a bad paste must never leave the operator with neither a working old credential nor a working new one.
Secrets never appear in output
Section titled “Secrets never appear in output”Not in logs, error messages, dashboards, diagnostic output, or the support bundle. Where a credential must be referenced, it is identified by name and a stable fingerprint, never by value.
This is the property most likely to be violated by accident — a debug log line, a verbose error containing a full request URL with an embedded API key — so it is stated as an absolute rather than a guideline, and is enforced by test.
Storage is honest about its protection level
Section titled “Storage is honest about its protection level”Credentials are stored in files the operator owns, with restrictive permissions. lemonfiber states plainly what that does and does not protect against: it protects against other users on a shared machine; it does not protect against malware running as the operator, or against an unencrypted backup.
Overstating protection is worse than stating a modest guarantee accurately.
Rotation reminders, not enforcement
Section titled “Rotation reminders, not enforcement”Where a credential has a known lifetime or has gone stale, lemonfiber surfaces it as an advisory. It does not expire credentials or force rotation — that would break a working system on a schedule the operator didn’t choose.
States
Section titled “States”| State | Meaning |
|---|---|
absent |
Required but not supplied |
active |
Present and last validated successfully |
stale |
Present, not validated within the freshness window |
invalid |
Validation failed at last attempt |
rotating |
Replacement supplied and being validated |
superseded |
Replaced; scheduled for destruction |
Edge cases
Section titled “Edge cases”| Situation | Behaviour |
|---|---|
| Rotation fails validation | Keep the existing credential; report the failure. Never leave the operator with neither. |
| Rotation succeeds but a consumer fails to restart | Report which service is out of sync and offer retry. Don’t leave a partial rollout silent. |
| Service regenerates its own API key | Detect the mismatch and re-propagate. This is a real occurrence after some upgrades and silently breaks inter-service wiring. |
| Operator asks to view a stored secret | Permitted, with explicit confirmation and a warning about shoulder-surfing and scrollback. It’s their secret; refusing outright is paternalistic. |
| Credential appears in a service’s own logs | Outside lemonfiber’s control. Note it in the support-bundle redaction rules and redact on the way out. |
| Backup contains credentials | State it explicitly. A backup is as sensitive as the credentials it holds, and operators routinely forget this. |
| Config file permissions are too open | Detect and offer to tighten. Report as a diagnostic finding. |
| Same credential used by multiple services | Rotate everywhere atomically, or report exactly which consumers were updated. |
| Operator supplies a credential lemonfiber can’t attribute | Reject it rather than storing an unattributable secret. |
Acceptance criteria
Section titled “Acceptance criteria”| ID | Requirement |
|---|---|
| A7-R1 | lemonfiber MUST provide a single inventory of every credential it holds, showing name, consumer, storage location, and validation state. |
| A7-R2 | The inventory MUST NOT display credential values. |
| A7-R3 | Credential values MUST NOT appear in logs, errors, dashboards, diagnostics, or support bundles. |
| A7-R4 | Rotation MUST validate the replacement before destroying the existing value. |
| A7-R5 | On rotation failure, the existing credential MUST remain active and functional. |
| A7-R6 | Successful rotation MUST propagate to every consumer and MUST report any consumer that could not be updated. |
| A7-R7 | lemonfiber MUST detect a service-regenerated API key and MUST re-propagate it. |
| A7-R8 | Credential files MUST be created with permissions restricting access to the owning user. |
| A7-R9 | lemonfiber MUST state accurately what its credential storage protects against, and MUST NOT overstate it. |
| A7-R10 | Revealing a stored credential MUST be possible, and MUST require explicit confirmation with a warning. |
| A7-R11 | Stale credentials MUST be surfaced as advisories and MUST NOT be automatically expired. |
| A7-R12 | Backups containing credentials MUST be labelled as sensitive at creation time. |
| A7-R13 | Over-permissive credential file permissions MUST be reported as a diagnostic finding with a remedy. |
| A7-R14 | Where a service mints only a temporary credential, lemonfiber MUST generate a durable replacement, set it on the service, and record it for its consumers. The operator MUST NOT be required to handle it. |
Affected repos (GOV-R7): lemonfiber generates and propagates the
credential; lemonfiber-media-stack consumes it from the environment so that the
forwarded-port push keeps working without lemonfiber present.
Related
Section titled “Related”- A3 Credential validation — the validation used by rotation
- A6 Uninstall — credential destruction on removal
- C4 Support bundle — redaction rules
- D1 Service auto-wiring — the service-generated key flow
- G8 Privacy stance
This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page