[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 writes / network / kernel access to what restic actually # needs. Filesystem reads stay open: that's the whole job. NoNewPrivileges=true ProtectSystem=strict ReadWritePaths=/etc/restic-manager /var/lib/restic-manager ProtectHome=read-only 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