Commit b3f93d72 authored by toastal's avatar toastal
Browse files

nixos/h2o: TLS recommendations

From Mozilla’s ssl-config-generator project
parent 106d3395
Loading
Loading
Loading
Loading
+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.
    '';
  };
}
+111 −28
Original line number Diff line number Diff line
@@ -6,7 +6,6 @@
}:

# TODO: Gems includes for Mruby
# TODO: Recommended options
let
  cfg = config.services.h2o;
  inherit (config.security.acme) certs;
@@ -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 {
@@ -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
@@ -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 =
@@ -142,7 +227,7 @@ let
                  in
                  {
                    port = port.TLS;
                  ssl = value.tls.extraSettings // {
                    ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // {
                      inherit identity;
                    };
                  };
@@ -184,10 +269,12 @@ in
      };

      package = lib.mkPackageOption pkgs "h2o" {
        example = ''
        example = # nix
          ''
            pkgs.h2o.override {
              withMruby = false;
          };
              openssl = pkgs.openssl_legacy;
            }
          '';
      };

@@ -209,6 +296,8 @@ in
        example = 8443;
      };

      defaultTLSRecommendations = tlsRecommendationsOption;

      settings = mkOption {
        type = settingsFormat.type;
        default = { };
@@ -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.
+8 −1
Original line number Diff line number Diff line
{ config, lib, ... }:
{
  config,
  lib,
  ...
}:

let
  inherit (lib)
@@ -6,6 +10,8 @@ let
    mkOption
    types
    ;

  inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
in
{
  options = {
@@ -128,6 +134,7 @@ in
                    ]
                  '';
            };
            recommendations = tlsRecommendationsOption;
            extraSettings = mkOption {
              type = types.attrs;
              default = { };
+1 −0
Original line number Diff line number Diff line
@@ -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; };
}
+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)
      '';
  }
)