[Unit] Description=restic-manager agent Documentation=https://gitea.dcglab.co.uk/steve/restic-manager After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/restic-manager-agent -config /etc/restic-manager/agent.yaml Restart=always RestartSec=5 # The agent runs as root. A fleet-backup tool needs to read every # file on the system regardless of DAC permissions; running as a # dedicated unprivileged user means either silent skips on /home, # /root, /var/lib/, or operators having to add the # service user to every group whose files they want backed up. Both # are worse than the threat model already implies (the agent holds # repo credentials, executes arbitrary restic, and runs operator- # defined hooks — its blast radius is already large). # # The mitigation is aggressive systemd sandboxing of the root # process: drop all capabilities except the few we need, deny # writes outside our state dirs, and forbid privilege escalation. User=root Group=root # CAP_DAC_READ_SEARCH lets us read any file regardless of DAC perms # (the "backup everything" capability). CAP_DAC_OVERRIDE is needed # during restore for chown/chmod to recreate ownership. Drop the # rest — root in this process means "can read", not "can do". CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN AmbientCapabilities=CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN # Hardening — blocks privilege escalation even from root, and # confines kernel / namespace / privilege surface. Filesystem reads # stay open (that's the whole job) and restore writes are # unrestricted: a backup tool whose entire purpose is "put files # back where they belong" can't have ProtectHome=read-only or # ProtectSystem=strict without breaking on the first cross-user # restore. ProtectSystem=full keeps /usr, /boot, /efi read-only so a # compromised agent can't swap out /usr/bin/restic or drop a kernel # module, while leaving /home, /root, /var, /opt, /srv, /tmp etc. # writable for arbitrary restore targets. The agent is treated as a # high-trust component (it runs operator hooks as root and holds # repo credentials); the residual hardening is about kernel + privesc # protection, not write confinement. NoNewPrivileges=true ProtectSystem=full # ProtectSystem=full mounts /usr, /boot, /efi *and* /etc read-only. # The agent rewrites /etc/restic-manager/agent.yaml on enrolment and # whenever a new SecretsKey is minted, so we need a targeted # write-exemption for that dir. No exemption for the rest of /etc: # the agent has no business editing /etc/passwd, /etc/sudoers, etc. # # /usr/local/bin is writable so the self-update flow (P6-01) can # atomic-rename a fresh binary over the running one. Permitting the # whole directory (rather than just the binary path) is required # because os.Rename takes a write lock on the parent dir. ReadWritePaths=/etc/restic-manager /usr/local/bin ProtectHostname=true ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true ProtectClock=true PrivateTmp=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictRealtime=true RestrictSUIDSGID=true RestrictNamespaces=true LockPersonality=true MemoryDenyWriteExecute=true SystemCallArchitectures=native # (No SystemCallFilter — the cap drop above already constrains what # root can do; an allow-list filter killed restic with SIGSYS during # init because @system-service excludes some of the syscalls Go's # runtime + restic's file scanner reach for. The Protect*/Restrict* # toggles still cover network / kernel / mount / namespace.) [Install] WantedBy=multi-user.target