2 Commits

Author SHA1 Message Date
Axodouble a1d74cf36d Fixed odd issue with the cli not finding the socket properly
Release / release (push) Successful in 3m21s
Container image / image (push) Successful in 5m57s
2026-05-15 08:05:55 +00:00
Axodouble f60b0a0609 Added better documentation for fixing my own broken installs, and updated the install script to patch issues
Container image / image (push) Successful in 4m40s
2026-05-15 07:56:13 +00:00
5 changed files with 119 additions and 24 deletions
+27
View File
@@ -4,6 +4,32 @@ All notable changes to this project are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and 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). this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.1.1] — 2026-05-15
### 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.
### Fixed
- **CLI socket lookup as the daemon user.** `sudo -u quptime qu …`
no longer fails with `dial daemon socket /tmp/quptime-quptime/…:
no such file or directory` while the system daemon is running.
`config.SocketPath()` now probes the canonical systemd location
(`/run/quptime/quptime.sock`, then `/var/run/quptime/quptime.sock`)
regardless of euid before falling back to per-user paths, so the
CLI reaches the daemon's socket even when `sudo` has stripped
`RUNTIME_DIRECTORY` and `XDG_RUNTIME_DIR` from the environment.
## [v0.1.0] — 2026-05-15 ## [v0.1.0] — 2026-05-15
### Changed ### Changed
@@ -113,3 +139,4 @@ Initial public release.
[v0.0.1]: https://git.cer.sh/axodouble/quptime/releases/tag/v0.0.1 [v0.0.1]: https://git.cer.sh/axodouble/quptime/releases/tag/v0.0.1
[v0.1.0]: https://git.cer.sh/axodouble/quptime/releases/tag/v0.1.0 [v0.1.0]: https://git.cer.sh/axodouble/quptime/releases/tag/v0.1.0
[v0.1.1]: https://git.cer.sh/axodouble/quptime/releases/tag/v0.1.1
+9
View File
@@ -70,6 +70,15 @@ What it does:
`/etc/systemd/system/quptime.service` (hardened — matches the unit `/etc/systemd/system/quptime.service` (hardened — matches the unit
in [systemd.md](deployment/systemd.md)). Enables but does not start in [systemd.md](deployment/systemd.md)). Enables but does not start
the service, so you can configure identity before first boot. 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 ## Build from source
+6 -2
View File
@@ -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 - The data directory is read-only or owned by a different user — the
bootstrap can't write `node.yaml`. Fix permissions on 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 - Something else removed `node.yaml` mid-run (a config-management
tool, a misconfigured volume). Re-run `qu serve` and it will tool, a misconfigured volume). Re-run `qu serve` and it will
rebuild from env, or run `qu init` manually with the flags you 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 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 ## Probes look much slower than expected
+51 -9
View File
@@ -175,20 +175,62 @@ fi
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$DATA_DIR" 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 # - 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 # - the operator ran `qu init` or `qu serve` as root once (easy
# mistake: `sudo qu init` is shorter than the documented # mistake: `sudo qu init` is shorter than the documented
# `sudo -u quptime qu init`). When the daemon runs as root its # `sudo -u quptime qu init`). When the daemon runs as root its
# DataDir() resolves to /etc/quptime, so any files it writes land # DataDir() resolves to /etc/quptime, so any files it writes land
# here owned by root:root mode 0600 — the systemd service then # owned by root:root — the systemd service then fails with
# fails with `open node.yaml: permission denied`. # `open node.yaml: permission denied`.
# chown -R only changes ownership, not perms, so file modes set by # - someone or something (a stray `chmod -R`, a misguided backup
# the daemon (0600 for node.yaml, 0700 for keys/) are preserved. # restore) tightened or loosened modes. Re-running the installer
if [ -n "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then # should be enough to get back to a working baseline.
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" # The canonical layout (mirrors the modes the daemon writes itself
fi # 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" echo "> writing $SERVICE_FILE"
cat > "$SERVICE_FILE" <<'EOF' cat > "$SERVICE_FILE" <<'EOF'
+25 -12
View File
@@ -58,19 +58,20 @@ func DataDir() string {
// SocketPath returns the unix socket used for local CLI ↔ daemon control. // SocketPath returns the unix socket used for local CLI ↔ daemon control.
// //
// Resolution order: // Resolution order:
// 1. $QUPTIME_SOCKET — explicit operator override // 1. $QUPTIME_SOCKET — explicit operator override.
// 2. $RUNTIME_DIRECTORY — set by systemd when the unit declares // 2. $RUNTIME_DIRECTORY — set by systemd when the unit declares
// RuntimeDirectory=quptime. This is the path that matters in // RuntimeDirectory=quptime. This is the path the daemon uses
// practice: with User=quptime + PrivateTmp=true, the daemon's // when run under the packaged unit: /run/quptime/quptime.sock.
// /tmp is namespaced and invisible to the root CLI shell, so a // 3. The canonical system socket path — /run/quptime/quptime.sock —
// /tmp fallback yields "no such file" even though the daemon is // if it exists. This catches the CLI side regardless of who is
// happily listening. Anchoring on $RUNTIME_DIRECTORY puts the // invoking it: `sudo -u quptime qu status` strips RUNTIME_DIRECTORY
// socket at /run/quptime/quptime.sock, which is the same inode // and XDG_RUNTIME_DIR, so without this probe the CLI falls all
// the root-CLI default (/var/run/quptime/…) reaches via the // the way through to /tmp/quptime-<user>/… and reports "no such
// /var/run → /run symlink. // file" even while the daemon is happily listening.
// 3. /var/run/quptime/… when euid is 0 (CLI side, packaged installs) // 4. /var/run/quptime/… when euid is 0 (CLI side, packaged installs
// 4. $XDG_RUNTIME_DIR/quptime/… for user-mode installs // on systems where /var/run isn't a symlink to /run).
// 5. /tmp/quptime-<user>/… as a last resort // 5. $XDG_RUNTIME_DIR/quptime/… for user-mode installs.
// 6. /tmp/quptime-<user>/… as a last resort.
func SocketPath() string { func SocketPath() string {
if v := os.Getenv("QUPTIME_SOCKET"); v != "" { if v := os.Getenv("QUPTIME_SOCKET"); v != "" {
return v return v
@@ -84,6 +85,18 @@ func SocketPath() string {
} }
return filepath.Join(v, SocketName) return filepath.Join(v, SocketName)
} }
// If a system-managed daemon is already listening, route there
// regardless of euid. Without this, `sudo -u quptime qu …` can't
// find the socket the daemon (also running as quptime) created
// via RuntimeDirectory=.
for _, p := range []string{
"/run/quptime/" + SocketName,
"/var/run/quptime/" + SocketName,
} {
if _, err := os.Stat(p); err == nil {
return p
}
}
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
return "/var/run/quptime/" + SocketName return "/var/run/quptime/" + SocketName
} }