diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix index 3749dd114893..a844c4e78db3 100644 --- a/nixos/modules/services/security/clamav.nix +++ b/nixos/modules/services/security/clamav.nix @@ -65,6 +65,34 @@ in ''; }; }; + + clamonacc = { + enable = lib.mkOption { + default = false; + example = true; + description = '' + Whether to enable ClamAV on-access scanner. + + The settings for ClamAV's on-access scanner is configured in `clamd.conf` via `services.clamav.daemon.settings`. + Refer to on how to configure it. + + Example to scan `/home/foo/Downloads` (and block access until scanning is completed) would be: + ``` + services.clamav = { + daemon.enable = true; + clamonacc.enable = true; + + daemon.settings = { + OnAccessPrevention = true; + OnAccessIncludePath = "/home/foo/Downloads"; + }; + }; + ``` + ''; + type = lib.types.bool; + }; + }; + updater = { enable = lib.mkEnableOption "ClamAV freshclam updater"; @@ -172,6 +200,17 @@ in }; config = lib.mkIf (cfg.updater.enable || cfg.daemon.enable) { + assertions = [ + { + assertion = cfg.scanner.enable -> cfg.daemon.enable; + message = "ClamAV scanner requires ClamAV daemon to operate"; + } + { + assertion = cfg.clamonacc.enable -> cfg.daemon.enable; + message = "ClamAV on-access scanner requires ClamAV daemon to operate"; + } + ]; + environment.systemPackages = [ cfg.package ]; users.users.${clamavUser} = { @@ -189,8 +228,10 @@ in DatabaseDirectory = stateDir; LocalSocket = "/run/clamav/clamd.ctl"; PidFile = "/run/clamav/clamd.pid"; - User = "clamav"; + User = clamavUser; Foreground = true; + # Prevent infinite recursion in scanning + OnAccessExcludeUname = clamavUser; }; services.clamav.updater.settings = { @@ -216,11 +257,26 @@ in description = "ClamAV Antivirus Slice"; }; + systemd.sockets.clamav-daemon = lib.mkIf cfg.daemon.enable { + description = "Socket for ClamAV daemon (clamd)"; + wantedBy = [ "sockets.target" ]; + listenStreams = [ + cfg.daemon.settings.LocalSocket + ]; + socketConfig = { + SocketUser = clamavUser; + SocketGroup = clamavGroup; + # LocalSocketMode setting in clamd.conf is not prefixed with octal 0, add it here. + SocketMode = "0${cfg.daemon.settings.LocalSocketMode or "666"}"; + }; + }; + systemd.services.clamav-daemon = lib.mkIf cfg.daemon.enable { description = "ClamAV daemon (clamd)"; documentation = [ "man:clamd(8)" ]; after = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ]; wants = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ]; + requires = [ "clamav-daemon.socket" ]; wantedBy = [ "multi-user.target" ]; restartTriggers = [ clamdConfigFile ]; @@ -238,6 +294,20 @@ in }; }; + systemd.services.clamav-clamonacc = lib.mkIf cfg.clamonacc.enable { + description = "ClamAV on-access scanner (clamonacc)"; + after = [ "clamav-daemon.socket" ]; + requires = [ "clamav-daemon.socket" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ clamdConfigFile ]; + + # This unit must start as root to be able to use fanotify. + serviceConfig = { + ExecStart = "${cfg.package}/bin/clamonacc -F --fdpass"; + Slice = "system-clamav.slice"; + }; + }; + systemd.timers.clamav-freshclam = lib.mkIf cfg.updater.enable { description = "Timer for ClamAV virus database updater (freshclam)"; wantedBy = [ "timers.target" ]; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 77853f70dbc3..3ebcadf90136 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -359,6 +359,7 @@ in cinnamon = runTest ./cinnamon.nix; cinnamon-wayland = runTest ./cinnamon-wayland.nix; cjdns = runTest ./cjdns.nix; + clamav = runTest ./clamav.nix; clatd = runTest ./clatd.nix; clickhouse = import ./clickhouse { inherit runTest; diff --git a/nixos/tests/clamav.nix b/nixos/tests/clamav.nix new file mode 100644 index 000000000000..b84a195747e7 --- /dev/null +++ b/nixos/tests/clamav.nix @@ -0,0 +1,45 @@ +# Test ClamAV. + +{ lib, pkgs, ... }: +{ + name = "clamav"; + nodes = { + machine = { + services.clamav = { + daemon.enable = true; + clamonacc.enable = true; + + daemon.settings = { + OnAccessPrevention = true; + OnAccessIncludePath = "/opt"; + }; + }; + + # Add the definition for our test file. + # We cannot download definitions from Internet using freshclam in sandboxed test. + systemd.tmpfiles.settings."10-eicar"."/var/lib/clamav/test.hdb".L.argument = "${pkgs.runCommand + "test.hdb" + { } + '' + echo CLAMAVTEST > testfile + ${lib.getExe' pkgs.clamav "sigtool"} --sha256 testfile > $out + '' + }"; + + # Test using /opt as the ClamAV on-access scanner-protected directory. + systemd.tmpfiles.settings."10-testdir"."/opt".d = { }; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("default.target") + + # Write test file into the test directory. + # This won't trigger ClamAV as it scans on file open. + machine.succeed("echo CLAMAVTEST > /opt/testfile") + + machine.fail("cat /opt/testfile") + ''; +}