Open Archiver¶
Open Archiver stores an encrypted email archive on TrueNAS, with full-text search and Apache Tika/Tesseract OCR for attachments.
Status¶
This stack is implemented in the current branch under issue #789. Blocked candidate; production adoption is not yet approved. The request to proceed with implementation does not mean vulnerabilities are fixed or production risk has been accepted. Earlier runtime, restore, and publication evidence is recorded below; the latest revisions still require validation, and no TrueNAS rollout has occurred.
Known Security Risk and Adoption Review¶
Published image has fixable HIGH/CRITICAL vulnerabilities
A live Trivy scan of the exact published digest
sha256:b094239f1eff4a02cc85c68a8def7d8838bd70805705c71a85bf09c6c8aee702
reported 199 raw fixable HIGH/CRITICAL entries. This is not a
deduplicated count or an exploitability assessment. Findings include CRITICAL
@casl/ability 6.7.3 (fixed in 6.7.5) and multiple SvelteKit 2.38.1
advisories (fixed in 2.57.1). Do not describe this image as secure.
The 2026-09-27 per-image review in issue #789
covers all eight image inputs, including the derivative, upstream base, and
sidecars, with exact digests, linux/amd64 inventories, Trivy scans and
database timestamps, maintenance/confidence assessments, evidence links,
and remaining gaps. This closes the missing-inventory gap, not the adoption
gate: every image is NOT APPROVED. BusyBox's zero findings came from a
scan with no detected packages; Valkey's application coverage and support
remain unverified. Neither zero count establishes safety.
Decision: dependency remediation stays upstream. This repository will not maintain a patched dependency lock or fork. Before deployment, the operator must record production adoption clearance for every image under the image-adoption policy: verify an upstream-fixed replacement artifact, or explicitly review and document a narrowly justified risk exception. No such exception is recorded here; successful hardening and runtime/restore tests do not substitute for it.
The proposed upstream dependency-reporting issue is deferred as a TODO, not filed. Existing repository tracking remains issue #789; no additional issue is created by this documentation.
Image-bootstrap commit 831954bc4caac4edee212c2bf07cbabca6267c35 records the
published derivative image's provenance. The integration is implemented, with
no TrueNAS deployment; production adoption awaits upstream fixes or explicit,
narrowly reviewed acceptance.
Shared Traefik deliberately does not attach to open-archiver-frontend
or declare it as an external network until the app network exists. All eight
services, including init, migration, and backup, have no Compose profile
and are selected normally. Initial enrollment follows the existing manual
TrueNAS Custom App process after the
activation prerequisites; this is not an
inactive-by-default Compose stack.
Do not improvise host configuration or network changes outside Git.
Evidence and Remaining Acceptance¶
The table records the earlier implementation. It does not validate the
new app-role gate, non-recursive routine init/explicit repair mode, revised
Dockerfile/publication workflow, backup runner/freshness-checker changes,
or Valkey maxmemory behavior under load.
The Compose-pinned derivative is unchanged; the revised image source has not
yet been published or runtime tested. Real Entra configuration and host checks
remain pending.
| Item | Current evidence |
|---|---|
| App source | Exact upstream revision 2082eba984ca771c23c2a7c60fc9284794e24b9b, containing the archive integrity fix |
| Derivative build | Native rootless Podman build passed; runtime identity 3132:3132 loaded sqlite3 and could read application artifacts and all migration files |
| Published image | ghcr.io/devsecninja/truenas-apps/open-archiver:831954bc4caac4edee212c2bf07cbabca6267c35@sha256:b094239f1eff4a02cc85c68a8def7d8838bd70805705c71a85bf09c6c8aee702; anonymously pullable and pinned in Compose |
| Publication checks | Workflow 36323007982 passed the build, 18 supervisor tests, non-root artifact test, and GHCR publication with SBOM/provenance for pushed commit 831954bc4caac4edee212c2bf07cbabca6267c35 |
| Secrets | SOPS-native creation and encrypted-file validation completed for eight generated secrets; values are not reproduced here |
| Stack smoke | Exact published GHCR digest pulled anonymously; full stack recreated healthy in rootless Podman, with init and migration exiting 0 |
| Runtime controls | App verified as 3132:3132, CapEff=0, NoNewPrivs=1; observed pids.current was 68/100 for the app and 55/100 for Tika |
| Filesystem and database controls | App root-filesystem write failed with EROFS; archive hardlink succeeded; app database role verified with rolsuper=false, rolcreatedb=false, rolcreaterole=false |
| Repeat startup | Init succeeded repeatedly; restarting the original test stack preserved its email and search index |
| Provisioning tests | All 26 tests passed, including the new UID 3132 registry case; pre-existing Memos helper warnings remain |
| Application smoke | Real upstream schema migrated; first administrator created; second setup returned 403, unauthenticated access returned 401; ZIP containing MIME EML and a text attachment ingested and indexed exactly one email |
| OCR smoke | A generated PNG containing ARCHIVE OCR 1420 was recognized through the app-to-Tika isolated parser network |
| Backup | After queue drain and clean app exit 0, mapped-identity backup ran one PostgreSQL and one Redis job exactly once, exiting 0; both GPG+ZSTD dumps passed SHA1 verification |
| Backup permissions | Verified output directory 3132:3132, mode 0700; fresh dump mode 0600 |
| Recovery | Synthetic PostgreSQL, copied encrypted archive, and Valkey recovery passed on rootless Podman AMD64; exact outcomes |
| Search rebuild | Emptied Meilisearch documents to 0; POST reindex-all with mode=full rebuilt 1 document from preserved archive data; database email count remained 1 |
| HTTPS proxy boundary | Earlier router labels and repository middleware passed real HTTPS tests with upstream Traefik 3.7.10 and only Forward Auth replaced by a synthetic responder; this did not test the new role gate; exact scope and responses |
| Production gate | Not approved: known fixable HIGH/CRITICAL dependencies require an upstream-fixed artifact or explicit, narrowly reviewed operator risk acceptance; the raw scan count is not a deduplicated or exploitability assessment |
| Further validation | After clearing the security gate: actual Entra login, TrueNAS rollout, MFA, mailbox-provider integration, UI download acceptance, and load testing |
These synthetic results used no production credentials and are not host
deployment or storage-layer recovery evidence. Publication and full-stack
startup passed for the earlier pinned artifact, not the revised source.
The originally selected release v0.6.0 predates the integrity fix; it is
not a recommended downgrade or fallback.
The current workflow gives the image test job read-only repository access.
A separate publish job depends on it and can write to GHCR only on pushes
to main, not pull requests or feature branches. That job rebuilds the image,
then is configured to pull and run the exact pushed digest, checking UID
3132, native sqlite3 loading, and artifact readability before recording
the deployment reference. Passing the test job's local image checks alone
does not prove the separately rebuilt published artifact works.
Publication does not update the Compose pin or enable deployment. The unchanged
bootstrap digest predates these source revisions; the adoption follow-up must
review and pin a current-source, published and digest-verified derivative,
then complete runtime acceptance before activation. Do not treat the old pin
as evidence that the revised source has been published or verified.
Why¶
- Keep archived messages and attachments in private local storage rather than depending solely on the source mailbox.
- Search messages and OCR-extracted attachment text.
- Apply existing Traefik Forward Auth in front of separate local app authentication and MFA.
Encryption at rest is not end-to-end protection from the server: the app must decrypt content, Meilisearch stores searchable plaintext, and temporary imports/parser processing can expose plaintext locally.
Compose Files¶
These source links target the repository's default branch.
Access¶
| URL | Authentication | Purpose |
|---|---|---|
https://open-archiver.${DOMAINNAME} |
chain-auth@file, then app-local open-archiver-access role gate, then local authentication and MFA |
Web UI and API after reviewed activation |
https://open-archiver.${DOMAINNAME}/setup |
Same chain and role gate; assign only the bootstrap administrator before Custom App creation | First-run administrator setup |
The existing middleware uses
ItalyPaleAle's Traefik Forward Auth,
not Authelia. Built-in Open Archiver SSO is not available in the OSS edition;
the forward-auth session does not replace the local account or its MFA.
Every request on the app router passes chain-auth@file and then the
app-local Forward Auth condition Role("open-archiver-access"). A missing role
denies access, including GET /setup and POST /api/v1/auth/setup.
The shared main portal and access to other services are unchanged.
No host ports are published, and there is no public, mobile, API, or monitoring
route bypass. Having no Gatus integration or unauthenticated monitoring router
is intentional; in-container health checks remain enabled. Adding any route,
including monitoring, requires review.
Complete the bootstrap role prerequisite before
creating the Custom App or exposing its route.
Architecture¶
Services¶
All image references below are digest-pinned in the source files.
| Container | Image | Role |
|---|---|---|
open-archiver |
Derivative GHCR image; base docker.io/logiclabshq/open-archiver:2082eba |
Node supervisor plus API, frontend, ingestion worker, indexing worker, and scheduler |
open-archiver-db |
docker.io/library/postgres:17.11-alpine3.24 |
PostgreSQL metadata, users, and encrypted mailbox credentials |
open-archiver-db-backup |
docker.io/nfrastack/db-backup:4.9.2 |
One-shot PostgreSQL and Valkey backup |
open-archiver-init |
docker.io/library/busybox:1.38.0 |
Validate required settings, create private runtime paths, assign ownership, and exit |
open-archiver-meilisearch |
docker.io/getmeili/meilisearch:v1.38.2 |
Derived full-text search index |
open-archiver-migrate |
Same derivative image as the app | One-shot node /app/packages/backend/dist/database/migrate.js after PostgreSQL is healthy |
open-archiver-tika |
docker.io/apache/tika:3.2.2.0-full |
Attachment parsing and Tesseract OCR |
open-archiver-valkey |
docker.io/valkey/valkey:8.1.10-alpine3.24 |
Authenticated Redis-protocol queue and transient MFA state |
flowchart TD
Init["Init: validate and pre-own data"] --> DB["Healthy PostgreSQL"]
DB --> Migrate["Migration one-shot completes"]
Init --> Stores["Healthy Valkey and Meilisearch"]
Migrate --> App["App: five direct Node children"]
Stores --> App
Tika["Healthy Tika OCR"] --> App
App --> Backup["One-shot backup after app health"]
The Dockerfile uses pnpm install --frozen-lockfile --prod at build time,
allowing the esbuild and sqlite3 native build steps. Its sqlite3 check
uses an absolute module path while the working directory remains /app.
The final image uses
the dedicated app identity; runtime does not install dependencies or modify
tracked configuration. start.mjs launches direct Node processes rather than
package-manager wrappers. Any unexpected child exit stops the group. Migrations
are separate, so the app cannot serve before successful schema migration.
PostgreSQL reads config/init-database.sql, mounted read-only at
/docker-entrypoint-initdb.d/10-open-archiver.sql. Native psql \getenv
reads OPEN_ARCHIVER_DB_PASSWORD into the SQL variable; no shell wrapper is
used. The hook creates the openarchiver role with NOSUPERUSER,
NOCREATEDB, and NOCREATEROLE, grants ownership of the openarchiver
database, and revokes public database access and public schema creation.
The database container uses ${POSTGRES_ADMIN_PASSWORD} for its administrator;
init also receives it to validate that it is populated. The app, migrations,
and backup receive only ${POSTGRES_PASSWORD} for the app database role.
The hook runs on a fresh PostgreSQL data directory; changing its file or a
password environment value does not retroactively modify an existing role.
The PostgreSQL health check authenticates over TCP as openarchiver against
its database using OPEN_ARCHIVER_DB_PASSWORD, executes SELECT 1, and
requires exactly 1 in the output. This checks app-role authentication and
database access after bootstrap, rather than merely server availability via
pg_isready. It does not prove migrations or the full application work.
Valkey's authenticated valkey-cli ping health check uses
REDISCLI_AUTH=${REDIS_PASSWORD} and requires PONG.
Image tradeoffs:
- Upstream PostgreSQL Alpine supports
/docker-entrypoint-initdb.d; DHI PostgreSQL does not process the required role-bootstrap hooks. - DHI Valkey was found in the catalog, but an authenticated pull failed in the local validation environment. Upstream Alpine was selected so local tests and CI can use the same verifiable pinned image; this forgoes the hardened-image preference, not Valkey functionality.
- Tika's full variant is the explicitly selected OCR-capable image, rather than the smaller non-OCR variant.
Networks¶
| Network | Members | Access |
|---|---|---|
open-archiver-backend |
App, migrations, PostgreSQL, Valkey, Meilisearch, backup | Internal only |
open-archiver-frontend |
App only; shared Traefik attachment deferred | App egress to mailbox providers; HTTPS requires reviewed activation |
open-archiver-parser |
App and Tika only | Internal parser traffic; no direct internet route |
After the reviewed network activation, Traefik forwards to the frontend on
port 3000; the API uses
PORT_BACKEND=4000. PostgreSQL, Valkey, Meilisearch, and Tika endpoints are
internal, not host-published. Tika receives no archive mount or application
secrets and cannot directly reach backend services. A parser compromise
can still reach its app peer; network separation is not complete containment.
See network and access model.
Identity and Hardening¶
| Component/path | Configured UID:GID | Mechanism |
|---|---|---|
| App, migrations, Meilisearch | 3132:3132 |
Direct user:; svc-app-open-archiver |
| Backup process | 3132:3132 |
Pre-init creates/verifies archivebackup; DBBACKUP_USER/DBBACKUP_GROUP select it |
./data/archive, ./data/scratch, ./data/meilisearch |
3132:3132 |
Pre-owned by init |
./data/postgres |
70:70 |
Pre-owned by init; direct PostgreSQL identity |
./data/valkey |
999:1000 |
Pre-owned by init; direct Valkey identity |
| Tika | 35002:35002 |
Direct parser identity; tmpfs-only writable storage |
The dedicated TrueNAS account has matching primary UID/GID, no shared groups,
and admin_group_member=false. See Open Archiver Identity.
Routine init explicitly sets OPEN_ARCHIVER_REPAIR_PERMISSIONS=false and
uses umask 077. It creates and assigns ownership/modes only on the five
runtime directory roots listed above, applying u=rwX,g=,o=, and sets
./data itself to mode 0700. It does not traverse existing archive or
database trees and never chowns or writes ./config.
Restored descendants require the explicit, operator-only
permission repair procedure with all
writers stopped; an ordinary restart does not repair nested files.
Required variables still reject empty/sentinel values, and both encryption
keys must be 64 hexadecimal characters encoding 32 bytes.
Every container sets no-new-privileges, drops all capabilities, and has
pids_limit: 100. Init adds only CHOWN, FOWNER, and DAC_OVERRIDE.
All roots are read-only except the approved one-shot backup exception:
nfrastack /init writes /etc/bash/bashrc, and read-only startup failed.
This configuration exception is not approval to adopt the backup image.
The backup adds only CHOWN, DAC_OVERRIDE, FOWNER, SETUID, and SETGID
for path setup and privilege dropping. A successful dump did not prove correct
ownership: the earlier Open Archiver configuration's USER_DBBACKUP and
GROUP_DBBACKUP were ignored, leaving output owned by the image-default
identity. The current idempotent CONTAINER_INIT_PRE_COMMAND uses the verified
image-provided adduser/addgroup tools to create archivebackup and assert
its UID and primary GID before DBBACKUP_USER/DBBACKUP_GROUP select it.
The mapped-identity synthetic run verified output directory ownership
3132:3132 with mode 0700 and a fresh dump with mode 0600.
It mounts only backup output, not archive/database data or the Docker socket.
Runtime Settings¶
| Setting | Configured value |
|---|---|
| App memory | ${MEM_LIMIT:-4096m} |
| PostgreSQL memory | ${DB_MEM_LIMIT:-1024m}; shared memory 128mb |
| Valkey container memory | ${VALKEY_MEM_LIMIT:-512m} |
| Valkey maxmemory | --maxmemory ${VALKEY_MAXMEMORY:-192mb} |
| Meilisearch memory | ${MEILI_MEM_LIMIT:-1024m}; indexing memory 512Mb, two indexing threads |
| Tika memory | ${TIKA_MEM_LIMIT:-1536m}; -Xmx512m -XX:ActiveProcessorCount=2 per each of two JVMs; OMP_THREAD_LIMIT=2 |
| Backup memory | ${BACKUP_MEM_LIMIT:-512m} |
| Init / migration memory | 64m / 512m |
| Import body limit | BODY_SIZE_LIMIT=100M |
| Archive policy | ENABLE_DELETION=false, ALL_INCLUSIVE_ARCHIVE=false, ARCHIVE_DRAFTS=false |
| Synchronization | SYNC_FREQUENCY=*/5 * * * * |
| Concurrency | Ingestion worker, ingestion email, and indexing worker concurrency each 2 |
| Indexing | Worker heap 1024 MiB; Meilisearch batch 100, chunk 10 |
| Local storage | STORAGE_TYPE=local, STORAGE_LOCAL_ROOT_PATH=/archive |
| Valkey persistence | RDB --save 300 1; AOF enabled with everysec; noeviction policy |
| Sessions | JWT_EXPIRES_IN=1d |
| Shared environment | All eight containers include ../shared/env/tz.env |
Valkey's default maxmemory is 192 MiB, leaving 320 MiB within the
default 512 MiB container limit for allocator overhead, AOF buffers, and
fork copy-on-write. This reserve is not an OOM guarantee: maxmemory does
not cap total process or cgroup memory. With noeviction, reaching the limit
rejects new writes that require memory rather than evicting queue entries;
producers must handle those errors.
Tune VALKEY_MAXMEMORY and VALKEY_MEM_LIMIT together, keeping both positive
and leaving headroom below the container limit. Setting VALKEY_MAXMEMORY=0
disables the Valkey limit; it is not a safe way to resolve write rejections.
Monitor INFO MEMORY, especially used_memory_rss and
mem_not_counted_for_evict, plus cgroup peak memory under representative
imports, queue load, RDB snapshots, and AOF rewrites before accepting the
limits. See Valkey memory and eviction guidance.
These are initial limits, not validated capacity guarantees. Test representative
imports and OCR under the 100-task caps before accepting them. The app health
check requests /api/v1/auth/status through the local frontend; it does not
prove ingestion, indexing, MFA, or end-to-end archive integrity.
Persistent State¶
| Host path | Container path | Classification and recovery |
|---|---|---|
./backups/db-backup |
/backup |
Encrypted database/queue dump output; 48-hour local retention |
./data/archive |
/archive |
Critical encrypted messages/attachments; independently back up at storage layer |
./data/meilisearch |
/meili_data |
Rebuildable sensitive searchable plaintext; full reindex after loss |
./data/postgres |
/var/lib/postgresql/data |
PostgreSQL database; portable recovery from DB01 dump |
./data/scratch |
/tmp in app |
Regeneratable disk-backed imports/temp files; may contain sensitive plaintext or in-flight work |
./data/valkey |
/data in Valkey |
Persistent queue and transient MFA state; DB02 RDB backup plus storage-layer persistence |
The SQL hook at ./config/init-database.sql is mounted read-only. Tika uses
tmpfs scratch, not persistent archive storage. Do not delete scratch while
imports/workers are active: disable sources, drain work, and stop the app before
reviewed cleanup. Do not expose runtime paths to shared groups or SMB.
Secrets¶
secret.sops.env is decrypted to ignored .env by dccd. The eight generated
values have already been created and validated through native SOPS; do not
regenerate them during deployment or recovery.
| Variable | Classification | Purpose |
|---|---|---|
DB_ENC_PASSPHRASE |
Generated, 36 random bytes encoded as hex | GPG backup encryption |
DOMAINNAME |
Existing static value inherited from app configuration; not generated | HTTPS URL, origin, and router hostname |
ENCRYPTION_KEY |
Generated, 32 random bytes encoded as 64 hex characters | Encrypt stored source credentials/application secrets |
JWT_SECRET |
Generated, 36 random bytes encoded as hex | Sign local authentication tokens |
MEILI_MASTER_KEY |
Generated, 36 random bytes encoded as hex | Authenticate to Meilisearch |
POSTGRES_ADMIN_PASSWORD |
Generated, 36 random bytes encoded as hex | PostgreSQL administrator only; never passed to app, migrations, or backup |
POSTGRES_PASSWORD |
Generated, 36 random bytes encoded as hex | Nonsuperuser app database role |
REDIS_PASSWORD |
Generated, 36 random bytes encoded as hex | Valkey/Redis-protocol authentication |
STORAGE_ENCRYPTION_KEY |
Generated, 32 random bytes encoded as 64 hex characters | Encrypt local archived files |
Mailbox OAuth/IMAP credentials are user-supplied through the app UI,
not generated infrastructure secrets. Select only the provider permissions
needed for the chosen source and mailbox scope. No ADMIN_* or
DISABLE_SIGNUP environment variables are configured: create the first
administrator through /setup and verify setup locks afterward.
Preserve the original ENCRYPTION_KEY and STORAGE_ENCRYPTION_KEY with every
recovery plan. Replacing them is not a rotation procedure for existing
encrypted data. Store recovery access and the backup passphrase independently
of the NAS; never paste secret values into commands, logs, or issues.
First-Run Setup¶
Operator production acceptance required
Production rollout is blocked by the per-image decisions in Known Security Risk and Adoption Review until every image is cleared through verified upstream fixes or an explicit, documented, narrowly justified exception. Implementation approval alone is not risk acceptance. Do not execute the production rollout commands below until the activation prerequisites are complete. Remediation remains upstream; do not maintain a patched dependency fork here.
TrueNAS Enrollment¶
Open Archiver has no Compose profile: normal service selection includes all
eight services. In TrueNAS mode, dccd.sh -t skips the app while
/mnt/.ix-apps/app_configs/open-archiver/versions is absent. Manual Custom App
creation controls initial enrollment; it does not grant image-adoption or
vulnerability-risk approval. While adoption remains blocked, runtime
acceptance must be separately approved and isolated with synthetic data.
The enrollment guard is TrueNAS-specific
Generic/unscoped dccd runs outside TrueNAS mode and raw Compose do not check
TrueNAS enrollment. They select this stack even before its Custom App exists.
Use the sourced TrueNAS aliases and app-first rollout below, not generic
discovery. No profile flag or COMPOSE_PROFILES setting is required.
Activation Prerequisites¶
Publish the current-source derivative without activating it and require the publish job's checks against its exact pushed digest to pass. Review and record adoption clearance for every selected digest, and repeat isolated runtime acceptance of the image, role gate, init/repair, backup, and Valkey memory changes. The earlier smoke evidence does not cover these revisions. No TrueNAS rollout or real Entra authorization check has been verified.
Before production onboarding, pin the approved, current-source, published and digest-verified derivative in Compose through the normal reviewed change process; retaining the earlier bootstrap digest does not qualify. Complete the administrator-only Entra role assignment before Custom App creation.
Shared Traefik's network entries remain deferred to avoid missing-network
failures. During the existing manual rollout below, first let the Custom App
create open-archiver-frontend, then add the tracked Traefik attachment and
external declaration through normal review before the final dccd-all.
Coordinate scheduled full redeploys during onboarding so they cannot apply
the new dependency before that network exists. Do not manually connect
networks or create untracked Compose overrides.
Bootstrap Access Role¶
Before any TrueNAS Custom App creation or route exposure:
- In Microsoft Entra ID > App registrations, select the registration
identified by
${AZURE_CLIENT_ID}in the existing Forward Auth configuration. Under App roles, create an enabled role with allowed member types Users/Groups and value exactlyopen-archiver-access. - In the corresponding Enterprise application > Users and groups, assign that role only to the intended bootstrap administrator. Do not assign a broad group before setup, or change the shared portal's policy/global assignment requirements to restrict this one app.
- Use fresh sign-ins and fresh Forward Auth cookies for acceptance checks; existing sessions may retain earlier role claims. After routing is active, perform the denial/admission checks in rollout step 5 before creating the local administrator. No Entra role or assignment has been changed by this repository update; those actions and real-provider checks remain pending.
See Microsoft's app-role creation and assignment procedure
and the pinned Forward Auth authorization conditions.
Forward Auth maps Entra roles into the profile's Roles; the app-local
Role("open-archiver-access") condition checks that value. Keep this gate
after bootstrap and assign it only to approved archive users after local
setup is locked and MFA is enabled.
Rollout After Approval¶
Follow the existing TrueNAS onboarding sequence after image and runtime clearance. The include-only Custom App YAML needs no profile settings.
On svlnas, the aliases must already be sourced from
/mnt/vm-pool/apps/scripts/aliases.sh.
- Pull the merged changes and decrypt secrets with the standalone app-scoped alias:
The missing TrueNAS Custom App configuration causes an expected
deployment skip on this first pass. Do not start with dccd-all:
the frontend network must exist before Traefik joins it.
2. From the updated checkout, provision the registry-declared account, group,
and child dataset:
The helper preserves checked-out files and does not grant administrative
app-group membership.
3. Confirm the bootstrap role and administrator-only assignment above are in
place. Create a TrueNAS Custom App named open-archiver using:
Confirm the app has created open-archiver-frontend. Then add that network
to both the Traefik service's attachments and the external declarations in
tracked services/traefik/compose.yaml through the normal reviewed change
process. Coordinate automation so Traefik uses those entries only after
the network exists.
4. Run the canonical final deployment:
This applies the app and dependent DNS/Traefik integration through normal
ordering, decrypts secrets, and performs the default backup freshness check.
Confirm init and migrations exit successfully, the app and dependencies
are healthy, and the backup exits successfully with both DB01 and DB02
artifacts. Do not treat directory-only backup health as proof of a dump.
5. With separate fresh sign-ins/cookies, verify that unauthenticated requests
and an authenticated user without the role cannot reach the app, including
GET /setup and POST /api/v1/auth/setup. Verify the assigned bootstrap
administrator passes the role gate. With a cold browser cache, verify that
initial SvelteKit assets load and login/setup navigation completes without
HTTP 429 responses or login/redirect loops.
If any unauthorized request passes,
stop the Custom App and correct the reviewed configuration before proceeding;
do not bypass the gate. Only then create the local administrator at
https://open-archiver.${DOMAINNAME}/setup, enable MFA, save recovery codes
securely, and confirm setup is locked, including rejection of a second
setup submission. Do not widen role assignments until those checks pass;
retain the role gate for approved archive users thereafter.
6. Add a least-privilege mailbox source through the UI. Test a small import,
OCR of a representative attachment, full-text search, and downloads of
original messages/attachments. Verify that unauthorized UI/API requests
remain gated and no host-port or alternate route bypass exists.
7. Verify actual child-dataset snapshot/replication/off-site coverage and
complete the coordinated recovery test.
Record host evidence before describing the service or its backup as deployed
successfully.
Database Backup¶
| Property | Configuration |
|---|---|
| Image | Digest-pinned docker.io/nfrastack/db-backup:4.9.2 |
| Execution | Inline Bash runner: backup01-now now, then backup02-now now; MODE=MANUAL, MANUAL_RUN_FOREVER=FALSE; internal scheduling/notifications disabled |
| Cadence | Intended nightly host dccd run, plus full dccd-all deployments; no in-container nightly timer |
| DB01 | PostgreSQL openarchiver, backed up as nonsuperuser openarchiver |
| DB02 | Valkey via Redis protocol, including queue and transient MFA state |
| Compression / checksum | ZSTD / SHA1 sidecars |
| Encryption | GPG using ${DB_ENC_PASSPHRASE} |
| Retention | DEFAULT_CLEANUP_TIME=2880 minutes (48 hours) |
| Output | ./backups/db-backup |
| Normal dependency | Healthy app, after migrations |
| Acceptance | Default dccd-all backup freshness check with dccd.backup-jobs=01,02; verify both database artifacts |
The runner attempts both jobs exactly once, in order, even if DB01 fails.
It emits an ERROR diagnostic for each nonzero job-command exit and returns
nonzero if either command does. This replaces the pinned nfrastack 4.9.2
image's combined backup-now wrapper, which does not aggregate per-job
failures. This is local invocation and error handling, not an upstream image
fix or an application dependency fork.
The expected-job label requires exactly one successful completion for each of
01 and 02 in current-run logs. The
freshness checker rejects any nonzero or malformed
job completion even if another succeeds; invalid labels and missing or duplicate
expected completions also fail. Known dump or output-move error diagnostics
also fail the check, even if all completion records report 0. ANSI SGR resets
after numeric exit codes remain compatible with completion parsing.
Deployment commands and the operator-only one-off invocation are unchanged:
the service invokes the runner automatically.
Actual frequency follows the host dccd cron; the generic repository example runs forced deployments every 15 minutes. Confirm the intended nightly schedule rather than assuming Compose enforces it.
These dumps are not full-archive atomic backups. They do not include
./data/archive, and DB01/DB02 are separate engine recovery points.
The full child dataset also needs independently verified vm-pool snapshots,
replication, and encrypted off-site coverage.
For a coordinated checkpoint: disable sources and prevent writes, drain queues
before stopping the app, leave PostgreSQL/Valkey running, run the one-shot
backup with --no-deps, and capture matching quiesced archive/dataset state.
The upstream worker can force-exit after five seconds; the supervisor's
20-second deadline and Compose's 30-second grace period do not guarantee drain.
Follow Restore Open Archiver for the exact operator-only backup invocation and recovery sequence: restore a fresh owned database and matching archive with original keys, recover only matching queue state or deliberately reset/reconcile it, and fully reindex after Meilisearch loss. The 2026-09-27 synthetic PostgreSQL, copied encrypted archive, and Valkey recovery passed on rootless Podman AMD64, including both dump checksums and the corrected backup permissions. See the exact synthetic outcomes and remaining checks. No production credentials were used. These results predate the per-job runner and stricter freshness checker; no real-container backup run or new restore is recorded for those changes. The revised init/repair behavior, target-host deployment, and storage-layer recovery also remain unverified by those results.
Upgrade Notes¶
- Known security risk: the current published digest has fixable HIGH/CRITICAL dependencies. Prefer verified upstream fixes; production adoption otherwise requires an explicit, narrowly reviewed operator exception. Do not maintain downstream patched dependency locks or forks. Completing the implementation or passing runtime/restore tests does not waive this review.
- Keep the base revision, derivative build, and published Compose digest
coordinated; confirm publication and acceptance of each new digest.
The fixed revision is
2082eba984ca771c23c2a7c60fc9284794e24b9b;v0.6.0predates the archive integrity fix and is not a safe rollback recommendation. - Before upgrades, create and verify a coordinated checkpoint, preserving the current image identifiers and original encryption keys.
- Review schema and dependency changes; let the separate migration one-shot finish before serving traffic. Image rollback alone does not undo migrations.
- Retest direct-process startup, native
sqlite3loading, resource limits, OCR/import/search/download, authentication/MFA, and backup/restore against the exact published image. A successful build is not runtime acceptance. - Retest the role gate with fresh sessions for an assigned and a non-assigned
user, including both setup routes. Test routine non-recursive init and
explicit restored-tree permission repair separately. Include a cold-cache
browser load: initial SvelteKit assets and login navigation must complete
without HTTP
429responses or login/redirect loops. - Review PostgreSQL major-version and Meilisearch upgrade requirements separately. See Database Upgrades. Changing a PostgreSQL environment password does not rotate an existing role.