Merge pull request #152065 from chkno/stunnel-extraConfig

nixos/stunnel: Make free-form
This commit is contained in:
Rick van Schijndel 2022-07-26 23:24:31 +02:00 committed by GitHub
commit 9e9f6fc1c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 237 additions and 102 deletions

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

View File

@ -523,6 +523,7 @@ in {
starship = handleTest ./starship.nix {};
step-ca = handleTestOn ["x86_64-linux"] ./step-ca.nix {};
strongswan-swanctl = handleTest ./strongswan-swanctl.nix {};
stunnel = handleTest ./stunnel.nix {};
sudo = handleTest ./sudo.nix {};
swap-partition = handleTest ./swap-partition.nix {};
sway = handleTest ./sway.nix {};

174
nixos/tests/stunnel.nix Normal file
View File

@ -0,0 +1,174 @@
{ system ? builtins.currentSystem, config ? { }
, pkgs ? import ../.. { inherit system config; } }:
with import ../lib/testing-python.nix { inherit system pkgs; };
with pkgs.lib;
let
stunnelCommon = {
services.stunnel = {
enable = true;
user = "stunnel";
};
users.groups.stunnel = { };
users.users.stunnel = {
isSystemUser = true;
group = "stunnel";
};
};
makeCert = { config, pkgs, ... }: {
system.activationScripts.create-test-cert = stringAfter [ "users" ] ''
${pkgs.openssl}/bin/openssl req -batch -x509 -newkey rsa -nodes -out /test-cert.pem -keyout /test-key.pem -subj /CN=${config.networking.hostName}
( umask 077; cat /test-key.pem /test-cert.pem > /test-key-and-cert.pem )
chown stunnel /test-key.pem /test-key-and-cert.pem
'';
};
serverCommon = { pkgs, ... }: {
networking.firewall.allowedTCPPorts = [ 443 ];
services.stunnel.servers.https = {
accept = "443";
connect = 80;
cert = "/test-key-and-cert.pem";
};
systemd.services.simple-webserver = {
wantedBy = [ "multi-user.target" ];
script = ''
cd /etc/webroot
${pkgs.python3}/bin/python -m http.server 80
'';
};
};
copyCert = src: dest: filename: ''
from shlex import quote
${src}.wait_for_file("/test-key-and-cert.pem")
server_cert = ${src}.succeed("cat /test-cert.pem")
${dest}.succeed("echo %s > ${filename}" % quote(server_cert))
'';
in {
basicServer = makeTest {
name = "basicServer";
nodes = {
client = { };
server = {
imports = [ makeCert serverCommon stunnelCommon ];
environment.etc."webroot/index.html".text = "well met";
};
};
testScript = ''
start_all()
${copyCert "server" "client" "/authorized-server-cert.crt"}
server.wait_for_unit("simple-webserver")
server.wait_for_unit("stunnel")
client.succeed("curl --fail --cacert /authorized-server-cert.crt https://server/ > out")
client.succeed('[[ "$(< out)" == "well met" ]]')
'';
};
serverAndClient = makeTest {
name = "serverAndClient";
nodes = {
client = {
imports = [ stunnelCommon ];
services.stunnel.clients = {
httpsClient = {
accept = "80";
connect = "server:443";
CAFile = "/authorized-server-cert.crt";
};
httpsClientWithHostVerify = {
accept = "81";
connect = "server:443";
CAFile = "/authorized-server-cert.crt";
verifyHostname = "server";
};
httpsClientWithHostVerifyFail = {
accept = "82";
connect = "server:443";
CAFile = "/authorized-server-cert.crt";
verifyHostname = "wronghostname";
};
};
};
server = {
imports = [ makeCert serverCommon stunnelCommon ];
environment.etc."webroot/index.html".text = "hello there";
};
};
testScript = ''
start_all()
${copyCert "server" "client" "/authorized-server-cert.crt"}
server.wait_for_unit("simple-webserver")
server.wait_for_unit("stunnel")
# In case stunnel came up before we got the server's cert copied over
client.succeed("systemctl reload-or-restart stunnel")
client.succeed("curl --fail http://localhost/ > out")
client.succeed('[[ "$(< out)" == "hello there" ]]')
client.succeed("curl --fail http://localhost:81/ > out")
client.succeed('[[ "$(< out)" == "hello there" ]]')
client.fail("curl --fail http://localhost:82/ > out")
client.succeed('[[ "$(< out)" == "" ]]')
'';
};
mutualAuth = makeTest {
name = "mutualAuth";
nodes = rec {
client = {
imports = [ makeCert stunnelCommon ];
services.stunnel.clients.authenticated-https = {
accept = "80";
connect = "server:443";
verifyPeer = true;
CAFile = "/authorized-server-cert.crt";
cert = "/test-cert.pem";
key = "/test-key.pem";
};
};
wrongclient = client;
server = {
imports = [ makeCert serverCommon stunnelCommon ];
services.stunnel.servers.https = {
CAFile = "/authorized-client-certs.crt";
verifyPeer = true;
};
environment.etc."webroot/index.html".text = "secret handshake";
};
};
testScript = ''
start_all()
${copyCert "server" "client" "/authorized-server-cert.crt"}
${copyCert "client" "server" "/authorized-client-certs.crt"}
${copyCert "server" "wrongclient" "/authorized-server-cert.crt"}
# In case stunnel came up before we got the cross-certs in place
client.succeed("systemctl reload-or-restart stunnel")
server.succeed("systemctl reload-or-restart stunnel")
wrongclient.succeed("systemctl reload-or-restart stunnel")
server.wait_for_unit("simple-webserver")
client.fail("curl --fail --insecure https://server/ > out")
client.succeed('[[ "$(< out)" == "" ]]')
client.succeed("curl --fail http://localhost/ > out")
client.succeed('[[ "$(< out)" == "secret handshake" ]]')
wrongclient.fail("curl --fail http://localhost/ > out")
wrongclient.succeed('[[ "$(< out)" == "" ]]')
'';
};
}

View File

@ -1,4 +1,4 @@
{ lib, stdenv, fetchurl, openssl }:
{ lib, stdenv, fetchurl, openssl, nixosTests }:
stdenv.mkDerivation rec {
pname = "stunnel";
@ -28,6 +28,10 @@ stdenv.mkDerivation rec {
"localstatedir=\${TMPDIR}"
];
passthru.tests = {
stunnel = nixosTests.stunnel;
};
meta = {
description = "Universal tls/ssl wrapper";
homepage = "https://www.stunnel.org/";