9.0 KiB
2.1 credential boundaries and migration
This source change addresses audit H-1/H-3/H-4/H-5. It is not installed-node, container-image, browser UX, backup-artifact or release acceptance. It performs no live password rotation, wallet operation or publication.
At-rest boundaries
| Material | Actual protection | Recovery dependency |
|---|---|---|
identities/*.json Ed25519/Nostr secrets |
Plaintext JSON, owner-only file permissions | Protected identity/full backup |
identity/node_key |
Local file, available to the service account/root | Identity/full backup and seed recovery |
Message/contact stores and credential stores derived from node_key |
Authenticated ciphertext; disclosure of the key file plus store allows decryption | Both store and its original node key |
Manifest generated_secrets |
Plaintext owner-only files, passed to runtime secret references where supported | Full backup and app-native account recovery |
SecretsManager v2 records |
AES-256-GCM, random per-store key, authenticated version header | secrets/.manager/key-v2 plus records |
None of these filesystem-local designs protects against root or a complete
unencrypted disk image containing the keys. Disk encryption and hardware/keyring
custody are separate controls; no hardware key protection is claimed here.
Manifest-generated app secrets and the encrypted SecretsManager are distinct
stores. The v2 migration does not re-encrypt every application secret.
Encrypted manager migration
Production opens SecretsManager::open_protected on a blocking worker. Its
service-owned .manager directory is mode 0700; the random 32-byte key and lock
are mode 0600. Symlinks, public/wrong-owner custody, corrupt/missing keys and
unknown envelope versions fail closed. The exclusive file lock serializes
manager instances and mutations until the manager is dropped.
The old path-derived key is used only to authenticate v1 records. Every record is checked before a new key or replacement ciphertext is written. Legacy plaintext requires matching app/secret metadata and valid UTF-8. An unknown header is never interpreted as plaintext. A failure preserves all originals. The random key is synced before atomic per-record replacement. An interruption can leave a v1/v2 mixture; reopening validates both and resumes with the same key. Metadata, values and rotation counts are preserved by migration.
Back up the entire secrets tree, including .manager, with its permissions.
The existing full-backup inventory includes this tree. Never regenerate a lost
v2 key or remove an unreadable key: restore its protected backup with the
matching records. A legacy store moved before migration must first be migrated
at its original data-directory path, or recovered using that exact original
derivation in a reviewed offline procedure. The service does not guess paths
or silently overwrite ciphertext when legacy authentication fails.
PhotoPrism and Grafana
Fresh empty app storage receives a unique 32-byte random password, saved once
as <app>-admin-password. The manifests use generated_secrets/secret_env;
legacy configuration and installer paths read the same protected file. Grafana
no longer receives a literal interpolation placeholder or its upstream default.
Missing/unreadable credentials for an existing data directory are not
regenerated. Reconcile refuses that transition with accounts/data untouched.
Changing PhotoPrism's initial-password environment variable does not change an existing account. Its configuration documentation describes it as an initial superadmin password. Existing installations instead use the authenticated native account API. The adapter follows PhotoPrism's password endpoint and Grafana 10.2's user API. The exact pinned image versions still require container qualification.
Operator flow (backend API ready for later UX review):
- Preserve the application's existing data and account inventory. Use the current app administrator username/password, which may already be customized. Start a stopped app only through a separately authorized lifecycle action; migration never installs, starts, stops, removes or recreates containers.
- Obtain fresh node authorization for
security.rotate-app-adminand these exact operation parameters:{ "app_id": "photoprism", "username": "admin", "app_password": "<current app password>" }(orgrafana). - Call that method with those parameters plus the returned
reauth_token. Node session, origin, CSRF and role checks still apply. The node verifies native administrator login before recording a new random candidate. - The candidate is synced in
<app>-admin-rotation.jsonbefore the native password-change API is invoked. The application changes only that existing account's password. A fresh login must prove the candidate and the same account ID before canonical username/password files are promoted. - If a response is lost or verification fails, keep the pending file and canonical secret. Reauthorize and retry the same operation. The service first verifies the recorded candidate; it does not choose another password or reset the account twice. A pending file blocks lifecycle credential generation until completion. A privileged operator can recover its candidate from the private journal if the app's API is unavailable. Do not delete that journal or restore a whole app database merely to undo an uncertain reply.
The adapter uses only loopback with a fixed app/allocated port, no ambient proxy, no redirect following, bounded replies and request timeouts. Passwords are sent in request bodies/authorization headers, never subprocess arguments or logs. Credentials are retrieved through the freshly authorized dashboard RPC.
Fresh operation authorization
auth.reauthenticate takes password, optional enabled-TOTP code, target
method, and exact target params. A successful proof returns a random
reauth_token valid for 60 seconds. Include it only in that target request.
It is single-use, session-bound, operation/parameter-bound and memory-only.
Missing, expired, replayed, other-session, changed-parameter and changed
password/TOTP/role-state grants fail before handler dispatch. The current
session is validated at mint and consumption. Device-token login and remembered
session restoration never mint grants. Proof attempts are bounded node-wide.
reauth_for_funds is consulted centrally for sends, payments, channel actions,
fee bumps and payment-producing content/streaming operations. Signing, key and
backup exports, secret retrieval/rotation, device-token creation and changing
the session policy always require fresh proof. Payment recovery and completed
download paths do not require a new payment or a new payment grant. Seed reveal
flows retain their existing request shape and use the shared atomic
password/TOTP verifier instead of consuming a second grant/code.
TOTP steps and backup codes are consumed under the user-record write lock;
persistence failures refuse authorization. Concurrent login/reveal consumers
must use the atomic helpers rather than replacing stale TOTP records. The
dashboard now has a shared password/TOTP prompt, including the signer broker.
Only the explicit HTTP pre-dispatch error code -32041 opens it. It freezes
the original wire parameters, obtains a grant and dispatches once; a lost
payment response never triggers another payment or another prompt. Closing,
navigating, aborting or logging out cancels pending proof. Passwords, codes
and grants are not persisted in browser storage.
For application credentials, fresh proof protects
package.credentials-authorize. Its session/app-bound one-use capability is
minted after the human proof completes, and package.credentials consumes
that capability. Its 30-second lifetime therefore does not run while the
operator enters a password or waits for a new TOTP step. The manual acceptance
plan is fresh authorization UAT.
Required remaining acceptance
- Isolated backend regression, including the real HTTP middleware and native app API fixture tests; source tests remain distinct from running images.
- Pinned PhotoPrism/Grafana images: fresh installs; customized administrator accounts; shared-default migration; stopped/uninstalled decisions; failed native request; lost reply/retry; unchanged user/photo/dashboard inventory.
- Full-backup artifact restore, moved v2 store, missing/corrupt key handling, interrupted migration and operator recovery with private backup custody.
- Manual desktop/mobile light/dark UI review of fresh proof, app credentials, migration-required and pending-recovery flows, including TOTP/device/remember sessions and paid-file recovery that never pays again.
- Regenerate and sign catalogs through the separate release process. Edited checked-in catalog payloads do not retain prior signature validity.