New deployments/ergo/: the Ergo IRC server behind Caddy for Let's Encrypt TLS and the IRC-over-WebSocket endpoint. Docker rather than a native OpenRC/systemd service, because Alpine's apk ergo trails upstream (3.24 ships 2.18.0 against a 2.19.1 security release) and Debian/Alma package it at all -- so native would mean three install paths plus a per-distro ACME client. Both containers run with network_mode: host. IRC bans, throttling and cloaking key on the client's address, and Docker's userland proxy would hide every IPv6 client behind the bridge gateway; host mode also makes the repo's INPUT firewall genuinely govern 80/443/6697. Caddy reaches Ergo over loopback, which is what lets Ergo honour X-Forwarded-For (proxy-allowed-from defaults to localhost) and mark web sessions secure. - deploy.sh generates ircd.yaml ONCE from the pulled image's own default.yaml (version-matched), rewriting the listeners/websockets blocks wholesale rather than patching lines, then asserts hard post-conditions and validates with `ergo run --smoke` in a throwaway container before anything starts. - update.sh: pinned vX.Y.Z tags, GHSA + "### Security" release-note policies, pre-flight against the new image, user NOTICE + grace, stop-consistent DB snapshot, health check (IRC-level, not a bare TCP connect) and rollback that restores the DB only when the schema actually moved. Compatibility-break releases are held for review. certsync copies Caddy's cert pairwise-atomically and verifies the fingerprint served on 6697 after the rehash. - ergoctl: status/users/logs, validated edit+rehash, oper add/passwd/rm, moderation, backup/restore, cert and update passthrough. Talks IRC to the loopback listener over bash /dev/tcp and strips control characters from replies. - Ergo runs as a non-root system user, read-only rootfs, all caps dropped; Caddy keeps only NET_BIND_SERVICE, with admin API and HTTP/3 off. Reviewed adversarially across six lenses; 20 confirmed findings fixed, notably a dead SIGHUP fallback (`rc=$?` after an `if` is always 0), several `set -e` aborts from non-total pipelines, a release-list cache that only ever populated in a subshell, and re-runs that used shell defaults instead of the deployed .env. Verified locally: bash -n, LF endings, the ircd.yaml render against the real 2.19.1 template in both PLAINTEXT modes, the yaml/oper/version/env helpers, and the IRC client against a fake server (registration, oper, rehash success and 400-failure, control-character stripping, server-down paths). Not yet exercised on a Docker host: the containers themselves, ACME issuance and cert sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
3.7 KiB
YAML
81 lines
3.7 KiB
YAML
# Ergo stack -- Caddy (Let's Encrypt + WebSocket front) + Ergo IRC server.
|
|
#
|
|
# Topology -- BOTH services run in the HOST network namespace (no Docker NAT):
|
|
#
|
|
# IRC clients --> :6697 TLS ----------------------> ergo (sees the real IPv4/IPv6)
|
|
# web clients --> :443 caddy --/webirc--> 127.0.0.1:8097 ergo websocket (X-Forwarded-For)
|
|
# Let's Encrypt --> :80/:443 caddy (ACME); the issued cert is copied into ./ircd for
|
|
# Ergo's :6697 listener by `update.sh certsync` (+ SIGHUP rehash)
|
|
# loopback --> 127.0.0.1:6667 plaintext: compose healthcheck + `ergoctl` ONLY
|
|
# (loopback is exempt from ip-limits/bans and counts as secure)
|
|
#
|
|
# Why host networking: IRC bans, throttling and IP cloaking key on the client's
|
|
# address, and Docker's userland proxy would hide every IPv6 client behind the
|
|
# bridge gateway. Host mode also means the host's deny-by-default INPUT firewall
|
|
# really governs these ports (deploy.sh registers 80/443/6697). Consequences:
|
|
# no `ports:`/`networks:` here (Compose rejects them in host mode), Caddy reaches
|
|
# Ergo over 127.0.0.1, and the host's loopback is Ergo's trust boundary -- see the
|
|
# README ("Security model").
|
|
|
|
name: ergo
|
|
|
|
services:
|
|
# ---------------------------------------------------------------------------
|
|
# Caddy -- obtains/renews the Let's Encrypt cert, terminates HTTPS for the
|
|
# IRC-over-WebSocket path and serves a text landing page. Static config:
|
|
# apply Caddyfile edits with `docker compose restart caddy` (admin API is off).
|
|
# ---------------------------------------------------------------------------
|
|
caddy:
|
|
image: caddy:${CADDY_TAG:-2-alpine}
|
|
container_name: ergo-caddy
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
cap_drop: [ALL]
|
|
cap_add: [NET_BIND_SERVICE] # :80/:443 (root in-container needs only this)
|
|
security_opt: [no-new-privileges:true]
|
|
volumes:
|
|
- ./caddy/etc:/etc/caddy # Caddyfile + conf.d/ (directory mount, not a file)
|
|
- ./caddy/data:/data # ACME account + certs; host-readable for certsync
|
|
- ./caddy/config:/config
|
|
environment:
|
|
ERGO_DOMAIN: "${ERGO_DOMAIN}"
|
|
ACME_EMAIL: "${ACME_EMAIL}"
|
|
NETWORK_NAME: "${NETWORK_NAME}"
|
|
healthcheck:
|
|
# Loopback-only health site defined in the Caddyfile (the admin API is off).
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ | grep -q '^ok'"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ergo -- the IRC server. Runs as the host's `ergo` system user (deploy.sh
|
|
# creates it and chowns ./ircd), read-only rootfs, no capabilities. Every
|
|
# file it needs lives in ./ircd: ircd.yaml, ircd.db, ergo.motd, fullchain.pem,
|
|
# privkey.pem. `init: true` puts tini at PID 1 so `docker compose kill -s HUP`
|
|
# (rehash: reloads config, MOTD and TLS certs) reaches the ergo process.
|
|
# ---------------------------------------------------------------------------
|
|
ergo:
|
|
image: ${ERGO_IMAGE:-ghcr.io/ergochat/ergo}:${ERGO_TAG:-stable}
|
|
container_name: ergo
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
init: true
|
|
user: "${ERGO_UID:-1000}:${ERGO_GID:-1000}"
|
|
cap_drop: [ALL]
|
|
security_opt: [no-new-privileges:true]
|
|
read_only: true
|
|
tmpfs: [/tmp]
|
|
volumes:
|
|
- ./ircd:/ircd
|
|
healthcheck:
|
|
# IRC-level probe: Ergo answers a pre-registration QUIT with "ERROR :Quit"
|
|
# and closes. A bare TCP connect would pass even with a wedged server.
|
|
# busybox nc exits 0 after a timeout, hence the grep.
|
|
test: ["CMD-SHELL", "printf 'QUIT\r\n' | nc -w 3 127.0.0.1 6667 | grep -q '^ERROR'"]
|
|
interval: 60s
|
|
timeout: 6s
|
|
retries: 3
|
|
start_period: 20s
|