Skip to content

Restart apt-update timer on upgrade and sandbox its service unit#79

Merged
LarsLaskowski merged 3 commits into
mainfrom
claude/fix-issues-77-74-akpixv
Jul 17, 2026
Merged

Restart apt-update timer on upgrade and sandbox its service unit#79
LarsLaskowski merged 3 commits into
mainfrom
claude/fix-issues-77-74-akpixv

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • install.sh: capture whether pimonitor-apt-update.timer was already active before daemon-reload/enable --now, and explicitly systemctl restart it afterwards if so — same pattern already used for pimonitor.service (Restart pimonitor.service on upgrade if already running #76). enable --now is a no-op on an already-active unit, so without this a future change to packaging/pimonitor-apt-update.timer (e.g. a different refresh interval) would install the new unit file on an upgrade but leave the old schedule loaded until an unrelated reboot/restart.
  • pimonitor-apt-update.service: add the sandboxing directives suggested in Hardening: pimonitor-apt-update.service runs as root with no sandboxing #74 (ProtectSystem=full with ReadWritePaths=, ProtectHome, PrivateTmp, ProtectKernelModules, ProtectKernelLogs, ProtectControlGroups, a trimmed CapabilityBoundingSet=, and Nice=/IOSchedulingClass=idle), plus NoNewPrivileges=true.

For the capability set and NoNewPrivileges, I didn't want to guess — I ran apt-get update locally under setpriv with a trimmed capability bounding set and with --no-new-privs to check what's actually load-bearing:

  • apt drops privileges to the _apt sandbox user for the network fetch (needs CAP_SETUID/CAP_SETGID), then chowns/chmods the fetched index files back (CAP_CHOWN/CAP_FOWNER), and needs CAP_DAC_OVERRIDE to replace lock/partial files it doesn't own as _apt. Dropping any one of these five caused apt-get update to either fail outright (setuid/setgid/dac_override) or silently leave files root-owned instead of _apt-owned (chown/fowner).
  • NoNewPrivileges=true did not break anything in testing, contrary to the issue's initial assumption — it only blocks gaining privileges via execve of a setuid/capability-bearing binary, not a process's own setuid()/setgid() syscalls (which is how apt drops to _apt). So it's included.
  • RestrictAddressFamilies and syscall filtering weren't added; they're out of scope for what Hardening: pimonitor-apt-update.service runs as root with no sandboxing #74 asked for and would need more careful verification of apt's HTTPS/DNS code paths than I could do from this environment.

Also ran systemd-analyze verify against the changed unit file (clean, no warnings).

Related Issue

Closes #77
Closes #74

Checklist

  • Tests added/updated for the change (go test ./... passes locally) — not applicable, no Go code changed; verified instead via systemd-analyze verify and live apt-get update runs under setpriv with the trimmed capability set (see above)
  • go vet ./... and golangci-lint run are clean
  • Documentation updated if this changes the REST API (docs/API.md), configuration (README.md, packaging/pimonitor.example.yaml), or installation/packaging (packaging/install.sh, systemd units) — not applicable, no user-facing config/API surface changed
  • No breaking change to /api/v1/... response shapes, or a new API version was introduced instead — not applicable, no API changes

Verification note

Hardware verification is still recommended: I tested apt-get update's capability requirements empirically (via setpriv) and validated the unit syntax with systemd-analyze verify, but couldn't run this under a live systemd instance (this environment has no PID 1 systemd) or on real Pi hardware, so I'd suggest a systemctl start pimonitor-apt-update.service + systemctl status sanity check after deploying to a Pi.

- install.sh: capture whether pimonitor-apt-update.timer was already
  active before enable --now, same as already done for pimonitor.service,
  and explicitly restart it afterwards. 'enable --now' is a no-op on an
  already-active unit, so without this a future change to the timer's
  schedule would install the new unit file but keep the old schedule
  loaded until an unrelated restart/reboot (closes #77).

- pimonitor-apt-update.service: add cheap sandboxing directives now that
  the main pimonitor.service has them (closes #74). The capability
  bounding set and NoNewPrivileges were verified against a live
  'apt-get update' run: apt drops to the _apt user for the actual
  download (CAP_SETUID/CAP_SETGID) and chowns/chmods the fetched index
  files back (CAP_CHOWN/CAP_FOWNER/CAP_DAC_OVERRIDE); all five are
  load-bearing. NoNewPrivileges does not interfere with this since it's
  a plain setuid()/setgid() syscall drop, not an execve of a setuid
  binary, contrary to the issue's initial assumption.

@LarsLaskowski LarsLaskowski left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the repo checklist. Verification on the PR branch: go build ./..., go vet ./..., go test ./... all pass (no Go changes), bash -n packaging/install.sh is clean, and systemd-analyze verify on the changed unit exits clean, confirming the PR's claim.

I also independently re-checked the sandboxing claims empirically (strace of a live apt-get update with apt 2.x): the five-capability analysis and the NoNewPrivileges reasoning in the PR body hold up — NNP only gates privilege gain across execve, not apt's own setuid() drop to _apt. The timer restart logic in install.sh correctly mirrors the #76 pattern.

Two findings inline: ReadWritePaths= is inert under ProtectSystem=full (misleading as written), and the || true on the initial refresh now silently hides exactly the failure mode this PR introduces risk of.

Comment thread packaging/pimonitor-apt-update.service Outdated
Comment thread packaging/install.sh Outdated
claude and others added 2 commits July 17, 2026 16:08
- pimonitor-apt-update.service: ProtectSystem=full made ReadWritePaths= a
  no-op since /var is already writable under 'full' - switch to 'strict'
  so it actually confines writes. Re-verified with strace against a live
  'apt-get update' that only /var/lib/apt/lists and /tmp (already covered
  by PrivateTmp) are opened for writing; dropped /var/log/apt from
  ReadWritePaths since update never writes there.

- install.sh: the initial cache refresh swallowed failures via '|| true'.
  With this PR adding new sandboxing to that service, a directive breaking
  apt-get update on some setup would now install cleanly and only surface
  as a silently stale update count. Print a warning on failure instead.
@LarsLaskowski
LarsLaskowski merged commit ad4310f into main Jul 17, 2026
3 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/fix-issues-77-74-akpixv branch July 17, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants