1
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-06-27 19:46:40 +03:00
nixpkgs/nixos/modules/services/web-servers/h2o/default.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

554 lines
18 KiB
Nix
Raw Normal View History

{
config,
lib,
pkgs,
...
}:
# TODO: Gems includes for Mruby
let
cfg = config.services.h2o;
2025-02-23 09:46:16 +07:00
inherit (config.security.acme) certs;
inherit (lib)
literalExpression
mkDefault
mkEnableOption
mkIf
mkOption
types
;
2025-02-21 16:31:48 +07:00
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
settingsFormat = pkgs.formats.yaml { };
2025-02-23 09:46:16 +07:00
getNames = name: vhostSettings: rec {
server = if vhostSettings.serverName != null then vhostSettings.serverName else name;
cert =
if lib.attrByPath [ "acme" "useHost" ] null vhostSettings == null then
server
else
vhostSettings.acme.useHost;
};
# Attrset with the virtual hosts relevant to ACME configuration
acmeEnabledHostsConfigs = lib.foldlAttrs (
acc: name: value:
if value.acme == null || (!value.acme.enable && value.acme.useHost == null) then
acc
else
let
names = getNames name value;
virtualHostConfig = value // {
serverName = names.server;
certName = names.cert;
};
in
acc ++ [ virtualHostConfig ]
) [ ] cfg.hosts;
# Attrset with the ACME certificate names split by whether or not they depend
# on H2O serving challenges.
acmeCertNames =
2025-02-23 09:46:16 +07:00
let
partition =
acc: vhostSettings:
let
inherit (vhostSettings) certName;
isDependent = certs.${certName}.dnsProvider == null;
in
if isDependent && !(builtins.elem certName acc.dependent) then
acc // { dependent = acc.dependent ++ [ certName ]; }
else if !isDependent && !(builtins.elem certName acc.independent) then
acc // { independent = acc.independent ++ [ certName ]; }
else
acc;
certNames = lib.lists.foldl partition {
2025-02-23 09:46:16 +07:00
dependent = [ ];
independent = [ ];
} acmeEnabledHostsConfigs;
in
certNames
2025-02-23 09:46:16 +07:00
// {
all = certNames.dependent ++ certNames.independent;
2025-02-23 09:46:16 +07:00
};
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
port = {
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
};
2025-02-23 09:46:16 +07:00
names = getNames name value;
acmeSettings = lib.optionalAttrs (builtins.elem names.cert acmeCertNames.dependent) (
2025-02-23 09:46:16 +07:00
let
acmePort = 80;
acmeChallengePath = "/.well-known/acme-challenge";
in
{
"${names.server}:${builtins.toString acmePort}" = {
listen.port = acmePort;
paths."${acmeChallengePath}/" = {
"file.dir" = value.acme.root + acmeChallengePath;
};
};
}
);
httpSettings =
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
"${names.server}:${builtins.toString port.HTTP}" = value.settings // {
listen.port = port.HTTP;
};
}
// lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
"${names.server}:${builtins.toString port.HTTP}" = {
listen.port = port.HTTP;
paths."/" = {
redirect = {
status = value.tls.redirectCode;
url = "https://${names.server}:${builtins.toString port.TLS}";
};
};
};
};
2025-02-23 09:46:16 +07:00
tlsSettings =
lib.optionalAttrs
(
value.tls != null
&& builtins.elem value.tls.policy [
"add"
"only"
"force"
]
)
{
"${names.server}:${builtins.toString port.TLS}" =
let
tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value;
hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null;
# ATTENTION: Lets Encrypt has sunset OCSP stapling.
tlsRecAttrs =
# If using ACME, this module will disable H2Os default OCSP
# stapling.
#
# See: https://letsencrypt.org/2024/12/05/ending-ocsp/
lib.optionalAttrs (builtins.elem names.cert acmeCertNames.all) {
ocsp-update-interval = 0;
}
# Mozillas ssl-config-generator is at present still
# recommending this setting as well, but this module will
# skip setting a stapling value as Lets Encrypt + ACME is
# the most likely use case.
#
# See: https://github.com/mozilla/ssl-config-generator/issues/323
// 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 ];
}
);
2025-03-27 18:57:52 +07:00
listen =
let
identity =
value.tls.identity
++ lib.optional (builtins.elem names.cert acmeCertNames.all) {
key-file = "${certs.${names.cert}.directory}/key.pem";
certificate-file = "${certs.${names.cert}.directory}/fullchain.pem";
};
2025-03-27 18:57:52 +07:00
baseListen =
{
port = port.TLS;
ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // {
inherit identity;
};
}
// lib.optionalAttrs (value.host != null) {
host = value.host;
};
# QUIC, if used, will duplicate the TLS over TCP directive, but
# append some extra QUIC-related settings
quicListen = lib.optional (value.tls.quic != null) (baseListen // { inherit (value.tls) quic; });
in
{
2025-03-27 18:57:52 +07:00
listen = [ baseListen ] ++ quicListen;
2025-02-23 09:46:16 +07:00
};
2025-03-27 18:57:52 +07:00
in
value.settings // headerRecAttrs // listen;
};
2025-02-23 09:46:16 +07:00
in
# With a high likelihood of HTTP & ACME challenges being on the same port,
# 80, do a recursive update to merge the 2 settings together
(lib.recursiveUpdate acmeSettings httpSettings) // tlsSettings
) cfg.hosts;
h2oConfig = settingsFormat.generate "h2o.yaml" (
lib.recursiveUpdate { hosts = hostsConfig; } cfg.settings
);
2025-02-23 09:46:16 +07:00
# Executing H2O with our generated configuration; `mode` added as needed
h2oExe = ''${lib.getExe cfg.package} ${
lib.strings.escapeShellArgs [
"--conf"
"${h2oConfig}"
]
}'';
in
{
options = {
services.h2o = {
enable = mkEnableOption "H2O web server";
user = mkOption {
type = types.nonEmptyStr;
default = "h2o";
description = "User running H2O service";
};
group = mkOption {
type = types.nonEmptyStr;
default = "h2o";
description = "Group running H2O services";
};
package = lib.mkPackageOption pkgs "h2o" {
example = # nix
''
pkgs.h2o.override {
withMruby = false;
openssl = pkgs.openssl_legacy;
}
'';
};
defaultHTTPListenPort = mkOption {
type = types.port;
default = 80;
description = ''
If hosts do not specify listen.port, use these ports for HTTP by default.
'';
example = 8080;
};
defaultTLSListenPort = mkOption {
type = types.port;
default = 443;
description = ''
If hosts do not specify listen.port, use these ports for SSL by default.
'';
example = 8443;
};
defaultTLSRecommendations = tlsRecommendationsOption;
settings = mkOption {
type = settingsFormat.type;
default = { };
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
2025-02-27 12:50:34 +07:00
example =
literalExpression
# nix
''
{
compress = "ON";
ssl-offload = "kernel";
http2-reprioritize-blocking-assets = "ON";
"file.mime.addtypes" = {
"text/x-rst" = {
extensions = [ ".rst" ];
is_compressible = "YES";
};
};
}
'';
};
hosts = mkOption {
type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; }));
default = { };
description = ''
The `hosts` config to be merged with the settings.
Note that unlike YAML used for H2O, Nix will not support duplicate
keys to, for instance, have multiple listens in a host block; use the
virtual host options in like `http` & `tls` or use `$HOST:$PORT`
keys if manually specifying config.
'';
example =
literalExpression
# nix
''
{
"hydra.example.com" = {
tls = {
policy = "force";
2025-03-11 18:35:53 +07:00
identity = [
{
key-file = "/path/to/key";
certificate-file = "/path/to/cert";
};
];
extraSettings = {
minimum-version = "TLSv1.3";
};
};
settings = {
paths."/" = {
"file:dir" = "/var/www/default";
};
};
};
}
'';
};
};
};
config = mkIf cfg.enable {
2025-02-23 09:46:16 +07:00
assertions =
[
{
assertion =
!(builtins.hasAttr "hosts" h2oConfig)
|| builtins.all (
host:
let
hasKeyPlusCert = attrs: (attrs.key-file or "") != "" && (attrs.certificate-file or "") != "";
in
# TLS not used
(lib.attrByPath [ "listen" "ssl" ] null host == null)
# TLS identity property
|| (
builtins.hasAttr "identity" host
&& builtins.length host.identity > 0
&& builtins.all hasKeyPlusCert host.listen.ssl.identity
)
# TLS short-hand (was manually specified)
|| (hasKeyPlusCert host.listen.ssl)
) (lib.attrValues h2oConfig.hosts);
message = ''
TLS support will require at least one non-empty certificate & key
file. Use services.h2o.hosts.<name>.acme.enable,
services.h2o.hosts.<name>.acme.useHost,
services.h2o.hosts.<name>.tls.identity, or
services.h2o.hosts.<name>.tls.extraSettings.
'';
}
]
++ builtins.map (
name:
mkCertOwnershipAssertion {
cert = certs.${name};
groups = config.users.groups;
services = [
config.systemd.services.h2o
] ++ lib.optional (acmeCertNames.all != [ ]) config.systemd.services.h2o-config-reload;
2025-02-23 09:46:16 +07:00
}
) acmeCertNames.all;
2025-02-23 09:46:16 +07:00
users = {
users.${cfg.user} =
{
group = cfg.group;
}
// lib.optionalAttrs (cfg.user == "h2o") {
isSystemUser = true;
};
groups.${cfg.group} = { };
};
systemd.services.h2o = {
description = "H2O HTTP server";
wantedBy = [ "multi-user.target" ];
wants = lib.concatLists (map (certName: [ "acme-finished-${certName}.target" ]) acmeCertNames.all);
2025-02-23 09:46:16 +07:00
# Since H2O will be hosting the challenges, H2O must be started
before = builtins.map (certName: "acme-${certName}.service") acmeCertNames.dependent;
2025-02-23 09:46:16 +07:00
after =
[ "network.target" ]
++ builtins.map (certName: "acme-selfsigned-${certName}.service") acmeCertNames.all
++ builtins.map (certName: "acme-${certName}.service") acmeCertNames.independent; # avoid loading self-signed key w/ real cert, or vice-versa
serviceConfig = {
2025-02-23 09:46:16 +07:00
ExecStart = "${h2oExe} --mode 'master'";
ExecReload = [
"${h2oExe} --mode 'test'"
"${pkgs.coreutils}/bin/kill -HUP $MAINPID"
];
ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
User = cfg.user;
Group = cfg.group;
Restart = "always";
RestartSec = "10s";
RuntimeDirectory = "h2o";
RuntimeDirectoryMode = "0750";
CacheDirectory = "h2o";
CacheDirectoryMode = "0750";
LogsDirectory = "h2o";
LogsDirectoryMode = "0750";
ProtectSystem = "strict";
ProtectHome = mkDefault true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
};
2025-02-23 09:46:16 +07:00
preStart = "${h2oExe} --mode 'test'";
};
2025-02-23 09:46:16 +07:00
# This service waits for all certificates to be available before reloading
# H2O configuration. `tlsTargets` are added to `wantedBy` + `before` which
# allows the `acme-finished-$cert.target` to signify the successful updating
# of certs end-to-end.
systemd.services.h2o-config-reload =
let
tlsTargets = map (certName: "acme-${certName}.target") acmeCertNames.all;
tlsServices = map (certName: "acme-${certName}.service") acmeCertNames.all;
2025-02-23 09:46:16 +07:00
in
mkIf (acmeCertNames.all != [ ]) {
2025-02-23 09:46:16 +07:00
wantedBy = tlsServices ++ [ "multi-user.target" ];
before = tlsTargets;
after = tlsServices;
unitConfig = {
ConditionPathExists = map (
certName: "${certs.${certName}.directory}/fullchain.pem"
) acmeCertNames.all;
2025-02-23 09:46:16 +07:00
# Disable rate limiting for this since it may be triggered quickly
# a bunch of times if a lot of certificates are renewed in quick
# succession. The reload itself is cheap, so even doing a lot of them
# in a short burst is fine.
#
# FIXME: like Nginxs FIXME, theres probably a better way to do
# this.
StartLimitIntervalSec = 0;
};
serviceConfig = {
Type = "oneshot";
TimeoutSec = 60;
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active h2o.service";
ExecStart = "/run/current-system/systemd/bin/systemctl reload h2o.service";
};
};
security.acme.certs =
let
mkCerts =
acc: vhostSettings:
if vhostSettings.acme.useHost == null then
let
hasRoot = vhostSettings.acme.root != null;
in
acc
// {
"${vhostSettings.serverName}" = {
group = mkDefault cfg.group;
# If `acme.root` is `null`, inherit `config.security.acme`.
# Since `config.security.acme.certs.<cert>.webroot`s own
# default value should take precedence set priority higher than
# mkOptionDefault
webroot = lib.mkOverride (if hasRoot then 1000 else 2000) vhostSettings.acme.root;
# Also nudge dnsProvider to null in case it is inherited
dnsProvider = lib.mkOverride (if hasRoot then 1000 else 2000) null;
extraDomainNames = vhostSettings.serverAliases;
};
}
else
acc;
in
lib.lists.foldl mkCerts { } acmeEnabledHostsConfigs;
};
}