Ergo keeps history in RAM by default, so it is lost on every restart --
including the ones this stack's updater performs. HISTORY now selects a backend
at first deploy: sqlite (default, a file beside ircd.db, no extra container),
postgres (a pinned container via docker-compose.postgres.yml, loopback-only,
POSTGRES_PASSWORD_FILE so no secret lands in .env), or off. Both SQL backends
need Ergo 2.18.0+, which deploy.sh enforces. HISTORY_EXPIRE (default 30d) sets
retention, because upstream's 1w expire-time DELETES from persistent storage --
persistence with the shipped default would buy only a week.
Ergo opens the history backend only at startup and, unlike MySQL, has no
"after launching the server" guard for sqlite/postgresql: a rehash that enables
one reports success and then silently discards every message. So `ergoctl
history` restarts, and `ergoctl edit` detects a backend change and refuses to
apply it by rehash. The config validator runs with no network by design, so it
neutralises postgresql in its copy and reachability is checked separately.
Audit fixes (six lenses over first-deploy, runtime, lifecycle, security, docs):
- SECURITY (blocker): ircd/ is writable by the container uid while the 15-minute
cert sync and every ergoctl config edit run as root. cp/install/> follow a
symlink, so code execution in Ergo could redirect a root write onto any host
file. All such writes now stage under $STACK_DIR (0700 root) and land via mv
(rename(2) replaces a symlink instead of following it); root reads use cp -P
or refuse. Regression-tested with real symlinks.
- deploy.sh no longer swaps the image when ERGO_TAG changes on a deployed stack
(that bypassed pre-flight, snapshot, user warning and rollback); it points at
`TARGET_VERSION=... ergoctl update update`.
- restore was lossy and could lock you out: it now snapshots the current config
as well as the database, restores message history, re-points the postgres
password and re-hashes the local admin password into the restored config, and
validates before starting.
- `install -d -m` re-modes existing directories: `ergoctl backup /var/backups`
no longer chmods it 0700 root, and the updater no longer re-modes /var/log
(0775 root:syslog on Debian, which rsyslog needs).
- The admin oper password is printed only on a first run at a TTY, so it stays
out of cloud-init serial-console logs.
- A failed update is remembered, so the daily job stops repeating a disruptive
warn/stop/swap/roll-back cycle every night; postgres readiness gates an update
that would otherwise stop a healthy server it cannot restart.
- certsync no longer sends "TLS recovered" for outcomes that synced nothing.
- ergoctl history writes .env only after the restart is healthy, and returns 0.
- CR/LF is stripped before IRC framing, so a multi-line argument cannot inject a
second command; the ntfy token moves out of curl's argv.
- ufw/firewalld are additive, so 6667 is now explicitly revoked when PLAINTEXT=0.
- build.sh refuses to build a deploy.sh whose archive is missing a file the
script reads -- the failure mode that would have shipped a stack aborting on
every host.
Docs corrected against the code throughout, including retention, the pre-connect
account-registration default, encrypted-restore (AGE_IDENTITY), what a re-run
really does to .env, and what the update log does and does not contain.
Verified locally: all six suites pass (config render for each backend against the
real 2.19.1 template, yaml/oper/version/env helpers, the IRC client against a
fake server, and the audit fixes including the symlink escalation). Still not
exercised on a Docker host: the containers, ACME issuance, cert sync and
PostgreSQL itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>