Skip to content

Commit

Permalink
nixos/h2o: TLS recommendations
Browse files Browse the repository at this point in the history
From Mozilla’s ssl-config-generator project
  • Loading branch information
toastal committed Feb 24, 2025
1 parent d9fe8e1 commit f9dcdec
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 23 deletions.
150 changes: 128 additions & 22 deletions nixos/modules/services/web-servers/h2o/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
}:

# TODO: Gems includes for Mruby
# TODO: Recommended options
let
cfg = config.services.h2o;
inherit (config.security.acme) certs;
Expand All @@ -24,6 +23,31 @@ let

settingsFormat = pkgs.formats.yaml { };

tlsRecommendationsOption = mkOption {
type = types.nullOr (
types.enum [
"modern"
"intermediate"
"old"
]
);
default = null;
example = "intermediate";
description = ''
TLS settings recommendations from Mozilla’s ssl-config-generator project
(see <https://ssl-config.mozilla.org>).
modern
: Services with clients that support TLS 1.3 and 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, and should be used only as a last resort
'';
};

getNames = name: vhostSettings: rec {
server = if vhostSettings.serverName != null then vhostSettings.serverName else name;
cert =
Expand Down Expand Up @@ -76,6 +100,33 @@ 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";
tag = "v5.7.1";
guidelinesJSON =
lib.pipe
{

url = "https://raw.githubusercontent.com/mozilla/ssl-config-generator/refs/tags/${tag}/src/static/guidelines/${version}.json";
sha256 = "sha256:1mj2pcb1hg7q2wpgdq3ac8pc2q64wvwvwlkb9xjmdd9jm4hiyny7";
}
[
builtins.fetchurl
builtins.readFile
builtins.fromJSON
];
in
assert (lib.hasAttr "configurations" guidelinesJSON);
guidelinesJSON.configurations
else
null;

hostsConfig = lib.concatMapAttrs (
name: value:
let
Expand Down Expand Up @@ -130,23 +181,74 @@ let
]
)
{
"${names.server}:${builtins.toString port.TLS}" = value.settings // {
listen =
let
identity =
value.tls.identity
++ lib.optional (builtins.elem names.cert certNames.all) {
key-file = "${certs.${names.cert}.directory}/key.pem";
certificate-file = "${certs.${names.cert}.directory}/fullchain.pem";
"${names.server}:${builtins.toString port.TLS}" =
let
tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value;

hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null;

tlsRecAttrs = lib.optionalAttrs hasTLSRecommendations (
let
recs = mozTLSRecs.${tlsRecommendations};
in
# NOTE: OCSP stapling is being ignored since Let’s Encrypt
# has sunset it
{
min-version = builtins.head recs.tls_versions;
"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
// {
listen =
let
identity =
value.tls.identity
++ lib.optional (builtins.elem names.cert certNames.all) {
key-file = "${certs.${names.cert}.directory}/key.pem";
certificate-file = "${certs.${names.cert}.directory}/fullchain.pem";
};
in
{
port = port.TLS;
ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // {
inherit identity;
};
in
{
port = port.TLS;
ssl = value.tls.extraSettings // {
inherit identity;
};
};
};
}
// headerRecAttrs;
};
in
# With a high likelihood of HTTP & ACME challenges being on the same port,
Expand Down Expand Up @@ -184,11 +286,13 @@ in
};

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

defaultHTTPListenPort = mkOption {
Expand All @@ -209,6 +313,8 @@ in
example = 8443;
};

defaultTLSRecommendations = tlsRecommendationsOption;

settings = mkOption {
type = settingsFormat.type;
default = { };
Expand All @@ -219,7 +325,7 @@ in
type = types.attrsOf (
types.submodule (
import ./vhost-options.nix {
inherit config lib;
inherit config lib tlsRecommendationsOption;
}
)
);
Expand Down
8 changes: 7 additions & 1 deletion nixos/modules/services/web-servers/h2o/vhost-options.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{ config, lib, ... }:
{
config,
lib,
tlsRecommendationsOption,
...
}:

let
inherit (lib)
Expand Down Expand Up @@ -128,6 +133,7 @@ in
]
'';
};
recommendations = tlsRecommendationsOption;
extraSettings = mkOption {
type = types.attrs;
default = { };
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/web-servers/h2o/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 115 additions & 0 deletions nixos/tests/web-servers/h2o/tls-recommendations.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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 = recommendations;
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)
'';
}
)

0 comments on commit f9dcdec

Please sign in to comment.