From 1b98c83d3fc526ccf115f9345e09611b5400b90f Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:59:05 -0500 Subject: [PATCH] breaking up mtls with better wrappers --- modules/features/mtls.nix | 504 ------------------------ modules/features/mtls/default.nix | 167 ++++++++ modules/features/mtls/mtls-wrappers.nix | 125 ++++++ modules/hosts/soteria/default.nix | 20 +- 4 files changed, 302 insertions(+), 514 deletions(-) delete mode 100644 modules/features/mtls.nix create mode 100644 modules/features/mtls/default.nix create mode 100644 modules/features/mtls/mtls-wrappers.nix diff --git a/modules/features/mtls.nix b/modules/features/mtls.nix deleted file mode 100644 index d1b2faf..0000000 --- a/modules/features/mtls.nix +++ /dev/null @@ -1,504 +0,0 @@ -{ self, inputs, lib, ... }: -let - # Options that will be in common between the nixos module and the home-manager module. - mkOpts = config: let cfg = config.mtls; in { - enable = lib.mkEnableOption "Enable mTLS"; - subject = lib.mkOption { - description = "The Common Name, DNS Name, or IP address that will be set as the Subject Common Name for the certificate. If no Subject Alternative Names (SANs) are configured (via the --san flag) then the subject will be set as the only SAN."; - type = lib.types.str; - }; - certDir = lib.mkOption { - description = "String path to the directory where the certs will be stored"; - type = lib.types.str; - }; - caFile = lib.mkOption { - description = "String path for the root CA file"; - type = lib.types.str; - default = "${cfg.certDir}/root_ca.crt"; - }; - keyFile = lib.mkOption { - description = "String path for the private key"; - type = lib.types.str; - default = "${cfg.certDir}/key.pem"; - }; - certFile = lib.mkOption { - description = "String path for the public cert"; - type = lib.types.str; - default = "${cfg.certDir}/cert.pem"; - }; - bundleFile = lib.mkOption { - description = "String path for the mTLS key bundle"; - type = lib.types.str; - default = "${cfg.certDir}/mtls.pem"; - }; - san = lib.mkOption { - description = "List of SAN to give the mTLS cert"; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - provisioner = lib.mkOption { - type = lib.types.str; - default = "admin"; - }; - lifetime = lib.mkOption { - type = lib.types.str; - default = "24h"; - }; - renew = { - enable = lib.mkOption { - description = "Enable automatic mTLS certificate renewal using a systemd timer."; - type = lib.types.bool; - default = cfg.enable; - }; - onCalendar = lib.mkOption { - description = "systemd OnCalendar schedule for mTLS certificate renewal checks."; - type = lib.types.str; - default = "*:1/15"; - }; - randomizedDelaySec = lib.mkOption { - description = "Randomized delay added to renewal timer runs to avoid synchronized renewals."; - type = lib.types.str; - default = "5m"; - }; - user = lib.mkOption { - description = "User account to run the mTLS renewal service as."; - type = lib.types.str; - default = "root"; - }; - group = lib.mkOption { - description = "Group to run the mTLS renewal service as. Defaults to the configured renewal user."; - type = lib.types.nullOr lib.types.str; - default = "mtls"; - }; - reloadUnits = lib.mkOption { - description = "systemd units to try-reload-or-restart after a successful certificate renewal."; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - postCommands = lib.mkOption { - description = "Shell commands to run after a successful certificate renewal."; - type = lib.types.listOf lib.types.lines; - default = [ ]; - }; - }; - }; - - mkMtlsGenerateScript = { - pkgs, - subject, - provisioner, - san, - certFile, - keyFile, - bundleFile, - lifetime, - user, - group, - }: - let - sanArgs = lib.concatMapStringsSep " " (s: "--san \"${s}\"") san; - in - pkgs.writeShellApplication { - name = "mtls-generate"; - runtimeInputs = with pkgs; [ coreutils step-cli ]; - text = '' - set -euo pipefail - step ca certificate ${subject} ${certFile} ${keyFile} \ - --provisioner ${provisioner} \ - --not-before=-5m --not-after=${lifetime} \ - ${sanArgs} \ - "$@" - (umask 077; cat ${certFile} ${keyFile} > ${bundleFile}) - chown ${user}:${group} ${certFile} ${keyFile} ${bundleFile} - chmod 640 ${certFile} ${keyFile} ${bundleFile} - printf '\033[32m✔\033[0m \033[1mmTLS Bundle:\033[0m %s\n' ${lib.escapeShellArg bundleFile} - ''; - }; - -in -{ - flake.modules.nixos.mtls = { config, lib, pkgs, ... }: - let - cfg = config.mtls; - mtlsRenewWrapper = inputs.self.wrappers.mtlsRenew.apply { - inherit pkgs; - inherit (cfg) certDir keyFile certFile bundleFile; - inherit (cfg.renew) user group reloadUnits postCommands; - systemd = { - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - serviceConfig = { - Type = "oneshot"; - User = cfg.renew.user; - Group = cfg.renew.group; - }; - } // lib.optionalAttrs cfg.renew.enable { - startAt = cfg.renew.onCalendar; - }; - }; - in - { - options.mtls = (mkOpts config) // { - certDir = lib.mkOption { - description = "String path to where the mtls certs will be stored."; - type = lib.types.str; - default = "/etc/step-ca/certs"; - }; - bootstrap = { - enable = lib.mkOption { - description = "Enable initial mTLS issuance when cert material is missing or invalid."; - type = lib.types.bool; - default = false; - }; - wantedBy = lib.mkOption { - description = "systemd targets that should pull in mtls-bootstrap.service."; - type = with lib.types; listOf str; - default = [ "multi-user.target" ]; - }; - after = lib.mkOption { - description = "systemd units/targets that mtls-bootstrap.service should run after."; - type = with lib.types; listOf str; - default = [ "network-online.target" ]; - }; - wants = lib.mkOption { - description = "systemd units/targets that mtls-bootstrap.service should pull in."; - type = with lib.types; listOf str; - default = [ "network-online.target" ]; - }; - provisionerPasswordFile = lib.mkOption { - description = "Optional path passed to mtls-generate as --provisioner-password-file for noninteractive issuance."; - type = lib.types.nullOr lib.types.str; - default = null; - }; - }; - certReaders = lib.mkOption { - description = ""; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - }; - - config = lib.mkIf cfg.enable { - users.groups.certReaders = { - name = cfg.renew.group; - members = cfg.certReaders; - }; - - environment.systemPackages = with pkgs; lib.optionals cfg.enable [ - # step-cli - (mkMtlsGenerateScript { - inherit pkgs; - inherit (cfg) subject provisioner san certFile keyFile bundleFile lifetime; - inherit (cfg.renew) user group; - }) - (inputs.self.wrappers.mtlsCheck.apply { - inherit pkgs; - inherit (cfg) bundleFile; - }).wrapper - mtlsRenewWrapper.wrapper - ]; - - systemd.tmpfiles.rules = [ - "d ${cfg.certDir} 0750 ${cfg.renew.user} ${cfg.renew.group} -" - ]; - - systemd.packages = lib.mkIf cfg.renew.enable [ - mtlsRenewWrapper.outputs.systemd-system - ]; - - systemd.services.mtls-bootstrap = lib.mkIf cfg.bootstrap.enable { - description = "Issue initial mTLS certificate if missing or invalid"; - wantedBy = cfg.bootstrap.wantedBy; - after = cfg.bootstrap.after; - wants = cfg.bootstrap.wants; - path = with pkgs; [ coreutils step-cli ]; - serviceConfig = { - Type = "oneshot"; - User = cfg.renew.user; - Group = cfg.renew.group; - }; - script = '' - set -euo pipefail - - if [ -s "${cfg.certFile}" ] \ - && [ -s "${cfg.keyFile}" ] \ - && [ -s "${cfg.bundleFile}" ] \ - && step certificate inspect "${cfg.certFile}" >/dev/null 2>&1; then - echo "mTLS certificate already exists" - exit 0 - fi - - echo "Issuing initial mTLS certificate" - cmd=(/run/current-system/sw/bin/mtls-generate) - ${lib.optionalString (cfg.bootstrap.provisionerPasswordFile != null) '' - cmd+=(--provisioner-password-file "${cfg.bootstrap.provisionerPasswordFile}") - ''} - - "''${cmd[@]}" - ''; - }; - - systemd.timers.mtls-renew = lib.mkIf cfg.renew.enable { - wantedBy = [ "timers.target" ]; - timerConfig = { - Persistent = true; - AccuracySec = "1us"; - RandomizedDelaySec = cfg.renew.randomizedDelaySec; - }; - }; - }; - }; - - flake.modules.homeManager.mtls = { config, lib, pkgs, ... }: - let - cfg = config.mtls; - mtlsRenewWrapper = inputs.self.wrappers.mtlsRenew.apply { - inherit pkgs; - inherit (cfg) certDir keyFile certFile bundleFile; - inherit (cfg.renew) reloadUnits postCommands; - systemctlArgs = [ "--user" ]; - systemd = lib.optionalAttrs cfg.renew.enable { - startAt = cfg.renew.onCalendar; - }; - }; - in - { - options.mtls = (mkOpts config) // { - certDir = lib.mkOption { - description = "String path to where the mtls certs will be stored."; - type = lib.types.str; - default = "${config.home.homeDirectory}/.step/certs"; - }; - }; - - config = { - home.packages = with pkgs; lib.optionals cfg.enable [ - # step-cli - (mkMtlsGenerateScript { - inherit pkgs; - inherit (cfg) keyFile certFile bundleFile; - inherit (cfg) subject provisioner san lifetime; - inherit (cfg.renew) user group; - }) - (inputs.self.wrappers.mtlsCheck.apply { - inherit pkgs; - inherit (cfg) bundleFile; - }).wrapper - ]; - - systemd.user.tmpfiles.rules = lib.mkIf cfg.enable [ - "d ${cfg.certDir} 0700 - - -" # Ensure the cert directory exists and is writable by the user - ]; - - # Create the systemd service files for the user. - xdg.configFile = lib.mkIf cfg.renew.enable { - "systemd/user/mtls-renew.service".source = - "${mtlsRenewWrapper.outputs.systemd-user}/systemd/user/mtls-renew.service"; - "systemd/user/mtls-renew.timer".source = - "${mtlsRenewWrapper.outputs.systemd-user}/systemd/user/mtls-renew.timer"; - "systemd/user/mtls-renew.timer.d/override.conf".text = '' - [Timer] - Persistent=true - AccuracySec=1us - RandomizedDelaySec=${cfg.renew.randomizedDelaySec} - ''; - }; - - # Ensure the timer gets started - home.activation.mtlsRenewTimer = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if [ -n "$XDG_RUNTIME_DIR" ] && [ -S "$XDG_RUNTIME_DIR/systemd/private" ]; then - if [ "${lib.boolToString (cfg.enable && cfg.renew.enable)}" = "true" ]; then - run ${pkgs.systemd}/bin/systemctl --user daemon-reload - if ${pkgs.systemd}/bin/systemctl --user cat mtls-renew.timer >/dev/null 2>&1; then - run ${pkgs.systemd}/bin/systemctl --user enable --now mtls-renew.timer - else - verboseEcho "mtls-renew.timer unit file is not available; skipping enable" - fi - else - run ${pkgs.systemd}/bin/systemctl --user disable --now mtls-renew.timer || true - run ${pkgs.systemd}/bin/systemctl --user daemon-reload || true - fi - fi - ''; - }; - }; - - flake.wrappers = { - mtlsCheck = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { - options = { - bundleFile = lib.mkOption { - description = "String path for the mTLS key bundle"; - type = lib.types.str; - }; - }; - - config = { - binName = "mtls-check"; - # This pattern is necessary to wrap packages like openssl that provide more than one binary - package = config.pkgs.symlinkJoin { - name = "openssl"; - paths = [ config.pkgs.openssl.bin config.pkgs.openssl.man ]; - meta.mainProgram = "openssl"; - }; - args = [ - "x509" - "-noout" - "-in" config.bundleFile - "-subject" - "-issuer" - "-ext" "subjectAltName,extendedKeyUsage" - "-enddate" - ]; - }; - }); - - mtlsNeedsRenewal = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { - options = { - certFile = lib.mkOption { - description = "String path for the public cert"; - type = lib.types.str; - }; - }; - - config = { - binName = "mtls-needs-renewal"; - package = config.pkgs.step-cli; - preHook = '' - echo "Checking renewal status..." - ''; - args = [ "certificate" "needs-renewal" "${config.certFile}" ]; - }; - }); - - mtlsRenew = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { - imports = [ wlib.modules.systemd ]; - - options = { - certDir = lib.mkOption { - description = "String path to the directory where the certs will be stored"; - type = lib.types.str; - }; - keyFile = lib.mkOption { - description = "String path for the private key"; - type = lib.types.str; - default = "${config.certDir}/key.pem"; - }; - certFile = lib.mkOption { - description = "String path for the public cert"; - type = lib.types.str; - default = "${config.certDir}/cert.pem"; - }; - bundleFile = lib.mkOption { - description = "String path for the mTLS key bundle"; - type = lib.types.str; - default = "${config.certDir}/mtls.pem"; - }; - user = lib.mkOption { - description = "User that should own the renewed certificate files."; - type = lib.types.nullOr lib.types.str; - default = null; - }; - group = lib.mkOption { - description = "Group that should own the renewed certificate files."; - type = lib.types.nullOr lib.types.str; - default = null; - }; - reloadUnits = lib.mkOption { - description = "systemd units to try-reload-or-restart after a successful renewal."; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - postCommands = lib.mkOption { - description = "Shell commands to run after a successful renewal."; - type = lib.types.listOf lib.types.lines; - default = [ ]; - }; - systemctlArgs = lib.mkOption { - description = "Additional arguments to pass to systemctl when reloading units."; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - }; - - config = { - binName = "mtls-renew"; - package = let - systemctlCmd = "systemctl ${lib.escapeShellArgs config.systemctlArgs}"; - - hasReloadUnits = config.reloadUnits != [ ]; - renewReloadScript = lib.concatMapStringsSep "\n" (unit: '' - if ${systemctlCmd} --quiet is-active "${unit}"; then - ${systemctlCmd} try-reload-or-restart "${unit}" - fi - '') config.reloadUnits; - - hasPostCommands = config.postCommands != [ ]; - renewPostCommands = lib.concatStringsSep "\n" config.postCommands; - - hasOwnership = config.user != null && config.group != null; - in - config.pkgs.writeShellApplication { - name = "mtls-renew"; - runtimeInputs = with config.pkgs; [ coreutils step-cli systemd ]; - text = '' - set -euo pipefail - - YELLOW_BANG="\e[33m!\e[0m" - - force=0 - while [[ $# -gt 0 ]]; do - case $1 in - --force) - force=1 - shift - ;; - *) - echo -e "$YELLOW_BANG Warning: ignoring unrecognized argument '$1'" - exit 1 - ;; - esac - done - - if [[ $force -eq 0 ]] && ! step certificate needs-renewal "${config.certFile}"; then - echo "Skipping renew" - exit 0 - fi - - echo "Renewing mTLS certificate" - step ca renew --force "${config.certFile}" "${config.keyFile}" - (umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}") - - ${lib.optionalString hasOwnership '' - chown ${config.user}:${config.group} ${config.certFile} ${config.keyFile} ${config.bundleFile} - chmod 640 ${config.certFile} ${config.keyFile} ${config.bundleFile} - ''} - - ${lib.optionalString hasReloadUnits '' - echo "Reloading units: ${lib.concatStringsSep ", " config.reloadUnits}" - ${renewReloadScript} - ''} - - ${lib.optionalString hasPostCommands '' - echo "Running post commands" - ${renewPostCommands} - ''} - ''; - }; - extraPackages = [ - config.pkgs.step-cli - ]; - systemd = { - description = "Renew the mTLS certificate when Smallstep marks it ready"; - documentation = [ - "https://smallstep.com/docs/step-ca/certificate-authority-server-production" - ]; - startLimitIntervalSec = 0; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - serviceConfig.Type = lib.mkDefault "oneshot"; - # serviceConfig.ExecCondition = ""; - }; - }; - }); - }; -} \ No newline at end of file diff --git a/modules/features/mtls/default.nix b/modules/features/mtls/default.nix new file mode 100644 index 0000000..a081731 --- /dev/null +++ b/modules/features/mtls/default.nix @@ -0,0 +1,167 @@ +{ self, inputs, lib, ... }: +let + # Options that will be in common between the nixos module and the home-manager module. + mkOpts = config: let cfg = config.mtls; in { + enable = lib.mkEnableOption "Enable mTLS"; + subject = lib.mkOption { + description = "The Common Name, DNS Name, or IP address that will be set as the Subject Common Name for the certificate. If no Subject Alternative Names (SANs) are configured (via the --san flag) then the subject will be set as the only SAN."; + type = lib.types.str; + }; + certDir = lib.mkOption { + description = "String path to the directory where the certs will be stored"; + type = lib.types.str; + }; + caFile = lib.mkOption { + description = "String path for the root CA file"; + type = lib.types.str; + default = "${cfg.certDir}/root_ca.crt"; + }; + keyFile = lib.mkOption { + description = "String path for the private key"; + type = lib.types.str; + default = "${cfg.certDir}/key.pem"; + }; + certFile = lib.mkOption { + description = "String path for the public cert"; + type = lib.types.str; + default = "${cfg.certDir}/cert.pem"; + }; + bundleFile = lib.mkOption { + description = "String path for the mTLS key bundle"; + type = lib.types.str; + default = "${cfg.certDir}/mtls.pem"; + }; + san = lib.mkOption { + description = "List of SAN to give the mTLS cert"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + provisioner = lib.mkOption { + type = lib.types.str; + default = "admin"; + }; + lifetime = lib.mkOption { + type = lib.types.str; + default = "24h"; + }; + renew = { + enable = lib.mkOption { + description = "Enable automatic mTLS certificate renewal using a systemd timer."; + type = lib.types.bool; + default = cfg.enable; + }; + onCalendar = lib.mkOption { + description = "systemd OnCalendar schedule for mTLS certificate renewal checks."; + type = lib.types.str; + default = "*:1/15"; + }; + randomizedDelaySec = lib.mkOption { + description = "Randomized delay added to renewal timer runs to avoid synchronized renewals."; + type = lib.types.str; + default = "5m"; + }; + user = lib.mkOption { + description = "User account to run the mTLS renewal service as."; + type = lib.types.str; + default = "root"; + }; + group = lib.mkOption { + description = "Group to run the mTLS renewal service as. Defaults to the configured renewal user."; + type = lib.types.nullOr lib.types.str; + default = "mtls"; + }; + reloadUnits = lib.mkOption { + description = "systemd units to try-reload-or-restart after a successful certificate renewal."; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + postCommands = lib.mkOption { + description = "Shell commands to run after a successful certificate renewal."; + type = lib.types.listOf lib.types.lines; + default = [ ]; + }; + }; + }; +in +{ + flake.modules.nixos.mtls = { config, lib, pkgs, ... }: + let + cfg = config.mtls; + mtlsWrappers = inputs.self.wrappers.mtls; + mtlsGenerate = mtlsWrappers.generate.apply { + inherit pkgs; + inherit (cfg) subject; + SANs = cfg.san; + provisioner = "admin"; + provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path; + }; + mtlsRenew = mtlsWrappers.renew.apply { + inherit pkgs; + }; + mtlsCheck = mtlsWrappers.check.apply { + inherit pkgs; + }; + in + { + options.mtls = (mkOpts config) // { + certDir = lib.mkOption { + description = "String path to where the mtls certs will be stored."; + type = lib.types.str; + default = "/etc/step-ca/certs"; + }; + bootstrap = { + enable = lib.mkOption { + description = "Enable initial mTLS issuance when cert material is missing or invalid."; + type = lib.types.bool; + default = false; + }; + wantedBy = lib.mkOption { + description = "systemd targets that should pull in mtls-bootstrap.service."; + type = with lib.types; listOf str; + default = [ "multi-user.target" ]; + }; + after = lib.mkOption { + description = "systemd units/targets that mtls-bootstrap.service should run after."; + type = with lib.types; listOf str; + default = [ "network-online.target" ]; + }; + wants = lib.mkOption { + description = "systemd units/targets that mtls-bootstrap.service should pull in."; + type = with lib.types; listOf str; + default = [ "network-online.target" ]; + }; + provisionerPasswordFile = lib.mkOption { + description = "Optional path passed to mtls-generate as --provisioner-password-file for noninteractive issuance."; + type = lib.types.nullOr lib.types.str; + default = null; + }; + }; + certReaders = lib.mkOption { + description = ""; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + + config = lib.mkIf cfg.enable { + users.groups.certReaders = { + name = cfg.renew.group; + members = cfg.certReaders; + }; + + environment.systemPackages = [ + mtlsGenerate.wrapper + mtlsCheck.wrapper + mtlsRenew.wrapper + ]; + + systemd = { + packages = [ mtlsRenew.outputs.systemd-system ]; + # Timer-driven oneshot: only the timer is enabled. NixOS does not + # honor the unit's [Install] section for systemd.packages, so the + # wantedBy must be set explicitly here. + timers.mtls-renew.wantedBy = [ "timers.target" ]; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/features/mtls/mtls-wrappers.nix b/modules/features/mtls/mtls-wrappers.nix new file mode 100644 index 0000000..96008e7 --- /dev/null +++ b/modules/features/mtls/mtls-wrappers.nix @@ -0,0 +1,125 @@ +{ self, inputs, lib, ... }: +let + mkSANArgs = sans: builtins.concatLists (map (name: [ "--san" name ]) sans); + mkOpts = config: let cfg = config.mtls; in { + certDir = lib.mkOption { + description = "String path to the directory where the certs will be stored"; + type = lib.types.str; + default = "/etc/mtls"; + }; + keyFile = lib.mkOption { + description = "String path for the private key"; + type = lib.types.str; + default = "${config.certDir}/key.pem"; + }; + certFile = lib.mkOption { + description = "String path for the public cert"; + type = lib.types.str; + default = "${config.certDir}/cert.pem"; + }; + bundleFile = lib.mkOption { + description = "String path for the mTLS key bundle"; + type = lib.types.str; + default = "${config.certDir}/mtls.pem"; + }; + subject = lib.mkOption { + description = "Subject for the cert"; + type = lib.types.str; + }; + provisioner = lib.mkOption { + type = lib.types.nullOr lib.types.str; + }; + provisionerPasswordFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + }; + overwrite = lib.mkEnableOption "Overwrite existing cert file?"; + SANs = lib.mkOption { + description = "A list of Subject Alternative Names"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; +in +{ + flake.wrappers.mtls = { + generate = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { + options = (mkOpts config); + config = { + binName = "mtls-generate"; + package = config.pkgs.step-cli; + extraPackages = with config.pkgs; [ coreutils step-cli systemd ]; + preHook = "mkdir -p ${config.certDir}"; + args = [ + "ca" "certificate" + "${config.subject}" "${config.certFile}" "${config.keyFile}" + "--not-before" "-5m" + "--not-after" "24h" + ] + ++ lib.optionals (config.provisioner != null) [ "--provisioner" "${config.provisioner}" ] + ++ lib.optionals (config.provisionerPasswordFile != null) [ + "--provisioner-password-file" "${config.provisionerPasswordFile}" + ] + ++ lib.optionals config.overwrite [ "-f" ] + ++ mkSANArgs config.SANs; + postHook = '' + (umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}") + ''; + }; + }); + + renew = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { + # https://github.com/Lassulus/wrappers#generating-systemd-services + imports = [ wlib.modules.systemd ]; + options = (mkOpts config); + config = { + binName = "mtls-renew"; + package = config.pkgs.step-cli; + extraPackages = with config.pkgs; [ coreutils step-cli systemd ]; + args = [ + "ca" "renew" + "${config.certFile}" "${config.keyFile}" + ]; + postHook = '' + (umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}") + ''; + + systemd = { + description = "Renew the mTLS certificate when Smallstep marks it ready"; + documentation = [ + "https://smallstep.com/docs/step-ca/certificate-authority-server-production" + ]; + startLimitIntervalSec = 0; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = lib.mkDefault "oneshot"; + ExecCondition = "${lib.getExe config.pkgs.step-cli} certificate needs-renewal ${config.certFile}"; + }; + startAt = "hourly"; + }; + }; + }); + + check = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { + options = (mkOpts config); + config = { + binName = "mtls-check"; + # This pattern is necessary to wrap packages like openssl that provide more than one binary + package = config.pkgs.symlinkJoin { + name = "openssl"; + paths = [ config.pkgs.openssl.bin config.pkgs.openssl.man ]; + meta.mainProgram = "openssl"; + }; + args = [ + "x509" + "-noout" + "-in" config.bundleFile + "-subject" + "-issuer" + "-ext" "subjectAltName,extendedKeyUsage" + "-enddate" + ]; + }; + }); + }; +} \ No newline at end of file diff --git a/modules/hosts/soteria/default.nix b/modules/hosts/soteria/default.nix index fecbe4f..74f04e8 100644 --- a/modules/hosts/soteria/default.nix +++ b/modules/hosts/soteria/default.nix @@ -15,7 +15,7 @@ in inputs.home-manager.nixosModules.home-manager nixos."${username}" nixos.ssh-new - # nixos.mtls + nixos.mtls nixos.mysops nixos.docker nixos.step-client @@ -39,15 +39,15 @@ in ]; # users.users."${username}".extraGroups = [ "mtls" ]; - # mtls = { - # enable = true; - # subject = hostname; - # san = names; - # lifetime = "12h"; - # renew.onCalendar = "*:3/15"; - # renew.reloadUnits = [ "forgejo.service" "restic-rest-server.service" ]; - # certReaders = [ config.services.forgejo.user "restic" ]; - # }; + mtls = { + enable = true; + subject = hostname; + san = names; + # lifetime = "12h"; + # renew.onCalendar = "*:3/15"; + # renew.reloadUnits = [ "forgejo.service" "restic-rest-server.service" ]; + # certReaders = [ config.services.forgejo.user "restic" ]; + }; # forgejo = { # enable = true;