Loading nixos/modules/services/web-servers/h2o/common.nix 0 → 100644 +46 −0 Original line number Diff line number Diff line { lib }: { tlsRecommendationsOption = lib.mkOption { type = lib.types.nullOr ( lib.types.enum [ "modern" "intermediate" "old" ] ); default = null; example = "intermediate"; description = '' By default, H2O, without prejudice, will use as many TLS versions & cipher suites as it & the TLS library (OpenSSL) can support. The user is expected to hone settings for the security of their server. Setting some constraints is recommended, & if unsure about what TLS settings to use, this option gives curated TLS settings recommendations from Mozilla’s ‘SSL Configuration Generator’ project (see <https://ssl-config.mozilla.org>) or read more at Mozilla’s Wiki (see <https://wiki.mozilla.org/Security/Server_Side_TLS>). modern : Services with clients that support TLS 1.3 & don’t need backward compatibility intermediate : General-purpose servers with a variety of clients, recommended for almost all systems old : Compatible with a number of very old clients, & should be used only as a last resort The default for all virtual hosts can be set with services.h2o.defaultTLSRecommendations, but this value can be overridden on a per-host basis using services.h2o.hosts.<name>.tls.recommmendations. The settings will also be overidden by manual values set with services.settings.h2o.hosts.<name>.tls.extraSettings. NOTE: older/weaker ciphers might require overriding the OpenSSL version of H2O (such as `openssl_legacy`). This can be done with sevices.settings.h2o.package. ''; }; } nixos/modules/services/web-servers/h2o/default.nix +111 −28 Original line number Diff line number Diff line Loading @@ -6,7 +6,6 @@ }: # TODO: Gems includes for Mruby # TODO: Recommended options let cfg = config.services.h2o; inherit (config.security.acme) certs; Loading @@ -22,6 +21,8 @@ let mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib; inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; settingsFormat = pkgs.formats.yaml { }; getNames = name: vhostSettings: rec { Loading Loading @@ -76,6 +77,34 @@ let all = certNames'.dependent ++ certNames'.independent; }; mozTLSRecs = if cfg.defaultTLSRecommendations != null then let # NOTE: if updating, *do* verify the changes then adjust ciphers & # other settings with the tests @ # `nixos/tests/web-servers/h2o/tls-recommendations.nix` # & run with `nix-build -A nixosTests.h2o.tls-recommendations` version = "5.7"; git_tag = "v5.7.1"; guidelinesJSON = lib.pipe { urls = [ "https://ssl-config.mozilla.org/guidelines/${version}.json" "https://raw.githubusercontent.com/mozilla/ssl-config-generator/refs/tags/${git_tag}/src/static/guidelines/${version}.json" ]; sha256 = "sha256:1mj2pcb1hg7q2wpgdq3ac8pc2q64wvwvwlkb9xjmdd9jm4hiyny7"; } [ pkgs.fetchurl builtins.readFile builtins.fromJSON ]; in guidelinesJSON.configurations else null; hostsConfig = lib.concatMapAttrs ( name: value: let Loading Loading @@ -130,7 +159,63 @@ let ] ) { "${names.server}:${builtins.toString port.TLS}" = value.settings // { "${names.server}:${builtins.toString port.TLS}" = let tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value; hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null; # NOTE: Let’s Encrypt has sunset OCSP stapling. Mozilla’s # ssl-config-generator is at present still recommending this setting, but # this module will skip setting a stapling value as Let’s Encrypt + # ACME is the most likely use case. # # See: https://github.com/mozilla/ssl-config-generator/issues/323 tlsRecAttrs = lib.optionalAttrs hasTLSRecommendations ( let recs = mozTLSRecs.${tlsRecommendations}; in { min-version = builtins.head recs.tls_versions; cipher-preference = "server"; "cipher-suite-tls1.3" = recs.ciphersuites; } // lib.optionalAttrs (recs.ciphers.openssl != [ ]) { cipher-suite = lib.concatStringsSep ":" recs.ciphers.openssl; } ); headerRecAttrs = lib.optionalAttrs ( hasTLSRecommendations && value.tls != null && builtins.elem value.tls.policy [ "force" "only" ] ) ( let headerSet = value.settings."header.set" or [ ]; recs = mozTLSRecs.${tlsRecommendations}; hsts = "Strict-Transport-Security: max-age=${builtins.toString recs.hsts_min_age}; includeSubDomains; preload"; in { "header.set" = if builtins.isString headerSet then [ headerSet hsts ] else headerSet ++ [ hsts ]; } ); in value.settings // headerRecAttrs // { listen = let identity = Loading @@ -142,7 +227,7 @@ let in { port = port.TLS; ssl = value.tls.extraSettings // { ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // { inherit identity; }; }; Loading Loading @@ -184,10 +269,12 @@ in }; package = lib.mkPackageOption pkgs "h2o" { example = '' example = # nix '' pkgs.h2o.override { withMruby = false; }; openssl = pkgs.openssl_legacy; } ''; }; Loading @@ -209,6 +296,8 @@ in example = 8443; }; defaultTLSRecommendations = tlsRecommendationsOption; settings = mkOption { type = settingsFormat.type; default = { }; Loading @@ -216,13 +305,7 @@ in }; hosts = mkOption { type = types.attrsOf ( types.submodule ( import ./vhost-options.nix { inherit config lib; } ) ); type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; })); default = { }; description = '' The `hosts` config to be merged with the settings. Loading nixos/modules/services/web-servers/h2o/vhost-options.nix +8 −1 Original line number Diff line number Diff line { config, lib, ... }: { config, lib, ... }: let inherit (lib) Loading @@ -6,6 +10,8 @@ let mkOption types ; inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; in { options = { Loading Loading @@ -128,6 +134,7 @@ in ] ''; }; recommendations = tlsRecommendationsOption; extraSettings = mkOption { type = types.attrs; default = { }; Loading nixos/tests/web-servers/h2o/default.nix +1 −0 Original line number Diff line number Diff line Loading @@ -13,4 +13,5 @@ in { basic = handleTestOn supportedSystems ./basic.nix { inherit system; }; mruby = handleTestOn supportedSystems ./mruby.nix { inherit system; }; tls-recommendations = handleTestOn supportedSystems ./tls-recommendations.nix { inherit system; }; } nixos/tests/web-servers/h2o/tls-recommendations.nix 0 → 100644 +115 −0 Original line number Diff line number Diff line import ../../make-test-python.nix ( { lib, pkgs, ... }: let domain = "acme.test"; port = 8443; hello_txt = name: pkgs.writeTextFile { name = "/hello_${name}.txt"; text = "Hello, ${name}!"; }; mkH2OServer = recommendations: { pkgs, lib, ... }: { services.h2o = { enable = true; package = pkgs.h2o.override ( lib.optionalAttrs (builtins.elem recommendations [ "intermediate" "old" ]) { openssl = pkgs.openssl_legacy; } ); defaultTLSRecommendations = "modern"; # prove overridden hosts = { "${domain}" = { tls = { inherit port recommendations; policy = "force"; identity = [ { key-file = ../../common/acme/server/acme.test.key.pem; certificate-file = ../../common/acme/server/acme.test.cert.pem; } ]; }; settings = { paths."/"."file.file" = "${hello_txt recommendations}"; }; }; }; settings = { ssl-offload = "kernel"; }; }; security.pki.certificates = [ (builtins.readFile ../../common/acme/server/ca.cert.pem) ]; networking = { firewall.allowedTCPPorts = [ port ]; extraHosts = "127.0.0.1 ${domain}"; }; }; in { name = "h2o-tls-recommendations"; meta = { maintainers = with lib.maintainers; [ toastal ]; }; nodes = { server_modern = mkH2OServer "modern"; server_intermediate = mkH2OServer "intermediate"; server_old = mkH2OServer "old"; }; testScript = let portStr = builtins.toString port; in # python '' curl_basic = "curl -v --tlsv1.3 --http2 'https://${domain}:${portStr}/'" curl_head = "curl -v --head 'https://${domain}:${portStr}/'" curl_max_tls1_2 ="curl -v --tlsv1.0 --tls-max 1.2 'https://${domain}:${portStr}/'" 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}/'" 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}/'" server_modern.wait_for_unit("h2o.service") modern_response = server_modern.succeed(curl_basic) assert "Hello, modern!" in modern_response modern_head = server_modern.succeed(curl_head) assert "strict-transport-security" in modern_head server_modern.fail(curl_max_tls1_2) server_intermediate.wait_for_unit("h2o.service") intermediate_response = server_intermediate.succeed(curl_basic) assert "Hello, intermediate!" in intermediate_response intermediate_head = server_modern.succeed(curl_head) assert "strict-transport-security" in intermediate_head server_intermediate.succeed(curl_max_tls1_2) server_intermediate.succeed(curl_max_tls1_2_intermediate_cipher) server_intermediate.fail(curl_max_tls1_2_old_cipher) server_old.wait_for_unit("h2o.service") old_response = server_old.succeed(curl_basic) assert "Hello, old!" in old_response old_head = server_modern.succeed(curl_head) assert "strict-transport-security" in old_head server_old.succeed(curl_max_tls1_2) server_old.succeed(curl_max_tls1_2_intermediate_cipher) server_old.succeed(curl_max_tls1_2_old_cipher) ''; } ) Loading
nixos/modules/services/web-servers/h2o/common.nix 0 → 100644 +46 −0 Original line number Diff line number Diff line { lib }: { tlsRecommendationsOption = lib.mkOption { type = lib.types.nullOr ( lib.types.enum [ "modern" "intermediate" "old" ] ); default = null; example = "intermediate"; description = '' By default, H2O, without prejudice, will use as many TLS versions & cipher suites as it & the TLS library (OpenSSL) can support. The user is expected to hone settings for the security of their server. Setting some constraints is recommended, & if unsure about what TLS settings to use, this option gives curated TLS settings recommendations from Mozilla’s ‘SSL Configuration Generator’ project (see <https://ssl-config.mozilla.org>) or read more at Mozilla’s Wiki (see <https://wiki.mozilla.org/Security/Server_Side_TLS>). modern : Services with clients that support TLS 1.3 & don’t need backward compatibility intermediate : General-purpose servers with a variety of clients, recommended for almost all systems old : Compatible with a number of very old clients, & should be used only as a last resort The default for all virtual hosts can be set with services.h2o.defaultTLSRecommendations, but this value can be overridden on a per-host basis using services.h2o.hosts.<name>.tls.recommmendations. The settings will also be overidden by manual values set with services.settings.h2o.hosts.<name>.tls.extraSettings. NOTE: older/weaker ciphers might require overriding the OpenSSL version of H2O (such as `openssl_legacy`). This can be done with sevices.settings.h2o.package. ''; }; }
nixos/modules/services/web-servers/h2o/default.nix +111 −28 Original line number Diff line number Diff line Loading @@ -6,7 +6,6 @@ }: # TODO: Gems includes for Mruby # TODO: Recommended options let cfg = config.services.h2o; inherit (config.security.acme) certs; Loading @@ -22,6 +21,8 @@ let mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib; inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; settingsFormat = pkgs.formats.yaml { }; getNames = name: vhostSettings: rec { Loading Loading @@ -76,6 +77,34 @@ let all = certNames'.dependent ++ certNames'.independent; }; mozTLSRecs = if cfg.defaultTLSRecommendations != null then let # NOTE: if updating, *do* verify the changes then adjust ciphers & # other settings with the tests @ # `nixos/tests/web-servers/h2o/tls-recommendations.nix` # & run with `nix-build -A nixosTests.h2o.tls-recommendations` version = "5.7"; git_tag = "v5.7.1"; guidelinesJSON = lib.pipe { urls = [ "https://ssl-config.mozilla.org/guidelines/${version}.json" "https://raw.githubusercontent.com/mozilla/ssl-config-generator/refs/tags/${git_tag}/src/static/guidelines/${version}.json" ]; sha256 = "sha256:1mj2pcb1hg7q2wpgdq3ac8pc2q64wvwvwlkb9xjmdd9jm4hiyny7"; } [ pkgs.fetchurl builtins.readFile builtins.fromJSON ]; in guidelinesJSON.configurations else null; hostsConfig = lib.concatMapAttrs ( name: value: let Loading Loading @@ -130,7 +159,63 @@ let ] ) { "${names.server}:${builtins.toString port.TLS}" = value.settings // { "${names.server}:${builtins.toString port.TLS}" = let tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value; hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null; # NOTE: Let’s Encrypt has sunset OCSP stapling. Mozilla’s # ssl-config-generator is at present still recommending this setting, but # this module will skip setting a stapling value as Let’s Encrypt + # ACME is the most likely use case. # # See: https://github.com/mozilla/ssl-config-generator/issues/323 tlsRecAttrs = lib.optionalAttrs hasTLSRecommendations ( let recs = mozTLSRecs.${tlsRecommendations}; in { min-version = builtins.head recs.tls_versions; cipher-preference = "server"; "cipher-suite-tls1.3" = recs.ciphersuites; } // lib.optionalAttrs (recs.ciphers.openssl != [ ]) { cipher-suite = lib.concatStringsSep ":" recs.ciphers.openssl; } ); headerRecAttrs = lib.optionalAttrs ( hasTLSRecommendations && value.tls != null && builtins.elem value.tls.policy [ "force" "only" ] ) ( let headerSet = value.settings."header.set" or [ ]; recs = mozTLSRecs.${tlsRecommendations}; hsts = "Strict-Transport-Security: max-age=${builtins.toString recs.hsts_min_age}; includeSubDomains; preload"; in { "header.set" = if builtins.isString headerSet then [ headerSet hsts ] else headerSet ++ [ hsts ]; } ); in value.settings // headerRecAttrs // { listen = let identity = Loading @@ -142,7 +227,7 @@ let in { port = port.TLS; ssl = value.tls.extraSettings // { ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // { inherit identity; }; }; Loading Loading @@ -184,10 +269,12 @@ in }; package = lib.mkPackageOption pkgs "h2o" { example = '' example = # nix '' pkgs.h2o.override { withMruby = false; }; openssl = pkgs.openssl_legacy; } ''; }; Loading @@ -209,6 +296,8 @@ in example = 8443; }; defaultTLSRecommendations = tlsRecommendationsOption; settings = mkOption { type = settingsFormat.type; default = { }; Loading @@ -216,13 +305,7 @@ in }; hosts = mkOption { type = types.attrsOf ( types.submodule ( import ./vhost-options.nix { inherit config lib; } ) ); type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; })); default = { }; description = '' The `hosts` config to be merged with the settings. Loading
nixos/modules/services/web-servers/h2o/vhost-options.nix +8 −1 Original line number Diff line number Diff line { config, lib, ... }: { config, lib, ... }: let inherit (lib) Loading @@ -6,6 +10,8 @@ let mkOption types ; inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption; in { options = { Loading Loading @@ -128,6 +134,7 @@ in ] ''; }; recommendations = tlsRecommendationsOption; extraSettings = mkOption { type = types.attrs; default = { }; Loading
nixos/tests/web-servers/h2o/default.nix +1 −0 Original line number Diff line number Diff line Loading @@ -13,4 +13,5 @@ in { basic = handleTestOn supportedSystems ./basic.nix { inherit system; }; mruby = handleTestOn supportedSystems ./mruby.nix { inherit system; }; tls-recommendations = handleTestOn supportedSystems ./tls-recommendations.nix { inherit system; }; }
nixos/tests/web-servers/h2o/tls-recommendations.nix 0 → 100644 +115 −0 Original line number Diff line number Diff line import ../../make-test-python.nix ( { lib, pkgs, ... }: let domain = "acme.test"; port = 8443; hello_txt = name: pkgs.writeTextFile { name = "/hello_${name}.txt"; text = "Hello, ${name}!"; }; mkH2OServer = recommendations: { pkgs, lib, ... }: { services.h2o = { enable = true; package = pkgs.h2o.override ( lib.optionalAttrs (builtins.elem recommendations [ "intermediate" "old" ]) { openssl = pkgs.openssl_legacy; } ); defaultTLSRecommendations = "modern"; # prove overridden hosts = { "${domain}" = { tls = { inherit port recommendations; policy = "force"; identity = [ { key-file = ../../common/acme/server/acme.test.key.pem; certificate-file = ../../common/acme/server/acme.test.cert.pem; } ]; }; settings = { paths."/"."file.file" = "${hello_txt recommendations}"; }; }; }; settings = { ssl-offload = "kernel"; }; }; security.pki.certificates = [ (builtins.readFile ../../common/acme/server/ca.cert.pem) ]; networking = { firewall.allowedTCPPorts = [ port ]; extraHosts = "127.0.0.1 ${domain}"; }; }; in { name = "h2o-tls-recommendations"; meta = { maintainers = with lib.maintainers; [ toastal ]; }; nodes = { server_modern = mkH2OServer "modern"; server_intermediate = mkH2OServer "intermediate"; server_old = mkH2OServer "old"; }; testScript = let portStr = builtins.toString port; in # python '' curl_basic = "curl -v --tlsv1.3 --http2 'https://${domain}:${portStr}/'" curl_head = "curl -v --head 'https://${domain}:${portStr}/'" curl_max_tls1_2 ="curl -v --tlsv1.0 --tls-max 1.2 'https://${domain}:${portStr}/'" 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}/'" 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}/'" server_modern.wait_for_unit("h2o.service") modern_response = server_modern.succeed(curl_basic) assert "Hello, modern!" in modern_response modern_head = server_modern.succeed(curl_head) assert "strict-transport-security" in modern_head server_modern.fail(curl_max_tls1_2) server_intermediate.wait_for_unit("h2o.service") intermediate_response = server_intermediate.succeed(curl_basic) assert "Hello, intermediate!" in intermediate_response intermediate_head = server_modern.succeed(curl_head) assert "strict-transport-security" in intermediate_head server_intermediate.succeed(curl_max_tls1_2) server_intermediate.succeed(curl_max_tls1_2_intermediate_cipher) server_intermediate.fail(curl_max_tls1_2_old_cipher) server_old.wait_for_unit("h2o.service") old_response = server_old.succeed(curl_basic) assert "Hello, old!" in old_response old_head = server_modern.succeed(curl_head) assert "strict-transport-security" in old_head server_old.succeed(curl_max_tls1_2) server_old.succeed(curl_max_tls1_2_intermediate_cipher) server_old.succeed(curl_max_tls1_2_old_cipher) ''; } )