Merge pull request #152065 from chkno/stunnel-extraConfig
nixos/stunnel: Make free-form
This commit is contained in:
commit
9e9f6fc1c3
@ -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 = {
|
||||
|
@ -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
174
nixos/tests/stunnel.nix
Normal 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)" == "" ]]')
|
||||
'';
|
||||
};
|
||||
}
|
@ -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/";
|
||||
|
Loading…
Reference in New Issue
Block a user