Skip to content

Commit bec9ad1

Browse files
authored
nixos/h2o: TLS recommendations (#384730)
2 parents 723693d + b3f93d7 commit bec9ad1

File tree

5 files changed

+282
-29
lines changed

5 files changed

+282
-29
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
}

nixos/modules/services/web-servers/h2o/default.nix

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
}:
77

88
# TODO: Gems includes for Mruby
9-
# TODO: Recommended options
109
let
1110
cfg = config.services.h2o;
1211
inherit (config.security.acme) certs;
@@ -22,6 +21,8 @@ let
2221

2322
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
2423

24+
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
25+
2526
settingsFormat = pkgs.formats.yaml { };
2627

2728
getNames = name: vhostSettings: rec {
@@ -76,6 +77,34 @@ let
7677
all = certNames'.dependent ++ certNames'.independent;
7778
};
7879

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+
79108
hostsConfig = lib.concatMapAttrs (
80109
name: value:
81110
let
@@ -130,23 +159,79 @@ let
130159
]
131160
)
132161
{
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;
141232
};
142-
in
143-
{
144-
port = port.TLS;
145-
ssl = value.tls.extraSettings // {
146-
inherit identity;
147233
};
148-
};
149-
};
234+
};
150235
};
151236
in
152237
# With a high likelihood of HTTP & ACME challenges being on the same port,
@@ -184,11 +269,13 @@ in
184269
};
185270

186271
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+
'';
192279
};
193280

194281
defaultHTTPListenPort = mkOption {
@@ -209,20 +296,16 @@ in
209296
example = 8443;
210297
};
211298

299+
defaultTLSRecommendations = tlsRecommendationsOption;
300+
212301
settings = mkOption {
213302
type = settingsFormat.type;
214303
default = { };
215304
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
216305
};
217306

218307
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; }));
226309
default = { };
227310
description = ''
228311
The `hosts` config to be merged with the settings.

nixos/modules/services/web-servers/h2o/vhost-options.nix

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
{ config, lib, ... }:
1+
{
2+
config,
3+
lib,
4+
...
5+
}:
26

37
let
48
inherit (lib)
59
literalExpression
610
mkOption
711
types
812
;
13+
14+
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
915
in
1016
{
1117
options = {
@@ -128,6 +134,7 @@ in
128134
]
129135
'';
130136
};
137+
recommendations = tlsRecommendationsOption;
131138
extraSettings = mkOption {
132139
type = types.attrs;
133140
default = { };
@@ -195,6 +202,7 @@ in
195202

196203
settings = mkOption {
197204
type = types.attrs;
205+
default = { };
198206
description = ''
199207
Attrset to be transformed into YAML for host config. Note that the HTTP
200208
/ TLS configurations will override these config values.

nixos/tests/web-servers/h2o/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ in
1313
{
1414
basic = handleTestOn supportedSystems ./basic.nix { inherit system; };
1515
mruby = handleTestOn supportedSystems ./mruby.nix { inherit system; };
16+
tls-recommendations = handleTestOn supportedSystems ./tls-recommendations.nix { inherit system; };
1617
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
)

0 commit comments

Comments
 (0)