75b86fd60d
acme:
- acme.sh chmods its tree to owner-only (700/600) every run, which
broke the two-user model: a tree left owner-only by one user made
every acme.sh call of the other exit 2
- normalize_acme_home() reopens group access (sudo chmod g+rwX,
files only — setgid dirs trip RestrictSUIDSGID); _run_acme_preflight
is the choke point before every daemon acme.sh call + startup
- acme service now runs as the daemon user; --log persists the raw CA
transcript; SYS_LOG=6 journals manual issue/renew runs
- timer daily-only: two runs/day landed inside ZeroSSL's 24h
validation backoff (Retry-After: 86400) — a permanent renewal lockout
- _collect_acme no longer raises on cert-list failure; reports
status.error (AcmeState.status) so the certs page can surface it
firewall: re-stamp the applied baseline on live zone mutations
(interfaces/services/rich-rules/masquerade/forward-ports) so cancel-all
reverts to post-mutation state, not a stale install-era snapshot;
set_masquerade syncs the declarative config for existing zones;
add_forward_port records toaddr only with toport
status: apply-all accepts {"force": true} (forwarded to the firewall
apply only); ApplyConfirm force checkbox; applyResultToasts() — the
errors map wins over the 200; ActionButton checks errors before the
success toast; dashboard uses ApplyConfirm
system_import: drift re-imports carry the existing apply-meta; first
import stamps the adopted content as applied (it is the running state)
— no phantom pending changes
nginx: get_config only re-saves when migration actually changed the
config (no more owner/mtime churn on every read)
install: repair mis-owned top-level system dirs (tmpfiles
unsafe-path-transition), warn with a full-repair command for deeper
mis-ownership
daemon/server: loop.get_exception_handler() (aiohttp API fix)
tests: 888 pytest + 24 node passing; ruff clean
80 lines
3.2 KiB
Desktop File
80 lines
3.2 KiB
Desktop File
[Unit]
|
|
Description=Vacuum Wall Daemon (privileged backend)
|
|
Documentation=https://github.com/wall/vacuum-wall
|
|
After=network.target firewalld.service
|
|
Wants=firewalld.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User={{ USER_DAEMON_NAME }}
|
|
Group={{ USER_GROUP }}
|
|
WorkingDirectory={{ PROJECT_DIR }}
|
|
ExecStart={{ PROJECT_DIR }}/.venv/bin/python -m daemon
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
TimeoutStopSec=15
|
|
Environment=PATH=/usr/local/bin:/usr/bin
|
|
Environment=PYTHONUNBUFFERED=1
|
|
Environment=ACME_HOME={{ PROJECT_DIR }}/data/acme
|
|
# acme.sh routes its _info/_err lines through logger(1) -> journald when
|
|
# SYS_LOG is set (default: off). This journals manual issue/renew runs
|
|
# in real time under this unit, whose subprocess stdout is otherwise
|
|
# captured by the daemon and never seen by the journal.
|
|
# Levels: 3=error, 6=info, 7=debug.
|
|
Environment=SYS_LOG=6
|
|
Environment=HOME={{ PROJECT_DIR }}
|
|
|
|
# Runtime directories created before namespace setup. ProtectSystem=strict
|
|
# makes the whole hierarchy read-only, and namespace setup fails
|
|
# (exit 226/NAMESPACE) if any ReadWritePaths= entry is missing at spawn.
|
|
# /run is a fresh tmpfs at every boot, so volatile /run paths must be
|
|
# created up front (RuntimeDirectory= here; /run/firewalld via
|
|
# system/tmpfiles.d/vacuum-wall.conf and by firewalld itself) rather than
|
|
# at first use.
|
|
# vacuum-wall : secure temp files used during config apply
|
|
# nginx : /run/nginx (listed in ReadWritePaths)
|
|
RuntimeDirectory=vacuum-wall nginx
|
|
RuntimeDirectoryMode=0750
|
|
|
|
LogsDirectory=vacuum-wall
|
|
|
|
# Security hardening
|
|
ProtectSystem=strict
|
|
# NOTE: every ReadWritePaths= entry must exist when the unit spawns or namespace
|
|
# setup fails (226/NAMESPACE). Volatile /run entries are pre-created:
|
|
# /run/vacuum-wall, /run/nginx → RuntimeDirectory= (above)
|
|
# /run/firewalld → system/tmpfiles.d/vacuum-wall.conf (and is
|
|
# present while firewalld runs, which starts
|
|
# before this unit)
|
|
# /run/sudo is intentionally NOT listed: the daemon's sudo children use the
|
|
# NOPASSWD whitelist and never need sudo's session directory (verified with
|
|
# the directory absent). Listing it made the unit crash-loop whenever it
|
|
# restarted after the last sudo session had ended and sudo removed /run/sudo.
|
|
# /run/nginx.pid IS listed: nginx -t opens the pid file for *writing* in
|
|
# addition to -s/acme reading it, so a read-only mount makes every daemon-side
|
|
# `nginx -t` (and therefore /nginx/apply) fail with EROFS. The file is
|
|
# pre-created by system/tmpfiles.d/vacuum-wall.conf so the ReadWritePaths=
|
|
# entry always exists at spawn (nginx rewrites it on start; nginx -t does
|
|
# not modify its contents).
|
|
ReadWritePaths={{ PROJECT_DIR }} /tmp /etc/systemd/network /etc/nginx /etc/dnsmasq.d /etc/wireguard /run/vacuum-wall /run/firewalld /run/nginx /run/nginx.pid /var/log/nginx /var/log/vacuum-wall
|
|
PrivateTmp=yes
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectControlGroups=yes
|
|
ProtectHostname=yes
|
|
RestrictSUIDSGID=yes
|
|
MemoryDenyWriteExecute=yes
|
|
RestrictRealtime=yes
|
|
RestrictNamespaces=yes
|
|
LockPersonality=yes
|
|
SystemCallFilter=@system-service
|
|
PrivateDevices=yes
|
|
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
IPAddressDeny=any
|
|
IPAddressAllow=localhost
|
|
NoNewPrivileges=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|