Skip to content

Operating it

The care of a running instance, as distinct from using it: each procedure below was run against a live stack before it was written down.

Backup. The database is the only state; the containers hold nothing worth keeping. One command produces a dated, compressed dump:

docker compose exec -T db pg_dump -U manifest-identity -Fc manifest-identity > manifest-identity-$(date +%Y-%m-%d).dump

The dump contains every import, observation, governance record, campaign, and audit row. It contains password hashes and session token hashes but no passwords and no tokens, because none are ever stored. Store it where the database's readers are the only readers: the observations inside it name every identity in the connected accounts, which is reconnaissance material in the wrong hands.

Restore. Restore replaces the running database. Stop the application first so nothing writes mid-restore:

docker compose stop app
docker compose exec -T db pg_restore -U manifest-identity --clean --if-exists -d manifest-identity < manifest-identity-2026-08-19.dump
docker compose start app

The migration step brings a dump taken by an older schema forward on the next start, and a failed migration stops the start rather than serving the wrong schema.

Verify the backup. A backup that was never restored is a hope. Restore into a throwaway database and count:

docker compose exec -T db createdb -U manifest-identity restore_drill
docker compose exec -T db pg_restore -U manifest-identity -d restore_drill < manifest-identity-2026-08-19.dump
docker compose exec -T db psql -U manifest-identity -d restore_drill -c "select count(*) from observations"
docker compose exec -T db dropdb -U manifest-identity restore_drill

The count matches the live table or the backup is not a backup.

Retention. The record model is append-only by design: observations, governance history, campaign decisions, and audit rows exist to answer questions years later, so the data itself has no deletion schedule inside the application. Retention is therefore a property of the backups: keep daily dumps for thirty days and one dump per month for two years, deleting older ones, which bounds disk while preserving the ability to answer how any decision looked at the time it was made. An instance holding a real organization's data follows that organization's records schedule where it is stricter.

Verify the audit trail. Every audit row carries the hash of its own content and the row before it, so a row altered or removed by an actor with owner access breaks every hash after it, and each campaign evidence export carries the chain head at export time. The walk recomputes every hash and names the first row that fails; with an export's head passed as the anchor, it also confirms the trail still reaches it, which is what catches history rewritten after the export was taken:

docker compose exec app python -m manifest_identity.core.verify_chain --anchor <audit_chain_head from an evidence export>

Keep one evidence export per campaign outside the database; the anchor is only as independent as its copy.

The clean-slate reset, development only, deletes every import, every governance record, and the audit history:

docker compose down -v
docker compose up --build

The container is part of the attack surface

Least privilege applies to the container boundary, not only to code (D-042). Both services run with a read-only root filesystem, no privilege escalation route, bounded memory and processor use, and the database publishes no host port: only the application container can reach it. The application drops every Linux capability, because serving HTTP as an unprivileged user needs none; the database drops everything and adds back only the five its entrypoint uses to take ownership of a fresh volume. Writable paths are in-memory filesystems, so nothing written by an attacker survives a restart.

Every claim above is verifiable against the running stack:

docker compose exec app id                                  # uid=1000(manifest-identity), not root
docker compose exec app sh -c "echo x > /srv/manifest_identity/probe"  # fails: read-only file system
docker compose exec app sh -c "grep CapEff /proc/1/status"  # all zeros
docker inspect manifest-identity-db-1 --format '{{.HostConfig.PortBindings}}'  # map[]

The image itself is built from a digest-pinned base, linted in the pipeline, and the base's operating system packages are scanned on every pull request, blocking on critical findings that have fixes, because there the fix is moving the digest, which a pull request can do.