{ config, lib, pkgs, ... }: let cfg = config.services.stunnel; yesNo = val: if val then "yes" else "no"; verifyRequiredField = type: field: n: c: { assertion = lib.hasAttr field c; message = "stunnel: \"${n}\" ${type} configuration - Field ${field} is required."; }; verifyChainPathAssert = n: c: { 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"; }; removeNulls = lib.mapAttrs (_: lib.filterAttrs (_: v: v != null)); mkValueString = v: if v == true then "yes" else if v == false then "no" else lib.generators.mkValueStringDefault { } v; generateConfig = c: lib.generators.toINI { mkSectionName = lib.id; mkKeyValue = k: v: "${k} = ${mkValueString v}"; } (removeNulls c); in { ###### interface options = { services.stunnel = { enable = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to enable the stunnel TLS tunneling service."; }; user = lib.mkOption { type = with lib.types; nullOr str; default = "nobody"; description = "The user under which stunnel runs."; }; group = lib.mkOption { type = with lib.types; nullOr str; default = "nogroup"; description = "The group under which stunnel runs."; }; logLevel = lib.mkOption { type = lib.types.enum [ "emerg" "alert" "crit" "err" "warning" "notice" "info" "debug" ]; default = "info"; description = "Verbosity of stunnel output."; }; fipsMode = lib.mkOption { type = lib.types.bool; default = false; description = "Enable FIPS 140-2 mode required for compliance."; }; enableInsecureSSLv3 = lib.mkOption { type = lib.types.bool; default = false; description = "Enable support for the insecure SSLv3 protocol."; }; servers = lib.mkOption { description = '' Define the server configurations. See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`. ''; type = with lib.types; attrsOf ( attrsOf ( nullOr (oneOf [ bool int str ]) ) ); example = { fancyWebserver = { accept = 443; connect = 8080; cert = "/path/to/pem/file"; }; }; default = { }; }; clients = lib.mkOption { description = '' Define the client configurations. By default, verifyChain and OCSPaia are enabled and CAFile is set to `security.pki.caBundle`. See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`. ''; type = with lib.types; attrsOf ( attrsOf ( nullOr (oneOf [ bool int str ]) ) ); apply = let applyDefaults = c: { CAFile = config.security.pki.caBundle; 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 lib.mapAttrs (_: c: forceClient (setCheckHostFromVerifyHostname (applyDefaults c))); example = { foobar = { accept = "0.0.0.0:8080"; connect = "nixos.org:443"; verifyChain = false; }; }; default = { }; }; }; }; ###### implementation config = lib.mkIf cfg.enable { assertions = lib.concatLists [ (lib.singleton { assertion = (lib.length (lib.attrValues cfg.servers) != 0) || ((lib.length (lib.attrValues cfg.clients)) != 0); message = "stunnel: At least one server- or client-configuration has to be present."; }) (lib.mapAttrsToList verifyChainPathAssert cfg.clients) (lib.mapAttrsToList (verifyRequiredField "client" "accept") cfg.clients) (lib.mapAttrsToList (verifyRequiredField "client" "connect") cfg.clients) (lib.mapAttrsToList (verifyRequiredField "server" "accept") cfg.servers) (lib.mapAttrsToList (verifyRequiredField "server" "cert") cfg.servers) (lib.mapAttrsToList (verifyRequiredField "server" "connect") cfg.servers) ]; environment.systemPackages = [ pkgs.stunnel ]; environment.etc."stunnel.cfg".text = '' ${lib.optionalString (cfg.user != null) "setuid = ${cfg.user}"} ${lib.optionalString (cfg.group != null) "setgid = ${cfg.group}"} debug = ${cfg.logLevel} ${lib.optionalString cfg.fipsMode "fips = yes"} ${lib.optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3"} ; ----- SERVER CONFIGURATIONS ----- ${generateConfig cfg.servers} ; ----- CLIENT CONFIGURATIONS ----- ${generateConfig cfg.clients} ''; systemd.services.stunnel = { description = "stunnel TLS tunneling service"; after = [ "network.target" ]; wants = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; restartTriggers = [ config.environment.etc."stunnel.cfg".source ]; serviceConfig = { ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}"; Type = "forking"; }; }; }; meta.maintainers = with lib.maintainers; [ # Server side lschuermann # Client side das_j ]; }