diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix index df4908a0fff..55fac27f92c 100644 --- a/nixos/modules/services/networking/stunnel.nix +++ b/nixos/modules/services/networking/stunnel.nix @@ -7,80 +7,27 @@ let cfg = config.services.stunnel; yesNo = val: if val then "yes" else "no"; + verifyRequiredField = type: field: n: c: { + assertion = hasAttr field c; + message = "stunnel: \"${n}\" ${type} configuration - Field ${field} is required."; + }; + verifyChainPathAssert = n: c: { - assertion = c.verifyHostname == null || (c.verifyChain || c.verifyPeer); + assertion = (c.verifyHostname or null) == null || (c.verifyChain || c.verifyPeer); message = "stunnel: \"${n}\" client configuration - hostname verification " + "is not possible without either verifyChain or verifyPeer enabled"; }; - serverConfig = { - options = { - accept = mkOption { - type = types.either types.str types.int; - description = '' - On which [host:]port stunnel should listen for incoming TLS connections. - Note that unlike other softwares stunnel ipv6 address need no brackets, - so to listen on all IPv6 addresses on port 1234 one would use ':::1234'. - ''; - }; - - connect = mkOption { - type = types.either types.str types.int; - description = "Port or IP:Port to which the decrypted connection should be forwarded."; - }; - - cert = mkOption { - type = types.path; - description = "File containing both the private and public keys."; - }; - }; - }; - - clientConfig = { - options = { - accept = mkOption { - type = types.str; - description = "IP:Port on which connections should be accepted."; - }; - - connect = mkOption { - type = types.str; - description = "IP:Port destination to connect to."; - }; - - verifyChain = mkOption { - type = types.bool; - default = true; - description = "Check if the provided certificate has a valid certificate chain (against CAPath)."; - }; - - verifyPeer = mkOption { - type = types.bool; - default = false; - description = "Check if the provided certificate is contained in CAPath."; - }; - - CAPath = mkOption { - type = types.nullOr types.path; - default = null; - description = "Path to a directory containing certificates to validate against."; - }; - - CAFile = mkOption { - type = types.nullOr types.path; - default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - defaultText = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"''; - description = "Path to a file containing certificates to validate against."; - }; - - verifyHostname = mkOption { - type = with types; nullOr str; - default = null; - description = "If set, stunnel checks if the provided certificate is valid for the given hostname."; - }; - }; - }; - + removeNulls = mapAttrs (_: filterAttrs (_: v: v != null)); + mkValueString = v: + if v == true then "yes" + else if v == false then "no" + else generators.mkValueStringDefault {} v; + generateConfig = c: + generators.toINI { + mkSectionName = id; + mkKeyValue = k: v: "${k} = ${mkValueString v}"; + } (removeNulls c); in @@ -130,8 +77,13 @@ in servers = mkOption { - description = "Define the server configuations."; - type = with types; attrsOf (submodule serverConfig); + description = '' + Define the server configuations. + + See "SERVICE-LEVEL OPTIONS" in stunnel + 8. + ''; + type = with types; attrsOf (attrsOf (nullOr (oneOf [bool int str]))); example = { fancyWebserver = { accept = 443; @@ -143,8 +95,33 @@ in }; clients = mkOption { - description = "Define the client configurations."; - type = with types; attrsOf (submodule clientConfig); + description = '' + Define the client configurations. + + By default, verifyChain and OCSPaia are enabled and a CAFile is provided from pkgs.cacert. + + See "SERVICE-LEVEL OPTIONS" in stunnel + 8. + ''; + type = with types; attrsOf (attrsOf (nullOr (oneOf [bool int str]))); + + apply = let + applyDefaults = c: + { + CAFile = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + OCSPaia = true; + verifyChain = true; + } // c; + setCheckHostFromVerifyHostname = c: + # To preserve backward-compatibility with the old NixOS stunnel module + # definition, allow "verifyHostname" as an alias for "checkHost". + c // { + checkHost = c.checkHost or c.verifyHostname or null; + verifyHostname = null; # Not a real stunnel configuration setting + }; + forceClient = c: c // { client = true; }; + in mapAttrs (_: c: forceClient (setCheckHostFromVerifyHostname (applyDefaults c))); + example = { foobar = { accept = "0.0.0.0:8080"; @@ -169,6 +146,11 @@ in }) (mapAttrsToList verifyChainPathAssert cfg.clients) + (mapAttrsToList (verifyRequiredField "client" "accept") cfg.clients) + (mapAttrsToList (verifyRequiredField "client" "connect") cfg.clients) + (mapAttrsToList (verifyRequiredField "server" "accept") cfg.servers) + (mapAttrsToList (verifyRequiredField "server" "cert") cfg.servers) + (mapAttrsToList (verifyRequiredField "server" "connect") cfg.servers) ]; environment.systemPackages = [ pkgs.stunnel ]; @@ -183,36 +165,10 @@ in ${ optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3" } ; ----- SERVER CONFIGURATIONS ----- - ${ lib.concatStringsSep "\n" - (lib.mapAttrsToList - (n: v: '' - [${n}] - accept = ${toString v.accept} - connect = ${toString v.connect} - cert = ${v.cert} - - '') - cfg.servers) - } + ${ generateConfig cfg.servers } ; ----- CLIENT CONFIGURATIONS ----- - ${ lib.concatStringsSep "\n" - (lib.mapAttrsToList - (n: v: '' - [${n}] - client = yes - accept = ${v.accept} - connect = ${v.connect} - verifyChain = ${yesNo v.verifyChain} - verifyPeer = ${yesNo v.verifyPeer} - ${optionalString (v.CAPath != null) "CApath = ${v.CAPath}"} - ${optionalString (v.CAFile != null) "CAFile = ${v.CAFile}"} - ${optionalString (v.verifyHostname != null) "checkHost = ${v.verifyHostname}"} - OCSPaia = yes - - '') - cfg.clients) - } + ${ generateConfig cfg.clients } ''; systemd.services.stunnel = {