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 = {