diff --git a/hosts/vno1-oh2/configuration.nix b/hosts/vno1-oh2/configuration.nix index 9923ec2..36820d8 100644 --- a/hosts/vno1-oh2/configuration.nix +++ b/hosts/vno1-oh2/configuration.nix @@ -70,6 +70,14 @@ myData.ports.exporters.node ]; + nsd-acme = { + enable = true; + zones."grafana.jakstys.lt" = { + accountKey = config.age.secrets.letsencrypt-account-key.path; + staging = true; + }; + }; + deployerbot = { main = { enable = true; @@ -172,36 +180,13 @@ nsd = { enable = true; - remoteControl.enable = true; interfaces = ["0.0.0.0" "::"]; zones = { "jakstys.lt.".data = myData.jakstysLTZone; }; - extraConfig = '' - pattern: - name: "acme" - zonefile: "/var/lib/nsd/zones/%s." - ''; }; }; - systemd.services.nsd-control-setup = { - requiredBy = ["nsd.service"]; - before = ["nsd.service"]; - unitConfig.ConditionPathExists = [ - "|!/etc/nsd/nsd_control.key" - "|!/etc/nsd/nsd_control.pem" - "|!/etc/nsd/nsd_server.key" - "|!/etc/nsd/nsd_server.pem" - ]; - serviceConfig = { - Type = "oneshot"; - UMask = 0077; - }; - path = [pkgs.nsd pkgs.openssl]; - script = ''nsd-control-setup''; - }; - networking = { hostId = "f9117e1b"; hostName = "vno1-oh2"; diff --git a/modules/services/default.nix b/modules/services/default.nix index 173d515..03a2215 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -7,6 +7,7 @@ imports = [ ./deployerbot ./friendlyport + ./nsd-acme ./postfix ./syncthing ./zfsunlock diff --git a/modules/services/nsd-acme/default.nix b/modules/services/nsd-acme/default.nix new file mode 100644 index 0000000..c38b5d6 --- /dev/null +++ b/modules/services/nsd-acme/default.nix @@ -0,0 +1,157 @@ +{ + config, + lib, + pkgs, + ... +}: let + mkHook = zone: let + rc = config.services.nsd.remoteControl; + fullZone = "_acme-endpoint.${zone}"; + nsdconf = ''"$RUNTIME_DIRECTORY"/nsd.conf''; + 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/zones" + + sed \ + -e "s~${rc.controlKeyFile}~$CREDENTIALS_DIRECTORY/nsd_control.key~" \ + -e "s~${rc.controlCertFile}~$CREDENTIALS_DIRECTORY/nsd_control.pem~" \ + -e "s~${rc.serverKeyFile}~$CREDENTIALS_DIRECTORY/nsd_server.key~" \ + -e "s~${rc.serverCertFile}~$CREDENTIALS_DIRECTORY/nsd_server.pem~" \ + /etc/nsd/nsd.conf > ${nsdconf} + + [ "$TYPE" != "dns-01" ] && { echo "Skipping $TYPE"; exit 1; } + + write_zone() { + cat < "$DIR/${fullZone}.acme" + + echo "Activating ${fullZone}" + nsd-control -c ${nsdconf} addzone ${fullZone} acme + ;; + done) + echo "ACME request successful, cleaning up" + cleanup + ;; + failed) + echo "ACME request failed, cleaning up" + 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 ( + {...}: { + options = { + accountKey = lib.mkOption {type = path;}; + days = lib.mkOption { + type = int; + default = 30; + }; + staging = lib.mkOption { + type = bool; + default = false; + }; + }; + } + )); + }; + }; + + # TODO assert services.nsd.enable + config = lib.mkIf config.mj.services.nsd-acme.enable { + services.nsd.remoteControl.enable = true; + services.nsd.extraConfig = '' + pattern: + name: "acme" + zonefile: "/var/lib/nsd/zones/%s.acme" + ''; + + systemd.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; + ExecStart = "${pkgs.nsd}/bin/nsd-control-setup"; + }; + path = [pkgs.openssl]; + }; + } + // lib.mapAttrs' + ( + zone: cfg: let + sanitized = lib.strings.sanitizeDerivationName zone; + in + lib.nameValuePair "nsd-acme-${sanitized}" { + description = "dns-01 acme update for ${zone}"; + path = [pkgs.openssh pkgs.nsd]; + preStart = '' + mkdir -p "$STATE_DIRECTORY/private" + ln -sf "$CREDENTIALS_DIRECTORY/letsenctypt-account.key" \ + "$STATE_DIRECTORY/private/key.pem" + ''; + serviceConfig = { + ExecStart = let + hook = mkHook zone; + days = "--days ${builtins.toString cfg.days}"; + staging = + if cfg.staging + then "--staging" + else ""; + in "${pkgs.uacme} --verbose --days ${days} --hook ${hook} ${staging} issue ${zone}"; + DynamicUser = "yes"; + StateDirectory = "nsd-acme/${sanitized}"; + RuntimeDirectory = "nsd-acme/${sanitized}"; + LoadCredential = let + rc = config.services.nsd.remoteControl; + in [ + "nsd_control.key:${rc.controlKeyFile}" + "nsd_control.pem:${rc.controlCertFile}" + "nsd_server.key:${rc.serverKeyFile}" + "nsd_server.pem:${rc.serverCertFile}" + "letsencrypt-account.key:${cfg.accountKey}" + ]; + }; + } + ) + config.mj.services.nsd-acme.zones; + + mj.base.unitstatus.units = lib.mkIf config.mj.base.unitstatus.enable ["nsd-control-setup"]; + }; +}