Skip to content

Harden pimonitor.service with additional systemd sandboxing#52

Merged
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-22-djuzod
Jul 16, 2026
Merged

Harden pimonitor.service with additional systemd sandboxing#52
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-22-djuzod

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

Extends the systemd sandbox of packaging/pimonitor.service with a set of
cheap, widely-supported directives to further reduce the kernel attack
surface exposed to a process that parses network-facing input and shells
out to apt/vcgencmd (review finding S5, defense in depth).

Added to [Service]:

ProtectKernelModules=true
ProtectKernelLogs=true
ProtectClock=true
ProtectHostname=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
UMask=0077

Notes:

  • PrivateDevices is intentionally not setvcgencmd needs
    /dev/vchiq (or /dev/vcio) to read the GPU temperature. Enabling it
    would require paired DeviceAllow= rules and is out of scope.
  • MemoryDenyWriteExecute is safe — the binary is pure Go with CGO
    disabled in builds (no JIT, no W+X pages).
  • All existing directives are unchanged; the explanatory comment block in
    the unit is expanded to document the additions and the PrivateDevices
    caveat.

Related Issue

Closes #22

Verification

  • systemd-analyze verify packaging/pimonitor.service passes with no
    syntax errors on any of the new directives (only the expected warning
    that the ExecStart binary is absent in the CI/dev container).

On-device verification still required (this container has no booted
systemd, so systemd-analyze security cannot run here). On a Raspberry Pi
or Debian-family host, please confirm the acceptance criteria from the
issue:

  • systemctl daemon-reload && systemctl restart pimonitor starts cleanly;
    dashboard reachable; CPU/memory/disk/network/temperature metrics
    populate; the updates count appears after the next slow tick (i.e.
    apt list --upgradable still works under the syscall filter); GPU
    temperature still shows where vcgencmd exists.
  • journalctl -u pimonitor shows no seccomp/permission errors.
  • systemd-analyze security pimonitor.service score improves versus the
    previous unit (please record before/after here).

Checklist

  • Tests added/updated for the change (go test ./... passes locally) — n/a, packaging-only change (no Go code touched)
  • go vet ./... and golangci-lint run are clean — n/a, no Go code changed
  • 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) —
    the systemd unit's inline comment block is updated
  • No breaking change to /api/v1/... response shapes, or a new API
    version was introduced instead — n/a, no API change

Extend the systemd sandbox of pimonitor.service with cheap, widely
supported directives to further reduce the kernel attack surface exposed
to a process that parses network-facing input and shells out to
apt/vcgencmd (finding S5).

Add ProtectKernelModules, ProtectKernelLogs, ProtectClock,
ProtectHostname, RestrictNamespaces, LockPersonality,
MemoryDenyWriteExecute, RestrictRealtime, RestrictSUIDSGID, RemoveIPC,
SystemCallArchitectures=native, a @System-service SystemCallFilter that
denies @privileged/@resources, and UMask=0077.

PrivateDevices is intentionally left unset because vcgencmd needs
/dev/vchiq (or /dev/vcio) for the GPU temperature. MemoryDenyWriteExecute
is safe because the binary is pure Go with CGO disabled (no W+X pages).
Existing directives are unchanged; the comment block is expanded to
explain the additions.

Closes #22

@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 project's review checklist (.claude/skills/review-pr). go build ./..., go vet ./..., and go test ./... all pass on the branch (packaging-only change, as expected), and systemd-analyze verify is clean apart from the known missing-binary warning.

The direction is good and most directives are sound, but two of the new directives would break the service at runtime, and neither is catchable by systemd-analyze verify — see the inline comments:

  1. SystemCallFilter=~@privileged @resources kills the Go 1.22 binary with SIGSYS before main() (Go ≥1.19 calls setrlimit/prlimit64 at init, both in @resources, and no SystemCallErrorNumber= is set). The service would enter a restart loop.
  2. ProtectClock=true implicitly enables device-access enforcement (via its implied DeviceAllow=char-rtc r), blocking /dev/vchiq and silently killing the GPU temperature metric — despite the unit comment explicitly avoiding PrivateDevices for that reason.

Everything else checks out:

  • MemoryDenyWriteExecute claim verified: the arm/arm64 release targets in the Makefile set CGO_ENABLED=0, so no W+X pages.
  • ProtectKernelModules/Logs, ProtectHostname, RestrictNamespaces, LockPersonality, RestrictRealtime, RestrictSUIDSGID, RemoveIPC, SystemCallArchitectures=native, UMask=0077 are all appropriate for this service and don't conflict with what it does (read-only /proc///sys parsing, TCP listener, state dir writes).
  • On older Raspberry Pi OS releases with systemd < 245, some directives are unknown; systemd ignores them with a warning rather than failing, so that's fine.
  • No API, Go code, or dependency changes; comments/docs are in English.

The on-device verification checklist in the PR description is exactly right and would have caught issue 1 on the first systemctl restart — please keep that step after fixing the two findings.

Comment thread packaging/pimonitor.service
Comment thread packaging/pimonitor.service Outdated
Address two review findings that systemd-analyze verify cannot catch:

- Add SystemCallErrorNumber=EPERM. Since Go 1.19 the runtime raises
  RLIMIT_NOFILE via setrlimit/prlimit64 (both in @resources) at process
  init. Without a soft error number, the ~@resources deny rule would
  terminate the binary with SIGSYS before main() runs, causing a
  Restart=on-failure loop. EPERM lets Go (which ignores the setrlimit
  error) and the inherited apt/vcgencmd children degrade gracefully.

- Drop ProtectClock=true. It implies DeviceAllow=char-rtc, and any
  DeviceAllow= flips DevicePolicy into enforcing mode, which would
  silently block /dev/vchiq and kill the GPU temperature metric - the
  exact device access the unit comment says it preserves. CAP_SYS_TIME
  is already removed via the empty CapabilityBoundingSet and @clock
  syscalls are already denied by ~@PRIVILEGED, so ProtectClock added
  almost nothing here.
@LarsLaskowski
LarsLaskowski merged commit 4232aaf into main Jul 16, 2026
3 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-22-djuzod branch July 16, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: extend systemd sandboxing of pimonitor.service (namespaces, syscall filter, memory W^X)

2 participants