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>
65 lines
2.7 KiB
YAML
65 lines
2.7 KiB
YAML
# PostgreSQL overlay -- used ONLY when HISTORY=postgres.
|
|
#
|
|
# deploy.sh activates it by writing
|
|
# COMPOSE_FILE=docker-compose.yml:docker-compose.postgres.yml
|
|
# into .env, which docker compose reads by itself. So every `docker compose`
|
|
# command in this stack (up, ps, stop, logs, exec) sees the same set of services
|
|
# with no extra flags -- unlike compose profiles, where a service can be silently
|
|
# absent from one command and present in another.
|
|
#
|
|
# Networking: ergo and caddy run in the HOST namespace, so they cannot use
|
|
# compose's service DNS. PostgreSQL therefore stays on a normal bridge network
|
|
# and publishes only to the host's loopback; Ergo reaches it at 127.0.0.1:5432
|
|
# (datastore.postgresql.host in ircd.yaml). Nothing is exposed off-box.
|
|
#
|
|
# The major version is PINNED. PostgreSQL will not start on a data directory
|
|
# written by a different major version, so `update.sh` never touches this image.
|
|
# Upgrading it is a deliberate dump-and-restore -- see the README ("PostgreSQL").
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:${POSTGRES_TAG:-17-alpine}
|
|
container_name: ergo-postgres
|
|
restart: unless-stopped
|
|
# Loopback only. A Docker-published port bypasses the host INPUT firewall,
|
|
# so the bind address is the real restriction here.
|
|
ports:
|
|
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
|
|
environment:
|
|
POSTGRES_USER: "${POSTGRES_USER:-ergo}"
|
|
POSTGRES_DB: "${POSTGRES_DB:-ergo_history}"
|
|
# The password is read from a file so it never has to live in .env
|
|
# (compose interpolates .env, and a '$' in a password would break it).
|
|
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
|
|
# 17 and below keep the classic layout; set it explicitly so a future
|
|
# image default cannot move the data directory under us.
|
|
PGDATA: /var/lib/postgresql/data/pgdata
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
- ./secrets/postgres.pass:/run/secrets/postgres-password:ro
|
|
cap_drop: [ALL]
|
|
cap_add:
|
|
- CHOWN # initdb/entrypoint fix ownership of PGDATA
|
|
- DAC_READ_SEARCH
|
|
- FOWNER
|
|
- SETGID # the entrypoint drops from root to the postgres user
|
|
- SETUID
|
|
security_opt: [no-new-privileges:true]
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U \"${POSTGRES_USER:-ergo}\" -d \"${POSTGRES_DB:-ergo_history}\" -q"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
|
|
# Ergo must not come up before the database is accepting connections: with
|
|
# persistent history enabled it fails to start if the backend is unreachable.
|
|
# depends_on is orchestration only, so it works across the host/bridge split.
|
|
ergo:
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
|
|
volumes:
|
|
postgres-data:
|