at 21.11-pre 6.6 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.stunnel; 8 yesNo = val: if val then "yes" else "no"; 9 10 verifyChainPathAssert = n: c: { 11 assertion = c.verifyHostname == null || (c.verifyChain || c.verifyPeer); 12 message = "stunnel: \"${n}\" client configuration - hostname verification " + 13 "is not possible without either verifyChain or verifyPeer enabled"; 14 }; 15 16 serverConfig = { 17 options = { 18 accept = mkOption { 19 type = types.either types.str types.int; 20 description = '' 21 On which [host:]port stunnel should listen for incoming TLS connections. 22 Note that unlike other softwares stunnel ipv6 address need no brackets, 23 so to listen on all IPv6 addresses on port 1234 one would use ':::1234'. 24 ''; 25 }; 26 27 connect = mkOption { 28 type = types.int; 29 description = "To which port the decrypted connection should be forwarded."; 30 }; 31 32 cert = mkOption { 33 type = types.path; 34 description = "File containing both the private and public keys."; 35 }; 36 }; 37 }; 38 39 clientConfig = { 40 options = { 41 accept = mkOption { 42 type = types.str; 43 description = "IP:Port on which connections should be accepted."; 44 }; 45 46 connect = mkOption { 47 type = types.str; 48 description = "IP:Port destination to connect to."; 49 }; 50 51 verifyChain = mkOption { 52 type = types.bool; 53 default = true; 54 description = "Check if the provided certificate has a valid certificate chain (against CAPath)."; 55 }; 56 57 verifyPeer = mkOption { 58 type = types.bool; 59 default = false; 60 description = "Check if the provided certificate is contained in CAPath."; 61 }; 62 63 CAPath = mkOption { 64 type = types.nullOr types.path; 65 default = null; 66 description = "Path to a directory containing certificates to validate against."; 67 }; 68 69 CAFile = mkOption { 70 type = types.nullOr types.path; 71 default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; 72 description = "Path to a file containing certificates to validate against."; 73 }; 74 75 verifyHostname = mkOption { 76 type = with types; nullOr str; 77 default = null; 78 description = "If set, stunnel checks if the provided certificate is valid for the given hostname."; 79 }; 80 }; 81 }; 82 83 84in 85 86{ 87 88 ###### interface 89 90 options = { 91 92 services.stunnel = { 93 94 enable = mkOption { 95 type = types.bool; 96 default = false; 97 description = "Whether to enable the stunnel TLS tunneling service."; 98 }; 99 100 user = mkOption { 101 type = with types; nullOr str; 102 default = "nobody"; 103 description = "The user under which stunnel runs."; 104 }; 105 106 group = mkOption { 107 type = with types; nullOr str; 108 default = "nogroup"; 109 description = "The group under which stunnel runs."; 110 }; 111 112 logLevel = mkOption { 113 type = types.enum [ "emerg" "alert" "crit" "err" "warning" "notice" "info" "debug" ]; 114 default = "info"; 115 description = "Verbosity of stunnel output."; 116 }; 117 118 fipsMode = mkOption { 119 type = types.bool; 120 default = false; 121 description = "Enable FIPS 140-2 mode required for compliance."; 122 }; 123 124 enableInsecureSSLv3 = mkOption { 125 type = types.bool; 126 default = false; 127 description = "Enable support for the insecure SSLv3 protocol."; 128 }; 129 130 131 servers = mkOption { 132 description = "Define the server configuations."; 133 type = with types; attrsOf (submodule serverConfig); 134 example = { 135 fancyWebserver = { 136 accept = 443; 137 connect = 8080; 138 cert = "/path/to/pem/file"; 139 }; 140 }; 141 default = { }; 142 }; 143 144 clients = mkOption { 145 description = "Define the client configurations."; 146 type = with types; attrsOf (submodule clientConfig); 147 example = { 148 foobar = { 149 accept = "0.0.0.0:8080"; 150 connect = "nixos.org:443"; 151 verifyChain = false; 152 }; 153 }; 154 default = { }; 155 }; 156 }; 157 }; 158 159 160 ###### implementation 161 162 config = mkIf cfg.enable { 163 164 assertions = concatLists [ 165 (singleton { 166 assertion = (length (attrValues cfg.servers) != 0) || ((length (attrValues cfg.clients)) != 0); 167 message = "stunnel: At least one server- or client-configuration has to be present."; 168 }) 169 170 (mapAttrsToList verifyChainPathAssert cfg.clients) 171 ]; 172 173 environment.systemPackages = [ pkgs.stunnel ]; 174 175 environment.etc."stunnel.cfg".text = '' 176 ${ if cfg.user != null then "setuid = ${cfg.user}" else "" } 177 ${ if cfg.group != null then "setgid = ${cfg.group}" else "" } 178 179 debug = ${cfg.logLevel} 180 181 ${ optionalString cfg.fipsMode "fips = yes" } 182 ${ optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3" } 183 184 ; ----- SERVER CONFIGURATIONS ----- 185 ${ lib.concatStringsSep "\n" 186 (lib.mapAttrsToList 187 (n: v: '' 188 [${n}] 189 accept = ${toString v.accept} 190 connect = ${toString v.connect} 191 cert = ${v.cert} 192 193 '') 194 cfg.servers) 195 } 196 197 ; ----- CLIENT CONFIGURATIONS ----- 198 ${ lib.concatStringsSep "\n" 199 (lib.mapAttrsToList 200 (n: v: '' 201 [${n}] 202 client = yes 203 accept = ${v.accept} 204 connect = ${v.connect} 205 verifyChain = ${yesNo v.verifyChain} 206 verifyPeer = ${yesNo v.verifyPeer} 207 ${optionalString (v.CAPath != null) "CApath = ${v.CAPath}"} 208 ${optionalString (v.CAFile != null) "CAFile = ${v.CAFile}"} 209 ${optionalString (v.verifyHostname != null) "checkHost = ${v.verifyHostname}"} 210 OCSPaia = yes 211 212 '') 213 cfg.clients) 214 } 215 ''; 216 217 systemd.services.stunnel = { 218 description = "stunnel TLS tunneling service"; 219 after = [ "network.target" ]; 220 wants = [ "network.target" ]; 221 wantedBy = [ "multi-user.target" ]; 222 restartTriggers = [ config.environment.etc."stunnel.cfg".source ]; 223 serviceConfig = { 224 ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}"; 225 Type = "forking"; 226 }; 227 }; 228 229 meta.maintainers = with maintainers; [ 230 # Server side 231 lschuermann 232 # Client side 233 das_j 234 ]; 235 }; 236 237}