diff --git a/CHANGELOG.md b/CHANGELOG.md index a0016cd..bebe09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **`install.sh` now repairs data-dir permissions on every run.** + Re-running the installer reasserts the canonical ownership + (`quptime:quptime`) and modes across `/etc/quptime/` — `0750` on + the dir, `0700` on `keys/`, `0600` on `node.yaml`, `cluster.yaml`, + `trust.yaml`, and `keys/private.pem`, `0644` on `keys/public.pem` + and `keys/cert.pem`. Makes the installer the one-step recovery + path when something has tampered with modes (e.g. a stray + `chmod -R`, a backup restore, or an accidental `sudo qu init` + that left files owned by root). Unknown files in the dir are left + alone. + ## [v0.1.0] — 2026-05-15 ### Changed diff --git a/docs/installation.md b/docs/installation.md index a92bca1..e8a2362 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -70,6 +70,15 @@ What it does: `/etc/systemd/system/quptime.service` (hardened — matches the unit in [systemd.md](deployment/systemd.md)). Enables but does not start the service, so you can configure identity before first boot. +5. Repairs ownership and modes under `/etc/quptime/` to the canonical + layout (`0750` on the dir, `0700` on `keys/`, `0600` on + `node.yaml` / `cluster.yaml` / `trust.yaml` / `keys/private.pem`, + `0644` on `keys/public.pem` / `keys/cert.pem`). This makes the + installer idempotent for permission damage — if something + tightened or loosened modes (a stray `chmod -R`, a misguided + backup restore, an accidental `sudo qu init`), re-running + `install.sh` puts everything back without touching the contents + of those files. ## Build from source diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e4a72cd..fa83125 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -172,7 +172,9 @@ still see this error, the most likely causes are: - The data directory is read-only or owned by a different user — the bootstrap can't write `node.yaml`. Fix permissions on - `$QUPTIME_DIR`. + `$QUPTIME_DIR`. The fastest fix on a standard install is just to + re-run `install.sh` — it reasserts the canonical ownership and + modes on the whole tree without touching your config. - Something else removed `node.yaml` mid-run (a config-management tool, a misconfigured volume). Re-run `qu serve` and it will rebuild from env, or run `qu init` manually with the flags you @@ -197,7 +199,9 @@ load private key: ... ``` Permissions on `keys/private.pem` are wrong — should be 0600 and owned -by the daemon user. Fix and restart. +by the daemon user. Fix and restart. Re-running `install.sh` on a +standard install is the easiest path: it repairs ownership and modes +on the entire data dir. ## Probes look much slower than expected diff --git a/install.sh b/install.sh index 35ba28c..3ae2015 100644 --- a/install.sh +++ b/install.sh @@ -175,20 +175,62 @@ fi install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$DATA_DIR" -# Reassert ownership on the dir's contents. Two cases this catches: +# Repair ownership and permissions on the data dir's contents. Catches: # - re-running the installer over a previous install where the -# service user/group changed +# service user/group changed. # - the operator ran `qu init` or `qu serve` as root once (easy # mistake: `sudo qu init` is shorter than the documented # `sudo -u quptime qu init`). When the daemon runs as root its # DataDir() resolves to /etc/quptime, so any files it writes land -# here owned by root:root mode 0600 — the systemd service then -# fails with `open node.yaml: permission denied`. -# chown -R only changes ownership, not perms, so file modes set by -# the daemon (0600 for node.yaml, 0700 for keys/) are preserved. -if [ -n "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then - chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" -fi +# owned by root:root — the systemd service then fails with +# `open node.yaml: permission denied`. +# - someone or something (a stray `chmod -R`, a misguided backup +# restore) tightened or loosened modes. Re-running the installer +# should be enough to get back to a working baseline. +# The canonical layout (mirrors the modes the daemon writes itself +# in internal/config and internal/crypto): +# /etc/quptime/ quptime:quptime 0750 +# /etc/quptime/keys/ quptime:quptime 0700 +# /etc/quptime/node.yaml quptime:quptime 0600 +# /etc/quptime/cluster.yaml quptime:quptime 0600 +# /etc/quptime/trust.yaml quptime:quptime 0600 +# /etc/quptime/keys/private.pem quptime:quptime 0600 +# /etc/quptime/keys/public.pem quptime:quptime 0644 +# /etc/quptime/keys/cert.pem quptime:quptime 0644 +# The runtime dir /var/run/quptime is owned by systemd via +# RuntimeDirectory= and rebuilt at each service start, so we leave it +# alone. +repair_perms() { + # Always reset the top-level dir mode — `install -d` only sets it + # on creation, not on re-run. + chown "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" + chmod 0750 "$DATA_DIR" + + # Reassert ownership across the whole tree in one pass. + if [ -n "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then + chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" + fi + + # keys/ is a directory with its own tighter mode. + if [ -d "$DATA_DIR/keys" ]; then + chmod 0700 "$DATA_DIR/keys" + fi + + # Each known file gets its canonical mode if it exists. We don't + # create anything that isn't already there — that's `qu init`'s + # job — and we don't touch unknown files an operator may have + # parked in the dir. + local f + for f in node.yaml cluster.yaml trust.yaml keys/private.pem; do + [ -f "$DATA_DIR/$f" ] && chmod 0600 "$DATA_DIR/$f" + done + for f in keys/public.pem keys/cert.pem; do + [ -f "$DATA_DIR/$f" ] && chmod 0644 "$DATA_DIR/$f" + done +} + +repair_perms +echo "> reasserted ownership ($SERVICE_USER:$SERVICE_GROUP) and modes under $DATA_DIR" echo "> writing $SERVICE_FILE" cat > "$SERVICE_FILE" <<'EOF'