Created: 17-07-2026
Inputs: REVIEW.md and ai-review-2.md
Baseline: commit 118ab79 plus the working-tree changes reviewed on 17-07-2026
Relationship to the original: This is a separate, expanded implementation companion to MITIGATION_PLAN.md. It does not replace or modify that plan.
The implementation work for Phases 0-7 is complete on the mitigation branch. RedMan now has explicit hardened proxy and native local authentication modes, one migration path, bounded run/process pipelines, and a repeatable local quality gate.
Unattended production classification still requires deployment-only gates that cannot be proven in this workspace: validate a designated canary before additional targets, observe actual proxy source addresses, verify the restricted backup account and socket proxy on each configured host, and complete a real Hyper Backup SSH transfer. Do not deploy with guessed PEER_HOST, origin, or proxy trust values.
- Contain before refactoring. Disable or fence dangerous behavior before changing its internals.
- Write the failing regression first. Every Critical or High fix starts with a test that reproduces the audited defect.
- One invariant per change. Restore selection, source deletion, peer identity, and run locking should be independently reviewable and reversible.
- No silent fallbacks. Authentication mode, destructive sync policy, restore version, and peer ownership must be explicit.
- Do not broaden privileges to make a feature work. In particular, do not add
CAP_SYS_ADMIN, unrestricted root SSH, or direct Docker-socket access without a separate threat decision. - Canary one target at a time. Deploy to a designated canary, run the phase gate, then proceed to other configured targets.
- Keep contracts and documentation synchronized. Schema, API, environment, and behavior changes update
README.md,contracts/v1.json, migrations, seed data, and tests in the same change. - Prove greenfield behavior. Public product paths must work without private SSH aliases, host paths, accounts, groups, databases, or existing containers; reusable operator profiles stay in gitignored local configuration.
flowchart LR
P0[Phase 0<br/>Containment] --> P1[Phase 1<br/>Verification foundation]
P1 --> P2[Phase 2<br/>Data integrity]
P1 --> P3[Phase 3<br/>Trust and peer isolation]
P2 --> P4[Phase 4<br/>Reliability and scale]
P3 --> P4
P4 --> P5[Phase 5<br/>UX and product completeness]
P3 --> P6[Phase 6<br/>Native authentication]
P5 --> P7[Phase 7<br/>Maintainability]
P6 --> P7
| Phase | Objective | Indicative effort | Exit meaning |
|---|---|---|---|
| 0 | Stop known high-impact paths from being exploitable | 0.5-1 day | Controlled use only |
| 1 | Make tests prove protected behavior rather than route existence | 1-2 days | Fixes can be trusted |
| 2 | Restore backup and import data-integrity guarantees | 4-7 days | Restore-safe local operation |
| 3 | Establish real proxy, pairing, SSH, path, and peer boundaries | 5-8 days | Safe multi-peer operation |
| 4 | Make long-running operation bounded and recoverable | 3-5 days | Unattended-operation candidate |
| 5 | Remove misleading controls and repair core workflows | 4-7 days | Product workflows are complete |
| 6 | Add optional native authentication and authorization | 5-10 days | Direct access can be authenticated safely |
| 7 | Consolidate architecture and remove dead paths | 2-5 days | Sustainable maintenance baseline |
Effort is deliberately approximate. A phase closes on its gate, not on elapsed time.
Branch: fix/audit-mitigation-phase-0
Started: 17-07-2026
Completed on this branch:
- Correct historical revision ordering for browse, preview, download, and restore selection.
- Parse snapshot directory timestamps as UTC in retention and delta-keyframe logic.
- Add an idempotent baseline schema migration; clean volumes now start twice without seeding.
- Repair local auth test flags and make live compatibility fail on
401instead of counting it as success. - Restore peer quota data to authenticated request context.
- Keep cancelled SSD/Hyper runs terminal instead of overwriting them with
failed. - Treat
partialas terminal in frontend progress polling. - Bind paired Hyper jobs/runs to stable static peer identities; isolate shutdown and status access by peer.
- Add transactional run claims across manual and scheduled SSD, Hyper, and Rclone triggers.
- Disable unsafe delete-after-import in service, API, UI, and existing database settings.
- Add fail-closed empty/inaccessible-source checks for SSD, Hyper, Rclone upload/download, and bisync.
- Require explicit non-root backup scope and finite quota when accepting a peer.
- Reject malformed, non-Ed25519, and multi-line SSH public keys before authorized-key writes.
- Fix pairing expiry/error transitions and match peers by static identity rather than display name.
- Enforce realpath peer confinement, reject symlink escapes, and create destination directories safely.
- Remove unrestricted root browsing and sensitive RedMan/system paths from the main filesystem picker.
- Stream Hyper rsync file events into bounded database batches and cap retained diagnostic output.
- Use SQLite online backup, integrity checks, staged startup restore, and rollback-safe installation for WAL databases.
- Persist per-file Immich fingerprints/outcomes and delete only verified, unchanged source files.
- Bind protocol-v3 pairing signatures to canonical request/callback transcripts and require fingerprint confirmation.
- Hash incoming peer credentials, encrypt outgoing credentials, and migrate legacy values idempotently.
- Generate forced-command, source-restricted Ed25519 authorization entries for the dedicated
redman-backupaccount. - Route Docker access through a pinned, private socket proxy; remove RedMan's raw socket and host networking.
- Remove Immich credentials from process arguments and enforce a typed settings allowlist.
- Add deterministic fatal/graceful shutdown, bounded child termination, and service-owned scan completion.
- Validate cron expressions and next occurrences exactly, preserve selected minutes, and support eight-hour schedules.
- Make drive ejection an optional narrow-helper capability and disable unsupported UI controls.
- Persist per-snapshot accounting summaries and move deep delta verification into cancellable tracked runs.
- Replace blocking repeated quota scans with fail-closed, asynchronous, invalidated short-lived caching.
- Add lightweight adaptive progress polling that pauses in hidden tabs and stops at terminal state.
- Bound run/file, peer-audit, metric, and stale-summary retention; throttle routine peer-auth writes.
- Give partial and cancellation notifications distinct policy, severity, and terminal ordering.
- Surface initialization and form errors inline, preserving backend safety messages and retry paths.
- Replace every browser alert/confirm and legacy modal surface with one portal-based accessible dialog primitive.
- Eliminate mobile page/dialog overflow across 390x844, 768x1024, 1024x768, and 1440x900 viewports.
- Expose retention for plain/delta versioning and refresh SSD history immediately on filter changes.
- Complete manual Hyper setup with private URL/key, push/pull direction, and validated SSH overrides.
- Project last success/issue, next run, staleness, and verified restore onto configuration cards.
- Persist restore outcomes and optionally verify restored bytes with SHA-256 before reporting recoverability.
- Wire start/progress notification controls across all engines with per-run rate limiting and terminal cleanup.
- Expose normalized exclude patterns, empty-source opt-in, and database retention settings in the UI.
- Consolidate byte formatting, pairing retries, compact snapshot labels, and accessible icon names.
- Accept ADR 001 with explicit, non-fallback
proxyandlocalauthentication modes. - Add migration 22 identity, Argon2id credential, opaque session, recovery, and auth-audit schema.
- Implement first-admin bootstrap, login/logout, session expiry, CSRF, password rotation, and one-time host recovery.
- Add provider-specific admin/viewer roles and fail-closed permission declarations for every contracted API route.
- Preserve Pangolin proxy mode while requiring exact HTTPS origin, narrow trusted proxy CIDRs, and explicit account provisioning.
- Add account/session/audit UI, viewer self-account access, and read-only feature controls.
- Revoke sessions on logout, recovery, role/disable changes, password rotation, and database restore.
- Harden concurrent lockout/recovery, opaque proxy subjects, client-IP derivation, and private database backup modes.
- Preserve per-target auth configuration during deploy and remove shipped admin auto-provisioning.
- Consolidate schema ownership in numbered migrations 0-25 and make seeding data-only.
- Share feature-scoped run claims, pagination, progress, detail, ownership, and cancellation transitions.
- Use one bounded, chunk-aware rsync parser and process registry for local and SSH transfers.
- Remove abandoned delta, notification, redaction, shell-deletion, and frontend plumbing while preserving frozen v1 exports.
- Normalize and validate
PEER_API_PORT,PEER_HOST, and SSH runtime configuration across every launch artifact. - Extract and component-test the repeated per-job notification policy UI.
- Add a noninteractive lint, regression, clean-start, compatibility, build, audit, browser, and accessibility gate.
Validated on this branch:
- 71 focused and integration regression files passed.
- Pairing protocol-v3 flow and 16 transcript-tamper checks passed.
- Static compatibility: 685 passed, 0 failed, 2 skipped.
- Strict live API/peer compatibility: 711 passed, 0 failed, 0 skipped.
- Strict live compatibility: 466 passed, 0 failed, 19 skipped.
- Comprehensive SSD/delta/restore suite: 88 passed, 0 failed; 1,416 file operations and 676 delta chains verified.
- Full production and development dependency audit: 0 vulnerabilities.
- ESLint, frontend production build, backend syntax checks, and editor diagnostics passed.
- Repeatable
npm --prefix app run validategate passed end to end. - Playwright browser/Axe smoke: 5 passed across desktop/mobile, 1 intentionally skipped desktop-only mobile-navigation case.
- Independent final correctness review: no unresolved Critical, High, or Medium findings.
- Final Alpine image built from the workspace lock and passed health, local-auth, schema-25, and graceful-shutdown smoke checks.
- Greenfield Linux/Unraid SSH acceptance passed: clean account/group provisioning, shared root ownership, replaceable and replayed reboot persistence, canonical-root key filtering, scoped rsync, and injected-key/shell/PTY/forwarding/path-escape rejection.
- Greenfield Compose acceptance passed from empty host paths with managed host-key add/revoke, redacted public health, authenticated health details, first-admin bootstrap/login, generic storage roots, pruned dependency fixtures, and no mandatory Docker proxy sidecars.
- Live exact-path Docker proxy acceptance passed: container/network listing, single-snapshot stats, start, and stop work; archive, logs, exec, create, restart, kill, query expansion, and read-proxy mutation are denied. Greenfield Compose also enables both optional sidecars and exercises RedMan's real Dockerode list/start/synthesized-restart/stop flow.
- Public deploy tooling contains no built-in private targets: generic
--customconfiguration and gitignored--profile NAMEfiles are both fail-closed and regression tested. - Playwright page-overflow matrix: 24 route/viewport combinations passed; stacked dialog focus/inert/Escape behavior passed live.
- Native local-auth browser flow: bootstrap, logout, generic failure, login, accounts/audit, viewer restrictions, and recovery UI passed.
- Final security review: 0 Critical, 0 High; all auth release blockers verified fixed.
- Final Alpine image: Argon2id/SQLite loaded; fail-closed startup, direct-spoof rejection, provisioned proxy, and mutation-origin policy passed.
Deferred because they require deployment facts or a separate architectural phase:
- Narrow the production forward-auth allowlist after observing the actual reverse-proxy source on each configured host; SSH key-agent access was unavailable during this tranche.
- Run the committed
redman-backupbootstrap and socket-proxy deployment on a designated canary, then each additional target; real-host SSH verification remains unavailable.
Goal: reduce current exposure without depending on large code changes.
- Determine the actual socket source address RedMan sees for an authenticated Pangolin/Newt request.
- Set
TRUSTED_PROXIESto that exact address or the narrowest required container subnet. - Change an empty
TRUSTED_PROXIESvalue to mean trust none. - Firewall port 8090 so ordinary LAN and site-to-site VPN clients cannot reach it directly. Do not bind to loopback until Newt routing has been verified.
- Keep the health check local and unauthenticated; do not expose administrative routes through the health endpoint.
Done when: a direct request from LAN/VPN with Remote-User: admin returns 401; the same route through an authenticated Pangolin session succeeds; an unauthenticated public request is redirected by Pangolin.
- Force existing
delete_after_importsettings off. - Hide or disable the toggle until Phase 2 has a per-file import/deletion ledger.
- Preserve source media after partial, ambiguous, interrupted, or parser-failed imports.
Done when: no production path recursively deletes recognized media based only on process exit code or aggregate upload count.
- Temporarily disable historical preview, download, and restore for snapshots whose file resolution can traverse multiple later versions.
- Keep backup creation available, but show a clear operational warning in the version browser.
- Take a manual known-good copy of important data before testing the repair.
Done when: RedMan cannot silently return bytes from a revision other than the selected snapshot.
- Do not accept new peer pairings until Phase 3.
- Audit every configured host's
authorized_keysfile for RedMan-added keys and unexpected duplicate/newline entries. - Restrict existing keys by source IP where feasible, or temporarily remove host authorization when Hyper Backup is not running.
- Narrow every existing peer's
allowed_path_prefix; no peer should retain/. - Confirm port 8091 is reachable only over the intended LAN/WireGuard paths, never as a public Pangolin resource.
Done when: existing peer keys and prefixes are inventoried, no default / prefix remains, and new pairing is operationally frozen.
- Back up the live SQLite database, rclone configuration, identity file, SSH material, and deployment configuration out of the RedMan data volume.
- Record current container image IDs and source commit for every deployed instance.
- Pause broad feature work until the Phase 1 harness can detect protected-route failures.
Done when: every deployed instance has a tested rollback bundle and no unreviewed release can bypass the phase gates.
- Direct header spoofing is blocked operationally.
- Delete-after-import is disabled.
- Historical restore cannot silently serve an unverified revision.
- New pairing is paused and existing peer access is scoped.
- Rollback artifacts exist for every configured target.
Goal: ensure subsequent fixes are measured against real authenticated behavior.
- Add
REDMAN_LOCAL_DEV=1alongsideAUTH_DISABLED=trueintest/setup_local_test.sh. - Assert that the production guard still ignores both flags when
NODE_ENV=production. - Add a test for missing identity headers and a test for untrusted identity-header sources.
Done when: protected local test requests succeed through the intended development bypass, while production-mode bypass attempts return 401.
- Separate “route exists” checks from “protected operation worked” checks.
- Require expected success codes and response schemas for authenticated tests.
- Treat unexpected
401,403, HTML error bodies, or skipped assertions as failures.
Done when: deliberately breaking test authentication fails the live suite rather than reporting hundreds of passes.
- Replace duplicated cryptographic primitives in the standalone handshake test with imports from production modules.
- Add tamper cases for instance name, token, role/direction, static key, ephemeral key, callback URL, and SSH public key.
- Preserve a version-compatibility fixture for every accepted handshake version.
Done when: changing any signed transcript field fails validation and the test exercises the same functions used by pairing routes.
- Mark
pre-push.shexecutable or change all documentation and hooks to invoke it withbash. - Add explicit package scripts for backend syntax, integration tests, frontend build, dependency audit, and accessibility/browser smoke checks.
- Add a clean-volume startup test using a temporary
DB_PATH.
Done when: one documented non-interactive command runs all release gates from a clean checkout.
Create deterministic tests for:
- repeated-mutation snapshot restore;
- mixed-success media import with deletion requested;
- direct forged forward-auth headers;
- empty database startup;
- one-byte peer quota;
- symlink prefix escape;
- two-peer shutdown isolation;
- unrelated run lookup;
- manual/manual and scheduled/manual overlap;
- cancel-to-terminal-state behavior;
- Hyper/Rclone empty-source handling;
- online DB backup and staged restore.
Done when: each known defect fails for the expected reason on the reviewed baseline.
- No protected-route test treats
401as a pass. - Handshake tests import production logic.
- All Critical/High findings have a reproducer or an explicit testability exception.
- The frontend production build and backend syntax checks remain green.
Goal: make RedMan's backup, restore, import, and database recovery claims true.
- For a selected snapshot, inspect only version directories newer than that snapshot, in ascending order.
- Stop at the earliest applicable saved copy for each path.
- Apply identical semantics to browse, preview, download, restore, and delta reconstruction.
- Define behavior for deleted, renamed, type-changed, and multiply-modified paths.
Done when: a four-generation fixture restores every generation byte-for-byte, including a file changed in three consecutive runs.
- Keep generated timestamps explicitly UTC or explicitly local, never mixed.
- If timestamp directory names represent UTC, append
Zwhen converting them toDate. - Test retention boundaries across CET/CEST transitions and around midnight.
Done when: displayed snapshot time, retention age, and keyframe age agree in Europe/Amsterdam, UTC, and a DST transition fixture.
- Record one result per source file: uploaded, server-confirmed duplicate, skipped, failed, or unknown.
- Delete only files individually proven uploaded or confirmed duplicate according to an explicit policy.
- Verify asset identity with checksum or stable server asset ID where supported.
- Write a deletion audit before unlinking and retain failed/unknown source files.
- Require a fresh destructive confirmation when enabling the feature.
Done when: a mixed 6-success/4-failure fixture deletes exactly the six proven files and preserves the other four after restart.
- Add idempotent
CREATE TABLE IF NOT EXISTSand index creation for every baseline object before ALTER/seed-dependent migrations. - Keep
seed.jsonly for explicit development/demo reset data. - Ensure startup never drops user data.
- Reconcile the baseline against
contracts/v1.jsonand seed schema.
Done when: an empty volume starts successfully, an existing production DB migrates without data loss, and running startup twice is a no-op.
- Use
better-sqlite3onlinedb.backup()for live backups. - Run
PRAGMA integrity_checkagainst the produced backup. - Stage restore files beside the active DB and write a restart marker.
- On startup, before opening the production DB, atomically swap the staged file and remove stale WAL/SHM companions.
- Preserve the previous DB for rollback until the restored DB passes integrity checks.
Done when: concurrent writes during backup are present in a valid backup and a staged restore survives restart with integrity_check = ok.
- Share mount identity, accessibility, minimum-item, and empty-source checks across SSD, Hyper, Rclone sync, and future destructive jobs.
- Require an explicit per-job opt-in for a genuinely empty source.
- Re-check source health immediately before launching the child process.
- Keep source and destination overlap checks symmetric, including source-inside-another-destination cases.
Done when: an unmounted/empty source cannot delete a populated destination in any engine and intentional empty sync requires explicit confirmation.
- Acquire one normalized, transactional config/job lock for manual, scheduled, retry, and API-triggered execution.
- Normalize lock keys with
Number(configId). - Return
409 Conflictwith the active run ID when a duplicate trigger arrives. - Ensure pruning/version processing shares the same destination lock.
Done when: double-click, concurrent API calls, and scheduler/manual races all produce one child process and one active run.
- Centralize transitions and prevent executors from overwriting
cancelledwithfailed. - Define terminal states once:
completed,partial,failed, andcancelled. - Scope cancel endpoints by feature and run ownership.
- Reuse Rclone's signal-aware handling for rsync paths.
Done when: cancellation remains cancelled, wrong-feature cancellation returns 404, and every run reaches exactly one terminal state.
- Add optional checksum verification after restore and persist the result.
- Keep a durable “last verified” timestamp and result per configuration.
- Add a small scheduled restore-drill mode that restores into an isolated destination and compares bytes.
Done when: the UI and API can prove when a backup was last restored and verified, not merely when it was last written.
- Repeated-mutation restore passes byte-for-byte.
- Mixed-result import cannot delete unproven source files.
- Empty-volume startup and WAL-safe backup/restore pass.
- Every destructive engine blocks unhealthy/empty sources.
- Overlapping runs and non-monotonic terminal states are impossible in tests.
Goal: make every trust boundary explicit and enforce it in both HTTP and SSH transports.
- Rename the middleware and documentation from Authelia-specific terminology to generic forward auth/Pangolin Badger.
- Default
TRUSTED_PROXIESto trust none; require an explicit production value. - Strip identity headers before evaluating them unless the socket source is trusted.
- Add
AUTH_MODE=proxyas an explicit mode; never silently fall back to header trust or development bypass. - Keep direct port 8090 firewalled from ordinary LAN/VPN clients.
Done when: external Pangolin access works, direct forged headers fail from every non-proxy source, and an unset production proxy allowlist fails closed.
- Require an explicit peer backup root during acceptance; never create a peer with
allowed_path_prefix='/'. - Set a conservative default quota rather than unlimited storage.
- Remove
/from automatically advertised/allowed browser roots. - Deny RedMan's data directory,
.ssh, identity, DB, rclone config,/proc,/sys, and other sensitive paths regardless of configured roots.
Done when: accepting a peer cannot browse or prepare outside its dedicated backup root and the admin filesystem picker cannot expose RedMan secrets.
- Resolve configured roots and existing requested ancestors with
realpath. - Reject symlinks that leave the authorized root.
- Create missing path segments one at a time and re-check the final real path.
- Apply the same canonical boundary to browse, prepare, restore, and SSH transport.
Done when: a symlink inside an allowed root pointing to /etc is rejected by every relevant endpoint and transfer path.
- Use a dedicated unprivileged backup account on each NAS.
- Validate submitted public keys as one well-formed supported key line.
- Write
restrict,from=, and a forcedrrsync/wrapper command scoped to the peer root. - Match/deduplicate on key body after options are added.
- Make host-key authorization an explicit, auditable pairing step.
- Remove the writable bind to root's general
authorized_keyswhere possible.
Done when: rsync inside the permitted root succeeds, arbitrary commands/tunnels/PTY fail, newline injection is rejected, and compromise of the RedMan container cannot append a general root key.
Greenfield portability: Complete. scripts/setup-backup-user.sh provisions a clean generic Linux/OpenSSH host without pre-existing users/groups; its root-owned reader emits only canonical installer-approved forced-rrsync entries, while setup-unraid-backup-user.sh adds replaceable and executable reboot persistence. Compose uses explicit host data/storage/media/key paths, runtime storage roots are configurable, and reusable deployment targets live only in gitignored local profile configuration.
- Introduce a new handshake version.
- Canonically encode and sign protocol version, role/direction, token, instance ID/name, ephemeral key, static key, callback URL, SSH key, and requested scope.
- Reject unknown fields or ambiguous canonical encodings.
- Provide a deliberate transition path; never downgrade automatically.
Done when: mutating any transcript field invalidates the signature and legacy peers receive an explicit upgrade response.
- Show the identity fingerprint before acceptance and require explicit out-of-band confirmation.
- Match existing peers by static public key or stable peer ID, never display name.
- Move expiry validation before the
acceptingtransition and force every exception intofailedorexpired. - Generate a fresh token/state for “Try Again”.
Done when: two default-named RedMan instances remain distinct, expired/failed pairings never stick in accepting, and retry cannot reuse consumed state.
- Add stable
peer_idownership to Hyper jobs and runs. - Scope shutdown updates to
req.peer.id. - Scope
/peer/backup/status/:runIdto Hyper runs owned by the requesting peer. - Return
404for unrelated local, feature, or peer runs and redact internal paths from peer-facing errors.
Done when: peer A cannot read or alter peer B, SSD, Rclone, or Media Import run state.
- Include
storage_limit_bytesin the authenticated peer context. - Check both current usage and projected/reserved incoming size.
- Make concurrent prepare requests reserve capacity transactionally.
- Define behavior when usage calculation times out or is unavailable; fail closed for bounded peers.
Done when: a one-byte quota blocks prepare, concurrent reservations cannot exceed quota, and /peer/storage reports the configured value.
- Centralize URL parsing and DNS resolution for pairing, sync, connectivity, test-connection, and callbacks.
- Allow only configured private/VPN ranges and explicit hostname allowlists.
- Validate IPv4 and IPv6 strictly, re-check resolved addresses, and defend against DNS rebinding.
- Constrain callback ports to the expected peer API or an explicit allowlist.
- Add abort timeouts and response-size limits.
Done when: public/cloud-metadata targets, malformed private IPs, rebinding, and arbitrary private ports are rejected while configured VPN peers work.
Implemented evidence: all validated private peer, pairing, callback, connectivity, and discovery requests force redirect: 'error'; callers cannot override it. Numeric private-address and callback policy regressions remain in the focused gate.
- Hash peer API keys at rest and compare hashes; show plaintext only once at creation/rotation.
- Document that identity, SSH, and rclone secrets remain sensitive even at mode 0600.
- Evaluate encrypted storage or host-volume encryption for unattended secrets.
- Remove secrets from process arguments, including the Immich API key; prefer environment/stdin/config file with restrictive permissions.
- Rotate credentials after containment changes.
Done when: a copied database does not reveal live peer API keys and process listings do not reveal the Immich key.
- Replace arbitrary settings writes with a typed key allowlist and per-key validation.
- Put Docker access behind a socket proxy exposing only required endpoints.
- Document that Docker start/stop/create is host-root-equivalent even when the socket mount is read-only.
- Separate read-only monitoring from mutation privileges where practical.
Done when: unknown settings keys return 400, RedMan cannot call unapproved Docker endpoints, and discovery still works within its declared permissions.
Implemented evidence: RedMan rejects raw Docker sockets and uses separate repository-built exact-path sidecars. The read side permits only container/network listing and single-snapshot stats; the control side permits only start/stop, with restart synthesized from filtered state plus those calls. Pure and live-daemon tests deny broader reads and mutations.
- Re-run
npm audit --omit=devagainst the current lockfile. - Upgrade direct dependencies where compatible and document reachability/mitigation for unresolved transitive advisories.
- Add a repeatable audit gate with an explicit, reviewed exception file rather than silently accepting advisories.
Done when: no unreviewed Critical/High production advisory remains and the frontend/backend build and integration suite pass after upgrades.
- Pangolin proxy mode fails closed and direct spoofing is blocked in code and network policy.
- Peers have explicit roots/quotas and cannot escape via symlink.
- SSH grants rsync-only access as a non-root account.
- The full pairing transcript is signed and fingerprint-confirmed.
- Peer A cannot inspect or mutate peer B.
- SSRF/callback probes and dependency gates pass.
Goal: bound memory, I/O, retries, polling, and shutdown behavior for multi-TB use.
- Stop accepting new work, then stop the scheduler.
- Signal children and await
closewith a bounded grace period before final DB transitions. - Flush queued run-file inserts and notification state.
- Exit non-zero after
uncaughtException/fatal rejection so Docker restarts a clean process.
Done when: SIGTERM during transfer leaves consistent terminal state and partial data, while an injected uncaught exception causes a supervised restart rather than continued execution.
- Use
AbortControllerfor every peerfetch. - Set separate connect/response budgets and abort underlying requests during shutdown.
- Classify timeout errors as retryable or terminal consistently.
Done when: a peer accepting TCP but never responding cannot hold a run indefinitely.
- Reuse one itemize/progress parser for local and SSH rsync.
- Insert file events in bounded transactions as output arrives.
- Remove whole-run stdout accumulation and second-pass parsing.
- Keep parser grammar identical for spaces, deletions, and platform variants.
Done when: a 100,000-file Hyper transfer has bounded heap growth and file rows appear incrementally.
- Treat
partialas terminal. - Add lightweight progress-only endpoints without 1,000 file rows or aggregations.
- Use adaptive intervals, suspend polling while the document is hidden, and stop immediately at terminal state.
- Consider SSE using the existing notification transport for active runs.
Done when: terminal runs generate no further polling and active-run requests stay small and bounded.
- Define retention for
backup_runs,backup_run_files, peer audit rows, metrics, and summaries. - Rate-limit
last_seenupdates and successful-auth audit inserts per peer. - Preserve failures/security events longer than routine success telemetry.
Done when: sustained operation reaches a stable database size and active transfer polling does not create constant SQLite writes.
- Use a proven cron parser for validation, description, and next occurrence.
- Reject invalid expressions before persistence.
- Keep one run row across retries or explicitly relate attempts to a parent run.
- Distinguish skip, retry, partial, cancellation, and failure in run history.
Done when: weekly/monthly schedules show the correct next time and one cron tick has one traceable run lineage.
- Remove route-owned unbounded intervals.
- Let
runScanown completion, cancellation, cleanup, and side effects. - Derive file dates from the configured mount path.
- Correlate each import with its own Immich log rather than “most recent log”.
Done when: deleting a drive mid-scan leaks no timer and concurrent imports cannot consume each other's logs.
- Prefer a narrow host-side helper or documented manual eject over granting broad
CAP_SYS_ADMIN. - Hide/disable eject in environments where the capability is unavailable.
- Report a precise unsupported-state message rather than a generic warning.
Done when: the UI never offers a control that is guaranteed to fail in the shipped container.
- Store per-snapshot file count, bytes, delta savings, and tier metadata when a snapshot is created.
- Update aggregates transactionally during prune/rebase.
- Avoid recursive full-tree stats after every backup and list request.
Done when: snapshot listing and post-run accounting scale with snapshot count, not total file count across history.
- Move
verifyDeltaChainout of a synchronous request. - Persist progress/result/cancellation and expose it through the standard run model.
- Clean temporary reconstruction files on completion, cancellation, and restart.
Done when: a long verification returns quickly with a job ID and reports bounded progress without tying up one HTTP request.
- Cache
duresults with a short TTL and invalidate after known writes. - Do not run the same
dutwice in one prepare request. - Bound execution time and surface “usage unavailable” according to quota policy.
Done when: repeated storage requests return from cache and a large tree cannot block the event loop/request worker for 30 seconds each time.
- Give
partialits own title, severity, and body. - Wire cancellation events only after terminal state is committed.
- Ensure notification failures never mutate backup outcome.
Done when: a partial run is not announced as “failed - Unknown error” and cancellation produces one consistent event.
- Shutdown, timeout, retry, and fatal-error fault injection pass.
- Hyper memory remains bounded under a large-file-count fixture.
- Polling and telemetry writes are bounded.
- Long-running verification is asynchronous.
- Snapshot listing/accounting no longer recursively scans all versions.
Goal: ensure visible controls are functional, consequences are clear, and core status is readable on desktop and mobile.
- Catch submit failures in SSD, Hyper, and Rclone forms.
- Render persistent inline summaries and field-level validation.
- Replace empty initialization catches with actionable retry/error states.
- Preserve backend safety messages verbatim where safe.
Done when: an overlap/empty-source validation error is visible in the modal and can be corrected without opening developer tools.
- Provide dialog semantics, labels, focus trap/restoration, Escape, background inertness, and accessible icon names.
- Replace repeated modal markup and browser
alert()/confirm()flows. - Cover delete, restore, pairing, secrets, and destructive toggles.
Done when: keyboard-only and screen-reader smoke tests pass for every destructive or credential dialog.
- Remove fixed/min widths causing Settings and SSD overflow.
- Constrain header title, timestamps, tables, controls, and snapshot labels.
- Test 390 x 844, 768 x 1024, 1024 x 768, and desktop viewports.
Done when: no horizontal page scroll or clipped control remains at the audited mobile/tablet sizes.
- Validate cron on client and server.
- Fix every-eight-hours mapping and preserve minute fields in descriptions.
- Use the same parser/formatter as Phase 4 scheduler logic.
Done when: opening and saving a valid custom schedule is lossless and invalid cron cannot be enabled.
- Show retention controls for every versioned configuration, not only delta-enabled ones.
- Refresh run history immediately when its configuration filter changes.
- Explain whether deleting a config keeps or removes destination snapshots.
Done when: plain-versioning users can inspect/edit active retention and filters apply without a manual refresh.
- Make “Enter URL manually” provide a real manual URL path, or remove it.
- Expose direction, SSH user/host/port only if those modes are supported and safely validated.
- Remove orphaned
showAdvanced, destination suffix, and form state when not shipping those features.
Done when: every visible setup path can produce a valid job and no API-only option is implied by non-functional UI.
- Show last success, last failure, last verified restore, current staleness, and next run.
- Distinguish “backup completed” from “restore verified”.
- Link directly to the relevant run or verification result.
Done when: a user can answer “is this data protected and recoverable?” from the configuration list.
- Wire start, cancellation, and progress notifications into executors with rate limits, or remove their toggles/settings/exports.
- Keep completion/failure/partial semantics aligned across all engines.
- Remove legacy ntfy keys after a documented migration.
Done when: every visible notification control has an executor-level integration test.
- Add UI and documentation for
exclude_patterns, empty-source opt-in, and run-file retention if they remain supported. - Keep dangerous options under an advanced destructive-settings section with consequence text.
- Validate and preview exclude patterns before saving.
Done when: backend-supported safety fields can be inspected and changed through the UI and survive round trips unchanged.
- Display exact source, destination, selected timestamp, expected revision, overwrite behavior, and verification option before restore.
- Explain delete-after-import and job-deletion consequences.
- Keep durable success/failure results instead of transient browser alerts.
Done when: destructive actions require informed confirmation and leave an auditable result.
- Generate fresh pairing state for “Try Again”.
- Shorten/reflow snapshot option labels for narrow controls.
- Use one shared
formatBytesfrontend utility. - Give unfamiliar icon-only actions tooltips and accessible names.
Done when: targeted UI regression tests cover retry, snapshot selection, byte formatting, and icon labeling.
- No visible control is knowingly inert or impossible to complete.
- Validation errors and destructive consequences are visible.
- Mobile/tablet and accessible-dialog checks pass.
- Configuration cards expose backup and restore-verification health.
Goal: complete the currently unfinished native-login capability without weakening hardened proxy mode.
This phase is optional for proxy-only homelab use after Phase 3, but required before direct access to port 8090 is considered a supported deployment mode.
Choose explicitly among:
proxy: Pangolin Badger supplies identity; RedMan trusts only configured proxy sources;local: RedMan owns credentials, sessions, recovery, and authorization;oidc: RedMan validates an external identity provider directly;- a deliberately supported combination with deterministic precedence.
Do not auto-fallback from one mode to another. Document first-run bootstrap, lockout recovery, reverse-proxy interaction, and headless API access.
Done when: the ADR defines trust boundaries, supported modes, migration, recovery, and failure behavior before schema/UI work starts.
- Add users, credential methods, sessions, recovery events, and audit tables through numbered migrations.
- Start with
adminandviewer; require explicit permissions for restore, deletion, peer management, Docker mutation, secrets, and settings. - Map trusted proxy identity to an account only under an explicit provisioning policy.
Done when: an authenticated viewer cannot invoke any mutating or secret-bearing endpoint and every existing route has a declared permission.
- Prefer passkeys/WebAuthn or OIDC where practical.
- If passwords are supported, hash with a maintained Argon2id implementation using per-user salts and current parameters.
- Add rate limiting, generic failure responses, lockout/backoff, secure recovery, and credential rotation.
- Never ship default credentials.
Done when: credential storage, brute-force resistance, recovery, and rotation pass a dedicated security review.
- Use random opaque session tokens stored hashed server-side.
- Set
Secure,HttpOnly, and appropriateSameSitecookie attributes. - Rotate on login/privilege change, enforce idle/absolute expiry, and revoke on logout/recovery.
- Protect state-changing cookie-authenticated requests against CSRF.
Done when: session fixation, replay after logout, cross-site mutation, and expired-session tests fail closed.
- Present first-admin setup only when no account exists and only through a controlled local/bootstrap path.
- Add login, logout, session-expired, credential enrollment, recovery, and account/audit views.
- Preserve return paths without permitting open redirects.
Done when: a clean install can securely establish one admin and all subsequent unauthenticated API/UI requests require login.
- Keep Pangolin Badger compatibility and rename
autheliaAuthto a provider-neutral boundary. - Validate and normalize proxy identity fields; do not grant access from header presence alone.
- Make proxy-only deployments avoid a redundant second login while still applying RedMan authorization roles.
Done when: both proxy and local modes pass the same authorization suite and neither can be activated accidentally.
- Test upgrades from proxy-only databases.
- Test lost credential/recovery paths without DB surgery.
- Test mode changes, disabled accounts, role changes, session revocation, backup/restore of auth tables, and clock skew.
Done when: switching modes cannot create an unauthenticated window or lock out all administrators without a documented recovery mechanism.
- Authentication mode is explicit and fail-closed.
- Native login, sessions, recovery, and route authorization have security tests.
- Proxy mode remains compatible with Pangolin Badger.
- Direct port 8090 is supported only in an authenticated mode and remains firewalled by default.
Goal: remove the duplication and dead paths that allowed behavior to drift.
- After the Phase 2 baseline migration is proven, move remaining inline
db.jsmigrations into numbered migrations. - Keep schema definitions in one authoritative migration path; keep
seed.jsfor data only. - Add a schema snapshot/contract comparison test.
Status: Complete. Migrations 23-25 absorb legacy startup repairs, ntfy aliases, and peer SSH identity/access cleanup; db.js only orchestrates startup, seed.js is data-only, and compatibility inspects an actual migrated database.
- Extract common run list/detail/cancel/trigger validation without hiding feature-specific executor behavior.
- Centralize status transitions, ownership checks, pagination, and progress-only responses.
Status: Complete. runLifecycle.js is feature-scoped and database-injected; executors and notification labels remain owned by their routes.
- Use one parser and one bounded event pipeline for local and SSH rsync.
- Remove unreachable deletion parsing and reconcile platform-specific output through fixtures.
Status: Complete. Local and SSH paths share rsyncOutput.js, bounded 64 KiB tails, chunk framing, deletion/error parsing, and the existing shared process registry.
- Delete abandoned
rebaseDeltas, unused chain helpers/variables/parameters, and stream-of-consciousness comments. - Remove notification exports/settings that Phase 5 intentionally did not ship.
Status: Complete. The abandoned delta implementation and unused helpers/imports are removed. Frozen v1 names remain as compatibility surfaces; Phase 5 shipped and tests all notification controls, so none were removed as inert.
- Resolve
PEER_PORTversusPEER_API_PORTacross code, Compose, Unraid template, deploy script, and README. - Replace unusable
0.0.0.0advertised-host fallback with explicit configuration/discovery failure. - Validate environment variables at startup and fail with actionable errors.
Status: Complete. runtimeConfig.js owns validated listener/SSH values; production requires an explicit reachable PEER_HOST, and code, Compose, Unraid, deploy, local launchers, tests, and docs agree.
- Remove unused
suggestedSuffixplumbing and preview variable. - Remove or use
redact()/redactString(). - Replace constant
execSync('rm -rf ...')with filesystem APIs. - Retire legacy ntfy keys through a migration.
- Centralize duplicated byte formatting.
Status: Complete. Migration 24 preserves and retires legacy ntfy aliases; named UI/redaction/shell leftovers are gone, and frontend byte formatting has one utility.
- Extract networking/state machines, dialogs, and repeated form sections only where it reduces real complexity.
- Preserve existing visual language and API contracts.
- Add component-level tests for extracted workflows.
Status: Complete. The repeated SSD/Hyper/Rclone notification policy is one accessible component with Vite SSR state coverage; broader page-specific workflows remain local.
- Add lint, focused unit tests, integration tests, browser smoke tests, accessibility checks, dependency audit, and clean-volume startup to the documented validation command.
- Keep generated fixtures outside Git history or under explicit size limits.
Status: Complete. npm --prefix app run validate runs lint, 66 regression files, clean startup, compatibility, build, full dependency audit, and Playwright/Axe smoke; generated browser artifacts stay below ignored test/data/.
- One migration path and one run-status model remain.
- Local/SSH rsync use one parser.
- Dead/inert code identified by both reviews is removed or covered.
- Configuration names match every deployment artifact.
- Routine release validation is non-interactive and repeatable.
Gate result: Passed locally. Real-target canary validation, proxy-source observation, restricted host-user bootstrap, and end-to-end Hyper SSH remain deployment evidence, not unfinished code.
| Test | Scope | Required by phase | Pass condition |
|---|---|---|---|
| Forward-auth source | Main API | 0/3 | Direct forged headers fail; authenticated Pangolin request succeeds |
| Production auth bypass | Main API | 1 | Dev flags never disable production auth |
| Empty-volume startup | DB | 1/2 | Clean volume starts twice without seed or error |
| Repeated-mutation restore | SSD versions | 1/2 | Every selected snapshot matches expected bytes |
| DST timestamp retention | SSD versions | 2 | UTC/local display and retention agree across DST |
| Mixed media import | Media Import | 1/2 | Only individually proven files are deleted |
| Empty/unmounted source | SSD/Hyper/Rclone | 1/2 | Populated destination remains untouched |
| Concurrent triggers | All engines | 1/2 | One active child/run per job; duplicate gets 409 |
| Cancel terminal state | SSD/Hyper/Rclone | 1/2 | Final state remains cancelled |
| Online DB backup | SQLite | 2 | Integrity check passes with concurrent writes |
| Staged DB restore | SQLite | 2 | Atomic startup swap succeeds; rollback remains available |
| Pairing transcript tamper | Pairing | 1/3 | Every bound-field mutation rejects |
| SSH command restriction | Hyper | 3 | Rsync allowed; arbitrary shell/tunnel denied |
| Symlink escape | Peer API/SSH | 1/3 | Realpath outside root returns 403 |
| Quota and reservation | Peer API | 1/3 | Limit includes projected/concurrent transfers |
| Cross-peer shutdown | Peer API | 1/3 | Only requesting peer's runs change |
| Cross-peer status | Peer API | 1/3 | Unowned run IDs return 404 |
| SSRF/callback matrix | Pairing/Hyper | 3 | Public, metadata, malformed, rebound, and bad-port targets reject |
| Hyper large file count | Rsync | 4 | Heap remains bounded; file rows stream |
| Stalled peer | Hyper | 4 | Request aborts within configured timeout |
| Fatal/shutdown fault | Process | 4 | State flushes and container restarts cleanly |
| Partial polling | Frontend | 4 | Polling stops at partial; payload remains lightweight |
| Mobile sweep | Frontend | 5 | No horizontal overflow at required viewports |
| Dialog accessibility | Frontend | 5 | Focus, Escape, labels, inert background, restoration pass |
| Native auth modes | Main API | 6 | Proxy/local/OIDC modes are explicit and fail closed |
| Role authorization | Main API | 6 | Viewer cannot mutate, restore, delete, manage peers, Docker, or secrets |
For every phase that changes runtime behavior:
- Confirm no backup/import/verification job is active.
- Back up DB and secret/config volume using the last known-good method.
- Record current image ID, source commit, schema version, and database checksum.
- Deploy to the designated canary only.
- Run the phase-specific smoke/regression gate against the canary.
- Observe one scheduled cycle or 24 hours for lifecycle changes.
- Deploy to each additional target and repeat the gate.
- Roll back image and DB together if a schema/data invariant fails.
- Do not use
--forcedeployment around active destructive jobs except for an explicitly tested shutdown scenario.
Phase 0 complete. Manual use remains supervised; historical restore, delete-after-import, and new pairing stay unavailable.
Phases 1 and 2 complete. Local SSD backup/restore and Media Import satisfy data-integrity tests.
Phase 3 complete. Pairing, SSH, path, quota, status, and shutdown isolation pass adversarial tests.
Phase 4 complete and the relevant Phase 5 error/destructive UX is shipped. Long runs, shutdown, retries, polling, and database growth are bounded.
Phase 6 complete. Only then may direct port 8090 access be documented as supported; proxy mode remains the recommended default.
This map ensures every finding or grouped low-level observation in the two source reviews has a destination.
| Source finding | Plan task(s) |
|---|---|
| Engineering: fresh install crash | P1.4, P2.4 |
| Engineering: parallel migration systems | P2.4, P7.1 |
| Engineering: Hyper stdout buffering | P4.3, P7.3 |
| Engineering: duplicate rsync paths | P4.3, P7.3 |
| Engineering: cross-feature cancellation | P2.8, P7.2 |
| Engineering: peer port env mismatch | P7.5 |
| Engineering: wrong historical revision | P0.3, P1.5, P2.1 |
| Engineering: peer shutdown affects all runs | P1.5, P3.7 |
| Engineering: cancelled becomes failed | P1.5, P2.8 |
| Engineering: partial polling never stops | P4.4 |
| Engineering: wrong weekly/monthly next run | P4.6, P5.4 |
| Engineering: UTC parsed as local | P2.2 |
| Engineering: peers matched by display name | P3.6 |
| Engineering: pairing stuck in accepting | P3.6 |
| Engineering: unreachable deletion parser | P7.3 |
| Engineering: partial notification reported failed | P4.12 |
| Engineering: SchedulePicker rewrites schedules | P4.6, P5.4 |
| Engineering: hardcoded media mount / log collision | P4.7 |
| Engineering: eject impossible in container | P4.8 |
| Engineering: shutdown does not await children | P4.1 |
| Engineering: unsafe live DB restore/backup | P1.5, P2.5 |
| Engineering: manual overlap | P1.5, P2.7 |
| Engineering: retry creates unrelated run rows | P4.6 |
| Engineering: media scan interval leak | P4.7 |
| Engineering: type-sensitive config lock | P2.7 |
| Engineering: uncaught exception continues | P4.1 |
| Engineering: full-tree version stats | P4.9 |
| Engineering: synchronous delta verification | P4.10 |
Engineering: per-request du |
P4.11 |
| Engineering: heavy 1 Hz detail polling | P4.4 |
| Engineering: peer auth writes each request | P4.5 |
| Engineering: inert notification controls | P5.8, P7.4 |
| Engineering: abandoned delta code | P7.4 |
| Engineering: Hyper orphaned state/dead ends | P5.6, P7.6 |
| Engineering: pending safety fields lack UI | P5.9 |
| Engineering: assorted code leftovers | P7.5, P7.6 |
| Engineering: swallowed form errors | P5.1 |
| Engineering: hidden non-delta retention | P5.5 |
| Engineering: invalid cron silently accepted | P4.6, P5.4 |
| Engineering: run filter requires refresh | P5.5 |
| Engineering: browser alerts/confirms | P5.2, P5.10 |
| Engineering: byte-format/retry/snapshot polish | P5.11, P7.6 |
| Engineering: restore tests missing | P1.5, P2.1, P2.9 |
| Engineering: overlap validation gap | P2.6 |
Security: default peer prefix / |
P0.4, P3.2 |
| Security: unrestricted/newline-injectable SSH key | P0.4, P3.4 |
| Security: unrelated run disclosure | P1.5, P3.7 |
| Security: filesystem root guard no-op | P3.2 |
| Security: TOFU fingerprint and incomplete signature | P1.3, P3.5, P3.6 |
| Security: operator-controlled SSRF | P3.9 |
| Security: partial callback validation | P3.9 |
Security: peer-influenced du DoS note |
P4.11 |
| Security: broad/empty trusted proxies | P0.1, P3.1 |
| Security: plaintext secrets and API keys | P3.10 |
| Security: Docker socket root-equivalent | P3.11 |
| Security notes: API key comparison/timing | P3.10 |
| Security notes: Immich key in process args | P3.10 |
| Security notes: arbitrary settings keys | P3.11 |
| Full audit C1: direct forward-auth spoof | P0.1, P1.1, P3.1 |
| Full audit C2: unrestricted root SSH | P0.4, P3.4 |
| Full audit C3: unsigned pairing context | P1.3, P3.5 |
| Full audit C4: wrong historical restore | P0.3, P2.1 |
| Full audit C5: unsafe source deletion | P0.2, P2.3 |
| Full audit H1-H8 | P2.4, P3.8, P3.3, P3.7, P2.7, P2.6, P2.5 |
| Full audit H9: dependency advisories | P3.12 |
| Full audit M1-M9 | P2.8, P4.2-P4.8 |
| Full audit UX1-UX6 | P5.1-P5.3, P5.6, P5.7, P5.10 |
| Full audit T1-T3 | P5.6, P5.8, P5.9 |
| Full audit T4: Docker root equivalence | P3.11 |
| Full audit T5: restore confidence | P2.9, P5.7, P5.10 |
| Full audit structural concerns | P7.1-P7.8 |
| Full audit broken auth harness | P1.1, P1.2 |
| Full audit non-executable pre-push | P1.4 |
| Full audit duplicated handshake tests | P1.3 |
| Full audit missing negative/isolation tests | P1.5, regression matrix |
| Newly classified: native login/roles unfinished | P3.1, P6.1-P6.7 |
A mitigation item is complete only when:
- the original defect has a deterministic regression test;
- the test fails on the reviewed baseline and passes with the fix;
- behavior is consistent across SSD, Hyper, and Rclone where the invariant is shared;
- API/schema/environment contracts and documentation are updated;
- no unrelated working-tree changes were reverted or reformatted;
- canary validation passes before deployment to additional targets;
- rollback was considered and, for schema/data changes, exercised;
- the relevant phase gate is updated with evidence rather than marked complete by assertion.