diff options
author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2022-07-27 00:03:09 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-27 00:03:09 +0000 |
commit | 54f2dacce163c6c42e20c7114ec14167019ce507 (patch) | |
tree | 8f0f120e4ce44d6c898cbfb53c40d14b45ebb988 /nixos | |
parent | b21eff1a418869404cf487a32ad07022092bce96 (diff) | |
parent | f110f87498643036f1c95166fda8ac6738683307 (diff) | |
download | nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar.gz nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar.bz2 nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar.lz nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar.xz nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.tar.zst nixpkgs-54f2dacce163c6c42e20c7114ec14167019ce507.zip |
Merge staging-next into staging
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/modules/services/networking/stunnel.nix | 158 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/stunnel.nix | 174 |
3 files changed, 232 insertions, 101 deletions
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 <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 = { diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 8d26845615f..b0dd7ca0766 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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 {}; diff --git a/nixos/tests/stunnel.nix b/nixos/tests/stunnel.nix new file mode 100644 index 00000000000..22c087290fc --- /dev/null +++ b/nixos/tests/stunnel.nix @@ -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)" == "" ]]') + ''; + }; +} |