{ config, lib, pkgs, ... }: let cfg = config.mj.services.nsd-acme; mkHook = zone: let fullZone = "_acme-endpoint.${zone}"; in pkgs.writeShellScript "nsd-acme-hook" '' set -euo pipefail METHOD=$1 TYPE=$2 AUTH=$5 NOW=$(date +%y%m%d%H%M) DIR="/var/lib/nsd/acmezones" [ "$TYPE" != "dns-01" ] && { exit 1; } write_zone() { cat < "$DIR/${fullZone}.zone" nsd-control addzone ${fullZone} acme ;; done) cleanup ;; failed) cleanup ;; esac ''; in { options.mj.services.nsd-acme = with lib.types; { enable = lib.mkEnableOption "enable acme certs via nsd"; zones = lib.mkOption { default = {}; type = attrsOf (submodule ( {name, ...}: { options = { accountKey = lib.mkOption {type = path;}; days = lib.mkOption { type = int; default = 30; }; staging = lib.mkOption { type = bool; default = false; }; # Warning: paths here are here to be read from. Changing them will # not place the files somewhere else. certFile = lib.mkOption { type = str; default = "/var/lib/nsd-acme/${name}/${name}/cert.pem"; }; keyFile = lib.mkOption { type = str; default = "/var/lib/nsd-acme/${name}/private/${name}/key.pem"; }; }; } )); }; }; # TODO assert services.nsd.enable config = lib.mkIf cfg.enable { services.nsd.remoteControl.enable = true; services.nsd.extraConfig = '' pattern: name: "acme" zonefile: "/var/lib/nsd/acmezones/%s.zone" ''; systemd = { tmpfiles.rules = ["d /var/lib/nsd/acmezones 0755 nsd nsd -"]; services = { nsd-control-setup = { requiredBy = ["nsd.service"]; before = ["nsd.service"]; unitConfig.ConditionPathExists = let rc = config.services.nsd.remoteControl; in [ "|!${rc.controlKeyFile}" "|!${rc.controlCertFile}" "|!${rc.serverKeyFile}" "|!${rc.serverCertFile}" ]; serviceConfig = { Type = "oneshot"; UMask = 0077; }; script = '' ${pkgs.nsd}/bin/nsd-control-setup chown nsd:nsd /etc/nsd/nsd_{control,server}.{key,pem} ''; path = [pkgs.openssl]; }; } // lib.mapAttrs' ( zone: cfg: lib.nameValuePair "nsd-acme-${zone}" { description = "dns-01 acme update for ${zone}"; path = [pkgs.openssh pkgs.nsd]; preStart = '' mkdir -p "$STATE_DIRECTORY/private" ln -sf "$CREDENTIALS_DIRECTORY/letsencrypt-account-key" \ "$STATE_DIRECTORY/private/key.pem" ''; serviceConfig = { ExecStart = let hook = mkHook zone; days = builtins.toString cfg.days; in "${pkgs.uacme}/bin/uacme -c \${STATE_DIRECTORY} --verbose --days ${days} --hook ${hook} ${lib.optionalString cfg.staging "--staging"} issue ${zone}"; UMask = "0022"; User = "nsd"; Group = "nsd"; StateDirectory = "nsd-acme/${zone}"; LoadCredential = ["letsencrypt-account-key:${cfg.accountKey}"]; ReadWritePaths = ["/var/lib/nsd/acmezones"]; SuccessExitStatus = [0 1]; # from nixos/modules/security/acme/default.nix ProtectSystem = "strict"; PrivateTmp = true; CapabilityBoundingSet = [""]; DevicePolicy = "closed"; LockPersonality = true; MemoryDenyWriteExecute = true; NoNewPrivileges = true; PrivateDevices = true; ProtectClock = true; ProtectHome = true; ProtectHostname = true; ProtectControlGroups = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProcSubset = "pid"; RemoveIPC = true; # "cannot get devices" #RestrictAddressFamilies = [ # "AF_INET" # "AF_INET6" #]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallFilter = [ # 1. allow a reasonable set of syscalls "@system-service @resources" # 2. and deny unreasonable ones "~@privileged" # 3. then allow the required subset within denied groups "@chown" ]; }; } ) cfg.zones; timers = lib.mapAttrs' ( zone: _: lib.nameValuePair "nsd-acme-${zone}" { description = "nsd-acme for zone ${zone}"; wantedBy = ["timers.target"]; timerConfig = { OnCalendar = "*-*-* 01:30"; }; after = ["network-online.target"]; } ) cfg.zones; }; mj.base.unitstatus.units = lib.mkIf config.mj.base.unitstatus.enable ( ["nsd-control-setup"] ++ map (z: "nsd-acme-${z}") (lib.attrNames cfg.zones) ); }; }