Skip to content

Credential management & rotation

Status: Accepted · Audience: Operator · Area: A — Getting started


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.

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:

  1. New value supplied.
  2. Validated against the live service (A3).
  3. Only on success, propagated to every consumer and the old value destroyed.
  4. 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.

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.

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.

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
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.
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.

This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page