# 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 `-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](https://docs.photoprism.app/getting-started/config-options/) describes it as an initial superadmin password. Existing installations instead use the authenticated native account API. The adapter follows PhotoPrism's [password endpoint](https://github.com/photoprism/photoprism/blob/develop/internal/api/users_password.go) and Grafana 10.2's [user API](https://github.com/grafana/grafana/blob/v10.2.0/pkg/api/user.go). The exact pinned image versions still require container qualification. Operator flow (backend API ready for later UX review): 1. 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. 2. Obtain fresh node authorization for `security.rotate-app-admin` and these exact operation parameters: `{ "app_id": "photoprism", "username": "admin", "app_password": "" }` (or `grafana`). 3. 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. 4. The candidate is synced in `-admin-rotation.json` before 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. 5. 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](2.1-fresh-authorization-uat.md). ## 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.