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