Harden for production: fsync writes, deployment + backup tooling
All checks were successful
CI / check (push) Successful in 3m55s
All checks were successful
CI / check (push) Successful in 3m55s
The event log is the account/channel authority, so make it durable: persist() now fsyncs every committed event before reporting success, and a serialisation failure is an error instead of a silently-dropped blank line. Add a hardened systemd unit (scripts/echo.service), an atomic gzip backup script with retention (scripts/backup.sh), and a Deployment wiki page (install, durability, backup/restore, upgrade, monitoring).
This commit is contained in:
parent
7d1edefb70
commit
e551a01cbf
4 changed files with 82 additions and 1 deletions
|
|
@ -64,6 +64,7 @@ The [wiki](https://git.devtronic.pro/fedserv/echo/wiki) has the detail:
|
||||||
- [Configuration](https://git.devtronic.pro/fedserv/echo/wiki/Configuration) — the `config.toml` reference
|
- [Configuration](https://git.devtronic.pro/fedserv/echo/wiki/Configuration) — the `config.toml` reference
|
||||||
- [Directory API](https://git.devtronic.pro/fedserv/echo/wiki/Directory-API) — the gRPC account and channel directory for websites
|
- [Directory API](https://git.devtronic.pro/fedserv/echo/wiki/Directory-API) — the gRPC account and channel directory for websites
|
||||||
- [Writing a Module](https://git.devtronic.pro/fedserv/echo/wiki/Writing-a-Module) — build your own service or ircd link
|
- [Writing a Module](https://git.devtronic.pro/fedserv/echo/wiki/Writing-a-Module) — build your own service or ircd link
|
||||||
|
- [Deployment](https://git.devtronic.pro/fedserv/echo/wiki/Deployment) — running Echo in production: install, durability, backups
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
|
|
|
||||||
30
scripts/backup.sh
Executable file
30
scripts/backup.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Back up Echo's event log. The log is append-only, and compaction replaces it
|
||||||
|
# atomically (temp file + rename), so a plain copy is a consistent snapshot; a
|
||||||
|
# copy taken mid-append has at most a truncated final line, which Echo skips on
|
||||||
|
# load. Run from cron or a systemd timer, e.g. hourly.
|
||||||
|
#
|
||||||
|
# ECHO_DATA_DIR=/opt/echo ECHO_BACKUP_DIR=/opt/echo/backups ./backup.sh
|
||||||
|
#
|
||||||
|
# Restore: stop the service, gunzip a backup over the live log, start again:
|
||||||
|
# systemctl stop echo
|
||||||
|
# gunzip -c /opt/echo/backups/echo.db.<stamp>.jsonl.gz > /opt/echo/echo.db.jsonl
|
||||||
|
# systemctl start echo
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DATA_DIR="${ECHO_DATA_DIR:-/opt/echo}"
|
||||||
|
BACKUP_DIR="${ECHO_BACKUP_DIR:-$DATA_DIR/backups}"
|
||||||
|
KEEP="${ECHO_BACKUP_KEEP:-48}"
|
||||||
|
LOG="$DATA_DIR/echo.db.jsonl"
|
||||||
|
|
||||||
|
[ -f "$LOG" ] || { echo "no event log at $LOG" >&2; exit 1; }
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
dest="$BACKUP_DIR/echo.db.$stamp.jsonl.gz"
|
||||||
|
gzip -c "$LOG" > "$dest.tmp"
|
||||||
|
mv "$dest.tmp" "$dest"
|
||||||
|
echo "backed up $LOG -> $dest ($(du -h "$dest" | cut -f1))"
|
||||||
|
|
||||||
|
# Keep the most recent $KEEP backups.
|
||||||
|
ls -1t "$BACKUP_DIR"/echo.db.*.jsonl.gz 2>/dev/null | tail -n +"$((KEEP + 1))" | xargs -r rm -f
|
||||||
43
scripts/echo.service
Normal file
43
scripts/echo.service
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Systemd unit for running Echo in production. Copy to /etc/systemd/system/,
|
||||||
|
# edit the paths and User, then: systemctl daemon-reload && systemctl enable --now echo
|
||||||
|
#
|
||||||
|
# Assumes a dedicated system user and a data directory holding config.toml and
|
||||||
|
# the event log (echo.db.jsonl, written relative to WorkingDirectory):
|
||||||
|
# useradd --system --home-dir /opt/echo --shell /usr/sbin/nologin echo
|
||||||
|
# install -o echo -g echo target/release/echo /opt/echo/echo
|
||||||
|
# install -o echo -g echo config.toml /opt/echo/config.toml
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Echo IRC services
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=echo
|
||||||
|
Group=echo
|
||||||
|
WorkingDirectory=/opt/echo
|
||||||
|
ExecStart=/opt/echo/echo /opt/echo/config.toml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
# Give in-flight work a moment; the store fsyncs every event so a hard stop
|
||||||
|
# never loses a committed change.
|
||||||
|
TimeoutStopSec=15s
|
||||||
|
|
||||||
|
# Hardening. Echo needs only the network and its own data directory.
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
LockPersonality=true
|
||||||
|
ReadWritePaths=/opt/echo
|
||||||
|
# If you do NOT run a local sendmail (i.e. [email] uses a network relay like
|
||||||
|
# msmtp), you can tighten this to ProtectSystem=strict for a read-only root.
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -762,8 +762,15 @@ impl EventLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist(&self, entry: &LogEntry) -> std::io::Result<()> {
|
fn persist(&self, entry: &LogEntry) -> std::io::Result<()> {
|
||||||
|
// Serialise first, and treat a failure as an error rather than writing a
|
||||||
|
// blank line, which would silently drop a committed change.
|
||||||
|
let line = serde_json::to_string(entry)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
||||||
writeln!(f, "{}", serde_json::to_string(entry).unwrap_or_default())
|
writeln!(f, "{line}")?;
|
||||||
|
// fsync: a committed account/channel change is on disk before we report
|
||||||
|
// success, so a crash or power loss can't lose it.
|
||||||
|
f.sync_all()
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many entries the log currently holds.
|
// How many entries the log currently holds.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue