1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.radicale;
12
13 format = pkgs.formats.ini {
14 listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
15 };
16
17 pkg = if cfg.package == null then pkgs.radicale else cfg.package;
18
19 confFile =
20 if cfg.settings == { } then
21 pkgs.writeText "radicale.conf" cfg.config
22 else
23 format.generate "radicale.conf" cfg.settings;
24
25 rightsFile = format.generate "radicale.rights" cfg.rights;
26
27 bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
28
29in
30{
31 options.services.radicale = {
32 enable = mkEnableOption "Radicale CalDAV and CardDAV server";
33
34 package = mkOption {
35 description = "Radicale package to use.";
36 # Default cannot be pkgs.radicale because non-null values suppress
37 # warnings about incompatible configuration and storage formats.
38 type = with types; nullOr package // { inherit (package) description; };
39 default = null;
40 defaultText = literalExpression "pkgs.radicale";
41 };
42
43 config = mkOption {
44 type = types.str;
45 default = "";
46 description = ''
47 Radicale configuration, this will set the service
48 configuration file.
49 This option is mutually exclusive with {option}`settings`.
50 This option is deprecated. Use {option}`settings` instead.
51 '';
52 };
53
54 settings = mkOption {
55 type = format.type;
56 default = { };
57 description = ''
58 Configuration for Radicale. See
59 <https://radicale.org/v3.html#configuration>.
60 This option is mutually exclusive with {option}`config`.
61 '';
62 example = literalExpression ''
63 server = {
64 hosts = [ "0.0.0.0:5232" "[::]:5232" ];
65 };
66 auth = {
67 type = "htpasswd";
68 htpasswd_filename = "/etc/radicale/users";
69 htpasswd_encryption = "bcrypt";
70 };
71 storage = {
72 filesystem_folder = "/var/lib/radicale/collections";
73 };
74 '';
75 };
76
77 rights = mkOption {
78 type = format.type;
79 description = ''
80 Configuration for Radicale's rights file. See
81 <https://radicale.org/v3.html#authentication-and-rights>.
82 This option only works in conjunction with {option}`settings`.
83 Setting this will also set {option}`settings.rights.type` and
84 {option}`settings.rights.file` to appropriate values.
85 '';
86 default = { };
87 example = literalExpression ''
88 root = {
89 user = ".+";
90 collection = "";
91 permissions = "R";
92 };
93 principal = {
94 user = ".+";
95 collection = "{user}";
96 permissions = "RW";
97 };
98 calendars = {
99 user = ".+";
100 collection = "{user}/[^/]+";
101 permissions = "rw";
102 };
103 '';
104 };
105
106 extraArgs = mkOption {
107 type = types.listOf types.str;
108 default = [ ];
109 description = "Extra arguments passed to the Radicale daemon.";
110 };
111 };
112
113 config = mkIf cfg.enable {
114 assertions = [
115 {
116 assertion = cfg.settings == { } || cfg.config == "";
117 message = ''
118 The options services.radicale.config and services.radicale.settings
119 are mutually exclusive.
120 '';
121 }
122 {
123 assertion = cfg.config != "" || (cfg.settings ? auth && cfg.settings.auth ? type);
124 message = ''
125 Radicale 3.5.0 changed the default value for `auth.type` from `none` to `denyall`.
126 You probably don't want `denyall`, so please set this explicitly.
127 https://github.com/Kozea/Radicale/blob/v3.5.0/CHANGELOG.md
128 '';
129 }
130 ];
131
132 warnings =
133 optional (cfg.package == null && versionOlder config.system.stateVersion "17.09") ''
134 The configuration and storage formats of your existing Radicale
135 installation might be incompatible with the newest version.
136 For upgrade instructions see
137 https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
138 Set services.radicale.package to suppress this warning.
139 ''
140 ++ optional (cfg.package == null && versionOlder config.system.stateVersion "20.09") ''
141 The configuration format of your existing Radicale installation might be
142 incompatible with the newest version. For upgrade instructions see
143 https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
144 Set services.radicale.package to suppress this warning.
145 ''
146 ++ optional (cfg.config != "") ''
147 The option services.radicale.config is deprecated.
148 Use services.radicale.settings instead.
149 '';
150
151 services.radicale.settings.rights = mkIf (cfg.rights != { }) {
152 type = "from_file";
153 file = toString rightsFile;
154 };
155
156 environment.systemPackages = [ pkg ];
157
158 users.users.radicale = {
159 isSystemUser = true;
160 group = "radicale";
161 };
162
163 users.groups.radicale = { };
164
165 systemd.services.radicale = {
166 description = "A Simple Calendar and Contact Server";
167 after = [ "network.target" ];
168 requires = [ "network.target" ];
169 wantedBy = [ "multi-user.target" ];
170 serviceConfig = {
171 ExecStart = concatStringsSep " " (
172 [
173 "${pkg}/bin/radicale"
174 "-C"
175 confFile
176 ]
177 ++ (map escapeShellArg cfg.extraArgs)
178 );
179 User = "radicale";
180 Group = "radicale";
181 StateDirectory = "radicale/collections";
182 StateDirectoryMode = "0750";
183 # Hardening
184 CapabilityBoundingSet = [ "" ];
185 DeviceAllow = [
186 "/dev/stdin"
187 "/dev/urandom"
188 ];
189 DevicePolicy = "strict";
190 IPAddressAllow = mkIf bindLocalhost "localhost";
191 IPAddressDeny = mkIf bindLocalhost "any";
192 LockPersonality = true;
193 MemoryDenyWriteExecute = true;
194 NoNewPrivileges = true;
195 PrivateDevices = true;
196 PrivateTmp = true;
197 PrivateUsers = true;
198 ProcSubset = "pid";
199 ProtectClock = true;
200 ProtectControlGroups = true;
201 ProtectHome = true;
202 ProtectHostname = true;
203 ProtectKernelLogs = true;
204 ProtectKernelModules = true;
205 ProtectKernelTunables = true;
206 ProtectProc = "invisible";
207 ProtectSystem = "strict";
208 ReadWritePaths = lib.optional (hasAttrByPath [
209 "storage"
210 "filesystem_folder"
211 ] cfg.settings) cfg.settings.storage.filesystem_folder;
212 RemoveIPC = true;
213 RestrictAddressFamilies = [
214 "AF_INET"
215 "AF_INET6"
216 "AF_UNIX" # To log with systemd
217 ];
218 RestrictNamespaces = true;
219 RestrictRealtime = true;
220 RestrictSUIDSGID = true;
221 SystemCallArchitectures = "native";
222 SystemCallFilter = [
223 "@system-service"
224 "~@privileged"
225 "~@resources"
226 ];
227 UMask = "0027";
228 WorkingDirectory = "/var/lib/radicale";
229 };
230 };
231 };
232
233 meta.maintainers = with lib.maintainers; [ dotlambda ];
234}