1{ config, lib, pkgs, ... }:
2with lib;
3let
4 clamavUser = "clamav";
5 stateDir = "/var/lib/clamav";
6 clamavGroup = clamavUser;
7 cfg = config.services.clamav;
8 pkg = pkgs.clamav;
9
10 toKeyValue = generators.toKeyValue {
11 mkKeyValue = generators.mkKeyValueDefault { } " ";
12 listsAsDuplicateKeys = true;
13 };
14
15 clamdConfigFile = pkgs.writeText "clamd.conf" (toKeyValue cfg.daemon.settings);
16 freshclamConfigFile = pkgs.writeText "freshclam.conf" (toKeyValue cfg.updater.settings);
17 fangfrischConfigFile = pkgs.writeText "fangfrisch.conf" ''
18 ${lib.generators.toINI {} cfg.fangfrisch.settings}
19 '';
20in
21{
22 imports = [
23 (mkRemovedOptionModule [ "services" "clamav" "updater" "config" ] "Use services.clamav.updater.settings instead.")
24 (mkRemovedOptionModule [ "services" "clamav" "updater" "extraConfig" ] "Use services.clamav.updater.settings instead.")
25 (mkRemovedOptionModule [ "services" "clamav" "daemon" "extraConfig" ] "Use services.clamav.daemon.settings instead.")
26 ];
27
28 options = {
29 services.clamav = {
30 daemon = {
31 enable = mkEnableOption "ClamAV clamd daemon";
32
33 settings = mkOption {
34 type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
35 default = { };
36 description = ''
37 ClamAV configuration. Refer to <https://linux.die.net/man/5/clamd.conf>,
38 for details on supported values.
39 '';
40 };
41 };
42 updater = {
43 enable = mkEnableOption "ClamAV freshclam updater";
44
45 frequency = mkOption {
46 type = types.int;
47 default = 12;
48 description = ''
49 Number of database checks per day.
50 '';
51 };
52
53 interval = mkOption {
54 type = types.str;
55 default = "hourly";
56 description = ''
57 How often freshclam is invoked. See systemd.time(7) for more
58 information about the format.
59 '';
60 };
61
62 settings = mkOption {
63 type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
64 default = { };
65 description = ''
66 freshclam configuration. Refer to <https://linux.die.net/man/5/freshclam.conf>,
67 for details on supported values.
68 '';
69 };
70 };
71 fangfrisch = {
72 enable = mkEnableOption "ClamAV fangfrisch updater";
73
74 interval = mkOption {
75 type = types.str;
76 default = "hourly";
77 description = ''
78 How often freshclam is invoked. See systemd.time(7) for more
79 information about the format.
80 '';
81 };
82
83 settings = mkOption {
84 type = lib.types.submodule {
85 freeformType = with types; attrsOf (attrsOf (oneOf [ str int bool ]));
86 };
87 default = { };
88 example = {
89 securiteinfo = {
90 enabled = "yes";
91 customer_id = "your customer_id";
92 };
93 };
94 description = ''
95 fangfrisch configuration. Refer to <https://rseichter.github.io/fangfrisch/#_configuration>,
96 for details on supported values.
97 Note that by default urlhaus and sanesecurity are enabled.
98 '';
99 };
100 };
101
102 scanner = {
103 enable = mkEnableOption "ClamAV scanner";
104
105 interval = mkOption {
106 type = types.str;
107 default = "*-*-* 04:00:00";
108 description = ''
109 How often clamdscan is invoked. See systemd.time(7) for more
110 information about the format.
111 By default this runs using 10 cores at most, be sure to run it at a time of low traffic.
112 '';
113 };
114
115 scanDirectories = mkOption {
116 type = with types; listOf str;
117 default = [ "/home" "/var/lib" "/tmp" "/etc" "/var/tmp" ];
118 description = ''
119 List of directories to scan.
120 The default includes everything I could think of that is valid for nixos. Feel free to contribute a PR to add to the default if you see something missing.
121 '';
122 };
123 };
124 };
125 };
126
127 config = mkIf (cfg.updater.enable || cfg.daemon.enable) {
128 environment.systemPackages = [ pkg ];
129
130 users.users.${clamavUser} = {
131 uid = config.ids.uids.clamav;
132 group = clamavGroup;
133 description = "ClamAV daemon user";
134 home = stateDir;
135 };
136
137 users.groups.${clamavGroup} =
138 { gid = config.ids.gids.clamav; };
139
140 services.clamav.daemon.settings = {
141 DatabaseDirectory = stateDir;
142 LocalSocket = "/run/clamav/clamd.ctl";
143 PidFile = "/run/clamav/clamd.pid";
144 User = "clamav";
145 Foreground = true;
146 };
147
148 services.clamav.updater.settings = {
149 DatabaseDirectory = stateDir;
150 Foreground = true;
151 Checks = cfg.updater.frequency;
152 DatabaseMirror = [ "database.clamav.net" ];
153 };
154
155 services.clamav.fangfrisch.settings = {
156 DEFAULT.db_url = mkDefault "sqlite:////var/lib/clamav/fangfrisch_db.sqlite";
157 DEFAULT.local_directory = mkDefault stateDir;
158 DEFAULT.log_level = mkDefault "INFO";
159 urlhaus.enabled = mkDefault "yes";
160 urlhaus.max_size = mkDefault "2MB";
161 sanesecurity.enabled = mkDefault "yes";
162 };
163
164 environment.etc."clamav/freshclam.conf".source = freshclamConfigFile;
165 environment.etc."clamav/clamd.conf".source = clamdConfigFile;
166
167 systemd.services.clamav-daemon = mkIf cfg.daemon.enable {
168 description = "ClamAV daemon (clamd)";
169 after = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
170 wants = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
171 wantedBy = [ "multi-user.target" ];
172 restartTriggers = [ clamdConfigFile ];
173
174 serviceConfig = {
175 ExecStart = "${pkg}/bin/clamd";
176 ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
177 User = clamavUser;
178 Group = clamavGroup;
179 StateDirectory = "clamav";
180 RuntimeDirectory = "clamav";
181 PrivateTmp = "yes";
182 PrivateDevices = "yes";
183 PrivateNetwork = "yes";
184 };
185 };
186
187 systemd.timers.clamav-freshclam = mkIf cfg.updater.enable {
188 description = "Timer for ClamAV virus database updater (freshclam)";
189 wantedBy = [ "timers.target" ];
190 timerConfig = {
191 OnCalendar = cfg.updater.interval;
192 Unit = "clamav-freshclam.service";
193 };
194 };
195
196 systemd.services.clamav-freshclam = mkIf cfg.updater.enable {
197 description = "ClamAV virus database updater (freshclam)";
198 restartTriggers = [ freshclamConfigFile ];
199 requires = [ "network-online.target" ];
200 after = [ "network-online.target" ];
201
202 serviceConfig = {
203 Type = "oneshot";
204 ExecStart = "${pkg}/bin/freshclam";
205 SuccessExitStatus = "1"; # if databases are up to date
206 StateDirectory = "clamav";
207 User = clamavUser;
208 Group = clamavGroup;
209 PrivateTmp = "yes";
210 PrivateDevices = "yes";
211 };
212 };
213
214 systemd.services.clamav-fangfrisch-init = mkIf cfg.fangfrisch.enable {
215 wantedBy = [ "multi-user.target" ];
216 # if the sqlite file can be found assume the database has already been initialised
217 script = ''
218 db_url="${cfg.fangfrisch.settings.DEFAULT.db_url}"
219 db_path="''${db_url#sqlite:///}"
220
221 if [ ! -f "$db_path" ]; then
222 ${pkgs.fangfrisch}/bin/fangfrisch --conf ${fangfrischConfigFile} initdb
223 fi
224 '';
225 serviceConfig = {
226 Type = "oneshot";
227 StateDirectory = "clamav";
228 User = clamavUser;
229 Group = clamavGroup;
230 PrivateTmp = "yes";
231 PrivateDevices = "yes";
232 };
233 };
234
235 systemd.timers.clamav-fangfrisch = mkIf cfg.fangfrisch.enable {
236 description = "Timer for ClamAV virus database updater (fangfrisch)";
237 wantedBy = [ "timers.target" ];
238 timerConfig = {
239 OnCalendar = cfg.fangfrisch.interval;
240 Unit = "clamav-fangfrisch.service";
241 };
242 };
243
244 systemd.services.clamav-fangfrisch = mkIf cfg.fangfrisch.enable {
245 description = "ClamAV virus database updater (fangfrisch)";
246 restartTriggers = [ fangfrischConfigFile ];
247 requires = [ "network-online.target" ];
248 after = [ "network-online.target" "clamav-fangfrisch-init.service" ];
249
250 serviceConfig = {
251 Type = "oneshot";
252 ExecStart = "${pkgs.fangfrisch}/bin/fangfrisch --conf ${fangfrischConfigFile} refresh";
253 StateDirectory = "clamav";
254 User = clamavUser;
255 Group = clamavGroup;
256 PrivateTmp = "yes";
257 PrivateDevices = "yes";
258 };
259 };
260
261 systemd.timers.clamdscan = mkIf cfg.scanner.enable {
262 description = "Timer for ClamAV virus scanner";
263 wantedBy = [ "timers.target" ];
264 timerConfig = {
265 OnCalendar = cfg.scanner.interval;
266 Unit = "clamdscan.service";
267 };
268 };
269
270 systemd.services.clamdscan = mkIf cfg.scanner.enable {
271 description = "ClamAV virus scanner";
272 after = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
273 wants = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
274
275 serviceConfig = {
276 Type = "oneshot";
277 ExecStart = "${pkg}/bin/clamdscan --multiscan --fdpass --infected --allmatch ${lib.concatStringsSep " " cfg.scanner.scanDirectories}";
278 };
279 };
280 };
281}