lol

nixos/h2o: TLS recommendations (#384730)

authored by

lassulus and committed by
GitHub
bec9ad18 723693d2

+282 -29
+46
nixos/modules/services/web-servers/h2o/common.nix
··· 1 + { lib }: 2 + { 3 + tlsRecommendationsOption = lib.mkOption { 4 + type = lib.types.nullOr ( 5 + lib.types.enum [ 6 + "modern" 7 + "intermediate" 8 + "old" 9 + ] 10 + ); 11 + default = null; 12 + example = "intermediate"; 13 + description = '' 14 + By default, H2O, without prejudice, will use as many TLS versions & 15 + cipher suites as it & the TLS library (OpenSSL) can support. The user is 16 + expected to hone settings for the security of their server. Setting some 17 + constraints is recommended, & if unsure about what TLS settings to use, 18 + this option gives curated TLS settings recommendations from Mozilla’s 19 + ‘SSL Configuration Generator’ project (see 20 + <https://ssl-config.mozilla.org>) or read more at Mozilla’s Wiki (see 21 + <https://wiki.mozilla.org/Security/Server_Side_TLS>). 22 + 23 + modern 24 + : Services with clients that support TLS 1.3 & don’t need backward 25 + compatibility 26 + 27 + intermediate 28 + : General-purpose servers with a variety of clients, recommended for 29 + almost all systems 30 + 31 + old 32 + : Compatible with a number of very old clients, & should be used only as 33 + a last resort 34 + 35 + The default for all virtual hosts can be set with 36 + services.h2o.defaultTLSRecommendations, but this value can be overridden 37 + on a per-host basis using services.h2o.hosts.<name>.tls.recommmendations. 38 + The settings will also be overidden by manual values set with 39 + services.settings.h2o.hosts.<name>.tls.extraSettings. 40 + 41 + NOTE: older/weaker ciphers might require overriding the OpenSSL version 42 + of H2O (such as `openssl_legacy`). This can be done with 43 + sevices.settings.h2o.package. 44 + ''; 45 + }; 46 + }
+111 -28
nixos/modules/services/web-servers/h2o/default.nix
··· 6 6 }: 7 7 8 8 # TODO: Gems includes for Mruby 9 - # TODO: Recommended options 10 9 let 11 10 cfg = config.services.h2o; 12 11 inherit (config.security.acme) certs; ··· 21 20 ; 22 21 23 22 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib; 23 + 24 + inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; 24 25 25 26 settingsFormat = pkgs.formats.yaml { }; 26 27 ··· 76 77 all = certNames'.dependent ++ certNames'.independent; 77 78 }; 78 79 80 + mozTLSRecs = 81 + if cfg.defaultTLSRecommendations != null then 82 + let 83 + # NOTE: if updating, *do* verify the changes then adjust ciphers & 84 + # other settings with the tests @ 85 + # `nixos/tests/web-servers/h2o/tls-recommendations.nix` 86 + # & run with `nix-build -A nixosTests.h2o.tls-recommendations` 87 + version = "5.7"; 88 + git_tag = "v5.7.1"; 89 + guidelinesJSON = 90 + lib.pipe 91 + { 92 + urls = [ 93 + "https://ssl-config.mozilla.org/guidelines/${version}.json" 94 + "https://raw.githubusercontent.com/mozilla/ssl-config-generator/refs/tags/${git_tag}/src/static/guidelines/${version}.json" 95 + ]; 96 + sha256 = "sha256:1mj2pcb1hg7q2wpgdq3ac8pc2q64wvwvwlkb9xjmdd9jm4hiyny7"; 97 + } 98 + [ 99 + pkgs.fetchurl 100 + builtins.readFile 101 + builtins.fromJSON 102 + ]; 103 + in 104 + guidelinesJSON.configurations 105 + else 106 + null; 107 + 79 108 hostsConfig = lib.concatMapAttrs ( 80 109 name: value: 81 110 let ··· 130 159 ] 131 160 ) 132 161 { 133 - "${names.server}:${builtins.toString port.TLS}" = value.settings // { 134 - listen = 135 - let 136 - identity = 137 - value.tls.identity 138 - ++ lib.optional (builtins.elem names.cert certNames.all) { 139 - key-file = "${certs.${names.cert}.directory}/key.pem"; 140 - certificate-file = "${certs.${names.cert}.directory}/fullchain.pem"; 162 + "${names.server}:${builtins.toString port.TLS}" = 163 + let 164 + tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value; 165 + 166 + hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null; 167 + 168 + # NOTE: Let’s Encrypt has sunset OCSP stapling. Mozilla’s 169 + # ssl-config-generator is at present still recommending this setting, but 170 + # this module will skip setting a stapling value as Let’s Encrypt + 171 + # ACME is the most likely use case. 172 + # 173 + # See: https://github.com/mozilla/ssl-config-generator/issues/323 174 + tlsRecAttrs = lib.optionalAttrs hasTLSRecommendations ( 175 + let 176 + recs = mozTLSRecs.${tlsRecommendations}; 177 + in 178 + { 179 + min-version = builtins.head recs.tls_versions; 180 + cipher-preference = "server"; 181 + "cipher-suite-tls1.3" = recs.ciphersuites; 182 + } 183 + // lib.optionalAttrs (recs.ciphers.openssl != [ ]) { 184 + cipher-suite = lib.concatStringsSep ":" recs.ciphers.openssl; 185 + } 186 + ); 187 + 188 + headerRecAttrs = 189 + lib.optionalAttrs 190 + ( 191 + hasTLSRecommendations 192 + && value.tls != null 193 + && builtins.elem value.tls.policy [ 194 + "force" 195 + "only" 196 + ] 197 + ) 198 + ( 199 + let 200 + headerSet = value.settings."header.set" or [ ]; 201 + recs = mozTLSRecs.${tlsRecommendations}; 202 + hsts = "Strict-Transport-Security: max-age=${builtins.toString recs.hsts_min_age}; includeSubDomains; preload"; 203 + in 204 + { 205 + "header.set" = 206 + if builtins.isString headerSet then 207 + [ 208 + headerSet 209 + hsts 210 + ] 211 + else 212 + headerSet ++ [ hsts ]; 213 + } 214 + ); 215 + in 216 + value.settings 217 + // headerRecAttrs 218 + // { 219 + listen = 220 + let 221 + identity = 222 + value.tls.identity 223 + ++ lib.optional (builtins.elem names.cert certNames.all) { 224 + key-file = "${certs.${names.cert}.directory}/key.pem"; 225 + certificate-file = "${certs.${names.cert}.directory}/fullchain.pem"; 226 + }; 227 + in 228 + { 229 + port = port.TLS; 230 + ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // { 231 + inherit identity; 141 232 }; 142 - in 143 - { 144 - port = port.TLS; 145 - ssl = value.tls.extraSettings // { 146 - inherit identity; 147 233 }; 148 - }; 149 - }; 234 + }; 150 235 }; 151 236 in 152 237 # With a high likelihood of HTTP & ACME challenges being on the same port, ··· 184 269 }; 185 270 186 271 package = lib.mkPackageOption pkgs "h2o" { 187 - example = '' 188 - pkgs.h2o.override { 189 - withMruby = false; 190 - }; 191 - ''; 272 + example = # nix 273 + '' 274 + pkgs.h2o.override { 275 + withMruby = false; 276 + openssl = pkgs.openssl_legacy; 277 + } 278 + ''; 192 279 }; 193 280 194 281 defaultHTTPListenPort = mkOption { ··· 209 296 example = 8443; 210 297 }; 211 298 299 + defaultTLSRecommendations = tlsRecommendationsOption; 300 + 212 301 settings = mkOption { 213 302 type = settingsFormat.type; 214 303 default = { }; ··· 216 305 }; 217 306 218 307 hosts = mkOption { 219 - type = types.attrsOf ( 220 - types.submodule ( 221 - import ./vhost-options.nix { 222 - inherit config lib; 223 - } 224 - ) 225 - ); 308 + type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; })); 226 309 default = { }; 227 310 description = '' 228 311 The `hosts` config to be merged with the settings.
+9 -1
nixos/modules/services/web-servers/h2o/vhost-options.nix
··· 1 - { config, lib, ... }: 1 + { 2 + config, 3 + lib, 4 + ... 5 + }: 2 6 3 7 let 4 8 inherit (lib) ··· 6 10 mkOption 7 11 types 8 12 ; 13 + 14 + inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; 9 15 in 10 16 { 11 17 options = { ··· 128 134 ] 129 135 ''; 130 136 }; 137 + recommendations = tlsRecommendationsOption; 131 138 extraSettings = mkOption { 132 139 type = types.attrs; 133 140 default = { }; ··· 195 202 196 203 settings = mkOption { 197 204 type = types.attrs; 205 + default = { }; 198 206 description = '' 199 207 Attrset to be transformed into YAML for host config. Note that the HTTP 200 208 / TLS configurations will override these config values.
+1
nixos/tests/web-servers/h2o/default.nix
··· 13 13 { 14 14 basic = handleTestOn supportedSystems ./basic.nix { inherit system; }; 15 15 mruby = handleTestOn supportedSystems ./mruby.nix { inherit system; }; 16 + tls-recommendations = handleTestOn supportedSystems ./tls-recommendations.nix { inherit system; }; 16 17 }
+115
nixos/tests/web-servers/h2o/tls-recommendations.nix
··· 1 + import ../../make-test-python.nix ( 2 + { lib, pkgs, ... }: 3 + 4 + let 5 + domain = "acme.test"; 6 + port = 8443; 7 + 8 + hello_txt = 9 + name: 10 + pkgs.writeTextFile { 11 + name = "/hello_${name}.txt"; 12 + text = "Hello, ${name}!"; 13 + }; 14 + 15 + mkH2OServer = 16 + recommendations: 17 + { pkgs, lib, ... }: 18 + { 19 + services.h2o = { 20 + enable = true; 21 + package = pkgs.h2o.override ( 22 + lib.optionalAttrs 23 + (builtins.elem recommendations [ 24 + "intermediate" 25 + "old" 26 + ]) 27 + { 28 + openssl = pkgs.openssl_legacy; 29 + } 30 + ); 31 + defaultTLSRecommendations = "modern"; # prove overridden 32 + hosts = { 33 + "${domain}" = { 34 + tls = { 35 + inherit port recommendations; 36 + policy = "force"; 37 + identity = [ 38 + { 39 + key-file = ../../common/acme/server/acme.test.key.pem; 40 + certificate-file = ../../common/acme/server/acme.test.cert.pem; 41 + } 42 + ]; 43 + }; 44 + settings = { 45 + paths."/"."file.file" = "${hello_txt recommendations}"; 46 + }; 47 + }; 48 + }; 49 + settings = { 50 + ssl-offload = "kernel"; 51 + }; 52 + }; 53 + 54 + security.pki.certificates = [ 55 + (builtins.readFile ../../common/acme/server/ca.cert.pem) 56 + ]; 57 + 58 + networking = { 59 + firewall.allowedTCPPorts = [ port ]; 60 + extraHosts = "127.0.0.1 ${domain}"; 61 + }; 62 + }; 63 + in 64 + { 65 + name = "h2o-tls-recommendations"; 66 + 67 + meta = { 68 + maintainers = with lib.maintainers; [ toastal ]; 69 + }; 70 + 71 + nodes = { 72 + server_modern = mkH2OServer "modern"; 73 + server_intermediate = mkH2OServer "intermediate"; 74 + server_old = mkH2OServer "old"; 75 + }; 76 + 77 + testScript = 78 + let 79 + portStr = builtins.toString port; 80 + in 81 + # python 82 + '' 83 + curl_basic = "curl -v --tlsv1.3 --http2 'https://${domain}:${portStr}/'" 84 + curl_head = "curl -v --head 'https://${domain}:${portStr}/'" 85 + curl_max_tls1_2 ="curl -v --tlsv1.0 --tls-max 1.2 'https://${domain}:${portStr}/'" 86 + curl_max_tls1_2_intermediate_cipher ="curl -v --tlsv1.0 --tls-max 1.2 --ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256' 'https://${domain}:${portStr}/'" 87 + curl_max_tls1_2_old_cipher ="curl -v --tlsv1.0 --tls-max 1.2 --ciphers 'ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256' 'https://${domain}:${portStr}/'" 88 + 89 + server_modern.wait_for_unit("h2o.service") 90 + modern_response = server_modern.succeed(curl_basic) 91 + assert "Hello, modern!" in modern_response 92 + modern_head = server_modern.succeed(curl_head) 93 + assert "strict-transport-security" in modern_head 94 + server_modern.fail(curl_max_tls1_2) 95 + 96 + server_intermediate.wait_for_unit("h2o.service") 97 + intermediate_response = server_intermediate.succeed(curl_basic) 98 + assert "Hello, intermediate!" in intermediate_response 99 + intermediate_head = server_modern.succeed(curl_head) 100 + assert "strict-transport-security" in intermediate_head 101 + server_intermediate.succeed(curl_max_tls1_2) 102 + server_intermediate.succeed(curl_max_tls1_2_intermediate_cipher) 103 + server_intermediate.fail(curl_max_tls1_2_old_cipher) 104 + 105 + server_old.wait_for_unit("h2o.service") 106 + old_response = server_old.succeed(curl_basic) 107 + assert "Hello, old!" in old_response 108 + old_head = server_modern.succeed(curl_head) 109 + assert "strict-transport-security" in old_head 110 + server_old.succeed(curl_max_tls1_2) 111 + server_old.succeed(curl_max_tls1_2_intermediate_cipher) 112 + server_old.succeed(curl_max_tls1_2_old_cipher) 113 + ''; 114 + } 115 + )