at 16.09-beta 17 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.taskserver; 7 8 taskd = "${pkgs.taskserver}/bin/taskd"; 9 10 mkVal = val: 11 if val == true then "true" 12 else if val == false then "false" 13 else if isList val then concatStringsSep ", " val 14 else toString val; 15 16 mkConfLine = key: val: let 17 result = "${key} = ${mkVal val}"; 18 in optionalString (val != null && val != []) result; 19 20 mkManualPkiOption = desc: mkOption { 21 type = types.nullOr types.path; 22 default = null; 23 description = desc + '' 24 <note><para> 25 Setting this option will prevent automatic CA creation and handling. 26 </para></note> 27 ''; 28 }; 29 30 manualPkiOptions = { 31 ca.cert = mkManualPkiOption '' 32 Fully qualified path to the CA certificate. 33 ''; 34 35 server.cert = mkManualPkiOption '' 36 Fully qualified path to the server certificate. 37 ''; 38 39 server.crl = mkManualPkiOption '' 40 Fully qualified path to the server certificate revocation list. 41 ''; 42 43 server.key = mkManualPkiOption '' 44 Fully qualified path to the server key. 45 ''; 46 }; 47 48 mkAutoDesc = preamble: '' 49 ${preamble} 50 51 <note><para> 52 This option is for the automatically handled CA and will be ignored if any 53 of the <option>services.taskserver.pki.manual.*</option> options are set. 54 </para></note> 55 ''; 56 57 mkExpireOption = desc: mkOption { 58 type = types.nullOr types.int; 59 default = null; 60 example = 365; 61 apply = val: if isNull val then -1 else val; 62 description = mkAutoDesc '' 63 The expiration time of ${desc} in days or <literal>null</literal> for no 64 expiration time. 65 ''; 66 }; 67 68 autoPkiOptions = { 69 bits = mkOption { 70 type = types.int; 71 default = 4096; 72 example = 2048; 73 description = mkAutoDesc "The bit size for generated keys."; 74 }; 75 76 expiration = { 77 ca = mkExpireOption "the CA certificate"; 78 server = mkExpireOption "the server certificate"; 79 client = mkExpireOption "client certificates"; 80 crl = mkExpireOption "the certificate revocation list (CRL)"; 81 }; 82 }; 83 84 needToCreateCA = let 85 notFound = path: let 86 dotted = concatStringsSep "." path; 87 in throw "Can't find option definitions for path `${dotted}'."; 88 findPkiDefinitions = path: attrs: let 89 mkSublist = key: val: let 90 newPath = path ++ singleton key; 91 in if isOption val 92 then attrByPath newPath (notFound newPath) cfg.pki.manual 93 else findPkiDefinitions newPath val; 94 in flatten (mapAttrsToList mkSublist attrs); 95 in all isNull (findPkiDefinitions [] manualPkiOptions); 96 97 configFile = pkgs.writeText "taskdrc" ('' 98 # systemd related 99 daemon = false 100 log = - 101 102 # logging 103 ${mkConfLine "debug" cfg.debug} 104 ${mkConfLine "ip.log" cfg.ipLog} 105 106 # general 107 ${mkConfLine "ciphers" cfg.ciphers} 108 ${mkConfLine "confirmation" cfg.confirmation} 109 ${mkConfLine "extensions" cfg.extensions} 110 ${mkConfLine "queue.size" cfg.queueSize} 111 ${mkConfLine "request.limit" cfg.requestLimit} 112 113 # client 114 ${mkConfLine "client.allow" cfg.allowedClientIDs} 115 ${mkConfLine "client.deny" cfg.disallowedClientIDs} 116 117 # server 118 server = ${cfg.listenHost}:${toString cfg.listenPort} 119 ${mkConfLine "trust" cfg.trust} 120 121 # PKI options 122 ${if needToCreateCA then '' 123 ca.cert = ${cfg.dataDir}/keys/ca.cert 124 server.cert = ${cfg.dataDir}/keys/server.cert 125 server.key = ${cfg.dataDir}/keys/server.key 126 server.crl = ${cfg.dataDir}/keys/server.crl 127 '' else '' 128 ca.cert = ${cfg.pki.ca.cert} 129 server.cert = ${cfg.pki.server.cert} 130 server.key = ${cfg.pki.server.key} 131 server.crl = ${cfg.pki.server.crl} 132 ''} 133 '' + cfg.extraConfig); 134 135 orgOptions = { name, ... }: { 136 options.users = mkOption { 137 type = types.uniq (types.listOf types.str); 138 default = []; 139 example = [ "alice" "bob" ]; 140 description = '' 141 A list of user names that belong to the organization. 142 ''; 143 }; 144 145 options.groups = mkOption { 146 type = types.listOf types.str; 147 default = []; 148 example = [ "workers" "slackers" ]; 149 description = '' 150 A list of group names that belong to the organization. 151 ''; 152 }; 153 }; 154 155 certtool = "${pkgs.gnutls.bin}/bin/certtool"; 156 157 nixos-taskserver = pkgs.buildPythonPackage { 158 name = "nixos-taskserver"; 159 namePrefix = ""; 160 161 src = pkgs.runCommand "nixos-taskserver-src" {} '' 162 mkdir -p "$out" 163 cat "${pkgs.substituteAll { 164 src = ./helper-tool.py; 165 inherit taskd certtool; 166 inherit (cfg) dataDir user group fqdn; 167 certBits = cfg.pki.auto.bits; 168 clientExpiration = cfg.pki.auto.expiration.client; 169 crlExpiration = cfg.pki.auto.expiration.crl; 170 }}" > "$out/main.py" 171 cat > "$out/setup.py" <<EOF 172 from setuptools import setup 173 setup(name="nixos-taskserver", 174 py_modules=["main"], 175 install_requires=["Click"], 176 entry_points="[console_scripts]\\nnixos-taskserver=main:cli") 177 EOF 178 ''; 179 180 propagatedBuildInputs = [ pkgs.pythonPackages.click ]; 181 }; 182 183in { 184 options = { 185 services.taskserver = { 186 enable = mkOption { 187 type = types.bool; 188 default = false; 189 example = true; 190 description = '' 191 Whether to enable the Taskwarrior server. 192 193 More instructions about NixOS in conjuction with Taskserver can be 194 found in the NixOS manual at 195 <olink targetdoc="manual" targetptr="module-taskserver"/>. 196 ''; 197 }; 198 199 user = mkOption { 200 type = types.str; 201 default = "taskd"; 202 description = "User for Taskserver."; 203 }; 204 205 group = mkOption { 206 type = types.str; 207 default = "taskd"; 208 description = "Group for Taskserver."; 209 }; 210 211 dataDir = mkOption { 212 type = types.path; 213 default = "/var/lib/taskserver"; 214 description = "Data directory for Taskserver."; 215 }; 216 217 ciphers = mkOption { 218 type = types.nullOr (types.separatedString ":"); 219 default = null; 220 example = "NORMAL:-VERS-SSL3.0"; 221 description = let 222 url = "https://gnutls.org/manual/html_node/Priority-Strings.html"; 223 in '' 224 List of GnuTLS ciphers to use. See the GnuTLS documentation about 225 priority strings at <link xlink:href="${url}"/> for full details. 226 ''; 227 }; 228 229 organisations = mkOption { 230 type = types.attrsOf (types.submodule orgOptions); 231 default = {}; 232 example.myShinyOrganisation.users = [ "alice" "bob" ]; 233 example.myShinyOrganisation.groups = [ "staff" "outsiders" ]; 234 example.yetAnotherOrganisation.users = [ "foo" "bar" ]; 235 description = '' 236 An attribute set where the keys name the organisation and the values 237 are a set of lists of <option>users</option> and 238 <option>groups</option>. 239 ''; 240 }; 241 242 confirmation = mkOption { 243 type = types.bool; 244 default = true; 245 description = '' 246 Determines whether certain commands are confirmed. 247 ''; 248 }; 249 250 debug = mkOption { 251 type = types.bool; 252 default = false; 253 description = '' 254 Logs debugging information. 255 ''; 256 }; 257 258 extensions = mkOption { 259 type = types.nullOr types.path; 260 default = null; 261 description = '' 262 Fully qualified path of the Taskserver extension scripts. 263 Currently there are none. 264 ''; 265 }; 266 267 ipLog = mkOption { 268 type = types.bool; 269 default = false; 270 description = '' 271 Logs the IP addresses of incoming requests. 272 ''; 273 }; 274 275 queueSize = mkOption { 276 type = types.int; 277 default = 10; 278 description = '' 279 Size of the connection backlog, see <citerefentry> 280 <refentrytitle>listen</refentrytitle> 281 <manvolnum>2</manvolnum> 282 </citerefentry>. 283 ''; 284 }; 285 286 requestLimit = mkOption { 287 type = types.int; 288 default = 1048576; 289 description = '' 290 Size limit of incoming requests, in bytes. 291 ''; 292 }; 293 294 allowedClientIDs = mkOption { 295 type = with types; loeOf (either (enum ["all" "none"]) str); 296 default = []; 297 example = [ "[Tt]ask [2-9]+" ]; 298 description = '' 299 A list of regular expressions that are matched against the reported 300 client id (such as <literal>task 2.3.0</literal>). 301 302 The values <literal>all</literal> or <literal>none</literal> have 303 special meaning. Overidden by any entry in the option 304 <option>services.taskserver.disallowedClientIDs</option>. 305 ''; 306 }; 307 308 disallowedClientIDs = mkOption { 309 type = with types; loeOf (either (enum ["all" "none"]) str); 310 default = []; 311 example = [ "[Tt]ask [2-9]+" ]; 312 description = '' 313 A list of regular expressions that are matched against the reported 314 client id (such as <literal>task 2.3.0</literal>). 315 316 The values <literal>all</literal> or <literal>none</literal> have 317 special meaning. Any entry here overrides those in 318 <option>services.taskserver.allowedClientIDs</option>. 319 ''; 320 }; 321 322 listenHost = mkOption { 323 type = types.str; 324 default = "localhost"; 325 example = "::"; 326 description = '' 327 The address (IPv4, IPv6 or DNS) to listen on. 328 329 If the value is something else than <literal>localhost</literal> the 330 port defined by <option>listenPort</option> is automatically added to 331 <option>networking.firewall.allowedTCPPorts</option>. 332 ''; 333 }; 334 335 listenPort = mkOption { 336 type = types.int; 337 default = 53589; 338 description = '' 339 Port number of the Taskserver. 340 ''; 341 }; 342 343 fqdn = mkOption { 344 type = types.str; 345 default = "localhost"; 346 description = '' 347 The fully qualified domain name of this server, which is also used 348 as the common name in the certificates. 349 ''; 350 }; 351 352 trust = mkOption { 353 type = types.enum [ "allow all" "strict" ]; 354 default = "strict"; 355 description = '' 356 Determines how client certificates are validated. 357 358 The value <literal>allow all</literal> performs no client 359 certificate validation. This is not recommended. The value 360 <literal>strict</literal> causes the client certificate to be 361 validated against a CA. 362 ''; 363 }; 364 365 pki.manual = manualPkiOptions; 366 pki.auto = autoPkiOptions; 367 368 extraConfig = mkOption { 369 type = types.lines; 370 default = ""; 371 example = "client.cert = /tmp/debugging.cert"; 372 description = '' 373 Extra lines to append to the taskdrc configuration file. 374 ''; 375 }; 376 }; 377 }; 378 379 config = mkMerge [ 380 (mkIf cfg.enable { 381 environment.systemPackages = [ pkgs.taskserver nixos-taskserver ]; 382 383 users.users = optional (cfg.user == "taskd") { 384 name = "taskd"; 385 uid = config.ids.uids.taskd; 386 description = "Taskserver user"; 387 group = cfg.group; 388 }; 389 390 users.groups = optional (cfg.group == "taskd") { 391 name = "taskd"; 392 gid = config.ids.gids.taskd; 393 }; 394 395 systemd.services.taskserver-init = { 396 wantedBy = [ "taskserver.service" ]; 397 before = [ "taskserver.service" ]; 398 description = "Initialize Taskserver Data Directory"; 399 400 preStart = '' 401 mkdir -m 0770 -p "${cfg.dataDir}" 402 chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}" 403 ''; 404 405 script = '' 406 ${taskd} init 407 echo "include ${configFile}" > "${cfg.dataDir}/config" 408 touch "${cfg.dataDir}/.is_initialized" 409 ''; 410 411 environment.TASKDDATA = cfg.dataDir; 412 413 unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized"; 414 415 serviceConfig.Type = "oneshot"; 416 serviceConfig.User = cfg.user; 417 serviceConfig.Group = cfg.group; 418 serviceConfig.PermissionsStartOnly = true; 419 serviceConfig.PrivateNetwork = true; 420 serviceConfig.PrivateDevices = true; 421 serviceConfig.PrivateTmp = true; 422 }; 423 424 systemd.services.taskserver = { 425 description = "Taskwarrior Server"; 426 427 wantedBy = [ "multi-user.target" ]; 428 after = [ "network.target" ]; 429 430 environment.TASKDDATA = cfg.dataDir; 431 432 preStart = let 433 jsonOrgs = builtins.toJSON cfg.organisations; 434 jsonFile = pkgs.writeText "orgs.json" jsonOrgs; 435 helperTool = "${nixos-taskserver}/bin/nixos-taskserver"; 436 in "${helperTool} process-json '${jsonFile}'"; 437 438 serviceConfig = { 439 ExecStart = "@${taskd} taskd server"; 440 ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; 441 Restart = "on-failure"; 442 PermissionsStartOnly = true; 443 PrivateTmp = true; 444 PrivateDevices = true; 445 User = cfg.user; 446 Group = cfg.group; 447 }; 448 }; 449 }) 450 (mkIf (cfg.enable && needToCreateCA) { 451 systemd.services.taskserver-ca = { 452 wantedBy = [ "taskserver.service" ]; 453 after = [ "taskserver-init.service" ]; 454 before = [ "taskserver.service" ]; 455 description = "Initialize CA for TaskServer"; 456 serviceConfig.Type = "oneshot"; 457 serviceConfig.UMask = "0077"; 458 serviceConfig.PrivateNetwork = true; 459 serviceConfig.PrivateTmp = true; 460 461 script = '' 462 silent_certtool() { 463 if ! output="$("${certtool}" "$@" 2>&1)"; then 464 echo "GNUTLS certtool invocation failed with output:" >&2 465 echo "$output" >&2 466 fi 467 } 468 469 mkdir -m 0700 -p "${cfg.dataDir}/keys" 470 chown root:root "${cfg.dataDir}/keys" 471 472 if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then 473 silent_certtool -p \ 474 --bits ${toString cfg.pki.auto.bits} \ 475 --outfile "${cfg.dataDir}/keys/ca.key" 476 silent_certtool -s \ 477 --template "${pkgs.writeText "taskserver-ca.template" '' 478 cn = ${cfg.fqdn} 479 expiration_days = ${toString cfg.pki.auto.expiration.ca} 480 cert_signing_key 481 ca 482 ''}" \ 483 --load-privkey "${cfg.dataDir}/keys/ca.key" \ 484 --outfile "${cfg.dataDir}/keys/ca.cert" 485 486 chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert" 487 chmod g+r "${cfg.dataDir}/keys/ca.cert" 488 fi 489 490 if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then 491 silent_certtool -p \ 492 --bits ${toString cfg.pki.auto.bits} \ 493 --outfile "${cfg.dataDir}/keys/server.key" 494 495 silent_certtool -c \ 496 --template "${pkgs.writeText "taskserver-cert.template" '' 497 cn = ${cfg.fqdn} 498 expiration_days = ${toString cfg.pki.auto.expiration.server} 499 tls_www_server 500 encryption_key 501 signing_key 502 ''}" \ 503 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ 504 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ 505 --load-privkey "${cfg.dataDir}/keys/server.key" \ 506 --outfile "${cfg.dataDir}/keys/server.cert" 507 508 chgrp "${cfg.group}" \ 509 "${cfg.dataDir}/keys/server.key" \ 510 "${cfg.dataDir}/keys/server.cert" 511 512 chmod g+r \ 513 "${cfg.dataDir}/keys/server.key" \ 514 "${cfg.dataDir}/keys/server.cert" 515 fi 516 517 if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then 518 silent_certtool --generate-crl \ 519 --template "${pkgs.writeText "taskserver-crl.template" '' 520 expiration_days = ${toString cfg.pki.auto.expiration.crl} 521 ''}" \ 522 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ 523 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ 524 --outfile "${cfg.dataDir}/keys/server.crl" 525 526 chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl" 527 chmod g+r "${cfg.dataDir}/keys/server.crl" 528 fi 529 530 chmod go+x "${cfg.dataDir}/keys" 531 ''; 532 }; 533 }) 534 (mkIf (cfg.enable && cfg.listenHost != "localhost") { 535 networking.firewall.allowedTCPPorts = [ cfg.listenPort ]; 536 }) 537 ]; 538 539 meta.doc = ./doc.xml; 540}