nixos/stunnel: Make free-form

This unlocks stunnel's other ~100 configuration directives, allowing
full stunnel use in NixOS.
This commit is contained in:
Scott Worley 2022-02-22 16:42:29 -08:00
parent 61585d1cd7
commit 131399effb

View file

@ -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 <citerefentry><refentrytitle>stunnel</refentrytitle>
<manvolnum>8</manvolnum></citerefentry>.
'';
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 <citerefentry><refentrytitle>stunnel</refentrytitle>
<manvolnum>8</manvolnum></citerefentry>.
'';
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 = {