[Unit] Description=Hound Antivirus daemon Documentation=https://houndav.com/docs After=network.target # The gate is only useful while the filesystem it watches is mounted. RequiresMountsFor=/var/lib/hound [Service] Type=exec ExecStart=/usr/bin/houndd # Stated explicitly so the daemon and the CLI cannot drift apart. Environment=HOUNDD_SOCK=/run/hound/houndd.sock Restart=on-failure RestartSec=2s # ── Privilege ──────────────────────────────────────────────────────── # # houndd reduces its own capabilities at startup (see crates/houndd/src/ # caps.rs). This bounding set is the second half of that: it protects the # machine if the binary is ever replaced by one that does not, and it # means the daemon never *holds* the rest even momentarily. # # It cannot be narrower. fanotify needs SYS_ADMIN; scanning arbitrary # files needs DAC_READ_SEARCH; quarantining out of a directory owned by # someone else needs DAC_OVERRIDE; stripping the execute bit off a file # we do not own needs FOWNER. CapabilityBoundingSet=CAP_SYS_ADMIN CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER AmbientCapabilities=CAP_SYS_ADMIN CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER NoNewPrivileges=yes # ── Filesystem ─────────────────────────────────────────────────────── # # ProtectSystem=strict would be the obvious choice and is WRONG here: it # remounts everything read-only, and quarantine has to remove a threat # from wherever it landed. ProtectHome is off for the same reason — most # of what we quarantine is under /home. ProtectSystem=false ProtectHome=false ReadWritePaths=/var/lib/hound /var/log/hound StateDirectory=hound LogsDirectory=hound RuntimeDirectory=hound PrivateTmp=no # ── Everything else we can shut off ────────────────────────────────── ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectClock=yes ProtectHostname=yes ProtectProc=invisible RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes # MemoryDenyWriteExecute is deliberately NOT set, and this is a real # trade-off rather than an oversight. # # yara-x compiles rules to WebAssembly and JITs them, so it needs pages # that are writable and then executable. With W^X enforced the daemon # aborts at startup with "unable to make memory executable" — which is # exactly what happened on the first real install, after the unit had # passed systemd-analyze verify. A hardening directive that stops the # service is worse than the exposure it prevents, because the machine # ends up with no antivirus at all. # # What compensates: the scanner never executes scanned content, the # capability set is four of forty-one, the syscall filter below blocks # @module/@mount/@raw-io/@reboot, and the process cannot gain privileges. # Revisit if yara-x ever ships an interpreter-only mode. # MemoryDenyWriteExecute=yes RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@clock @cpu-emulation @debug @module @mount @obsolete @raw-io @reboot @swap UMask=0077 # ── Resources ──────────────────────────────────────────────────────── # A scanner that eats the machine is its own denial of service. MemoryMax=1G TasksMax=64 LimitNOFILE=65536 OOMScoreAdjust=-500 [Install] WantedBy=multi-user.target