nixos/h2o: TLS recommendations (#384730)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
{ 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.
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -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,23 +159,79 @@ 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;
|
||||
|
||||
# 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 =
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
# With a high likelihood of HTTP & ACME challenges being on the same port,
|
||||
@@ -184,11 +269,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 {
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{ 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 = { };
|
||||
@@ -195,6 +202,7 @@ in
|
||||
|
||||
settings = mkOption {
|
||||
type = types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
Attrset to be transformed into YAML for host config. Note that the HTTP
|
||||
/ TLS configurations will override these config values.
|
||||
|
||||
@@ -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; };
|
||||
}
|
||||
|
||||
@@ -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 = "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)
|
||||
'';
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user