2025-02-12 12:42:02 +07:00
|
|
|
|
{
|
|
|
|
|
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;
|
2025-02-12 12:42:02 +07:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2025-02-23 17:35:25 +07:00
|
|
|
|
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
|
|
|
|
|
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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.
|
2025-03-28 22:25:08 +07:00
|
|
|
|
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;
|
|
|
|
|
|
2025-03-28 22:25:08 +07:00
|
|
|
|
certNames = lib.lists.foldl partition {
|
2025-02-23 09:46:16 +07:00
|
|
|
|
dependent = [ ];
|
|
|
|
|
independent = [ ];
|
|
|
|
|
} acmeEnabledHostsConfigs;
|
|
|
|
|
in
|
2025-03-28 22:25:08 +07:00
|
|
|
|
certNames
|
2025-02-23 09:46:16 +07:00
|
|
|
|
// {
|
2025-03-28 22:25:08 +07:00
|
|
|
|
all = certNames.dependent ++ certNames.independent;
|
2025-02-23 09:46:16 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-02-23 17:35:25 +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;
|
|
|
|
|
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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;
|
|
|
|
|
|
2025-03-28 22:25:08 +07:00
|
|
|
|
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-12 12:42:02 +07:00
|
|
|
|
};
|
|
|
|
|
};
|
2025-02-23 09:46:16 +07:00
|
|
|
|
|
|
|
|
|
tlsSettings =
|
|
|
|
|
lib.optionalAttrs
|
|
|
|
|
(
|
|
|
|
|
value.tls != null
|
|
|
|
|
&& builtins.elem value.tls.policy [
|
|
|
|
|
"add"
|
|
|
|
|
"only"
|
|
|
|
|
"force"
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
{
|
2025-02-23 17:35:25 +07:00
|
|
|
|
"${names.server}:${builtins.toString port.TLS}" =
|
|
|
|
|
let
|
|
|
|
|
tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value;
|
|
|
|
|
|
|
|
|
|
hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null;
|
|
|
|
|
|
2025-03-27 22:54:59 +07:00
|
|
|
|
# ATTENTION: Let’s Encrypt has sunset OCSP stapling.
|
|
|
|
|
tlsRecAttrs =
|
|
|
|
|
# If using ACME, this module will disable H2O’s default OCSP
|
|
|
|
|
# stapling.
|
|
|
|
|
#
|
|
|
|
|
# See: https://letsencrypt.org/2024/12/05/ending-ocsp/
|
2025-03-28 22:25:08 +07:00
|
|
|
|
lib.optionalAttrs (builtins.elem names.cert acmeCertNames.all) {
|
2025-03-27 22:54:59 +07:00
|
|
|
|
ocsp-update-interval = 0;
|
2025-02-23 17:35:25 +07:00
|
|
|
|
}
|
2025-03-27 22:54:59 +07:00
|
|
|
|
# Mozilla’s ssl-config-generator is at present still
|
|
|
|
|
# recommending this setting as well, 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
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-02-23 17:35:25 +07:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-02-23 17:35:25 +07:00
|
|
|
|
listen =
|
|
|
|
|
let
|
|
|
|
|
identity =
|
|
|
|
|
value.tls.identity
|
2025-03-28 22:25:08 +07:00
|
|
|
|
++ lib.optional (builtins.elem names.cert acmeCertNames.all) {
|
2025-02-23 17:35:25 +07:00
|
|
|
|
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; });
|
2025-02-23 17:35:25 +07:00
|
|
|
|
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-12 12:42:02 +07:00
|
|
|
|
};
|
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
|
2025-02-12 12:42:02 +07:00
|
|
|
|
) 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}"
|
|
|
|
|
]
|
|
|
|
|
}'';
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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" {
|
2025-02-23 17:35:25 +07:00
|
|
|
|
example = # nix
|
|
|
|
|
''
|
|
|
|
|
pkgs.h2o.override {
|
|
|
|
|
withMruby = false;
|
|
|
|
|
openssl = pkgs.openssl_legacy;
|
|
|
|
|
}
|
|
|
|
|
'';
|
2025-02-12 12:42:02 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-23 17:35:25 +07:00
|
|
|
|
defaultTLSRecommendations = tlsRecommendationsOption;
|
|
|
|
|
|
2025-02-12 12:42:02 +07:00
|
|
|
|
settings = mkOption {
|
|
|
|
|
type = settingsFormat.type;
|
2025-02-20 15:29:15 +07:00
|
|
|
|
default = { };
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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";
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
'';
|
2025-02-12 12:42:02 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
hosts = mkOption {
|
2025-02-23 17:35:25 +07:00
|
|
|
|
type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; }));
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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 = [
|
2025-02-12 12:42:02 +07:00
|
|
|
|
{
|
|
|
|
|
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
|
2025-03-28 22:25:08 +07:00
|
|
|
|
] ++ lib.optional (acmeCertNames.all != [ ]) config.systemd.services.h2o-config-reload;
|
2025-02-23 09:46:16 +07:00
|
|
|
|
}
|
2025-03-28 22:25:08 +07:00
|
|
|
|
) acmeCertNames.all;
|
2025-02-23 09:46:16 +07:00
|
|
|
|
|
2025-02-12 12:42:02 +07:00
|
|
|
|
users = {
|
|
|
|
|
users.${cfg.user} =
|
|
|
|
|
{
|
|
|
|
|
group = cfg.group;
|
|
|
|
|
}
|
|
|
|
|
// lib.optionalAttrs (cfg.user == "h2o") {
|
|
|
|
|
isSystemUser = true;
|
|
|
|
|
};
|
|
|
|
|
groups.${cfg.group} = { };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
systemd.services.h2o = {
|
2025-02-22 09:51:45 +07:00
|
|
|
|
description = "H2O HTTP server";
|
2025-02-12 12:42:02 +07:00
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
2025-03-28 22:25:08 +07:00
|
|
|
|
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
|
2025-03-28 22:25:08 +07:00
|
|
|
|
before = builtins.map (certName: "acme-${certName}.service") acmeCertNames.dependent;
|
2025-02-23 09:46:16 +07:00
|
|
|
|
after =
|
|
|
|
|
[ "network.target" ]
|
2025-03-28 22:25:08 +07:00
|
|
|
|
++ 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
|
2025-02-12 12:42:02 +07:00
|
|
|
|
|
|
|
|
|
serviceConfig = {
|
2025-02-23 09:46:16 +07:00
|
|
|
|
ExecStart = "${h2oExe} --mode 'master'";
|
|
|
|
|
ExecReload = [
|
|
|
|
|
"${h2oExe} --mode 'test'"
|
|
|
|
|
"${pkgs.coreutils}/bin/kill -HUP $MAINPID"
|
|
|
|
|
];
|
2025-02-12 12:42:02 +07:00
|
|
|
|
ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
|
|
|
|
|
User = cfg.user;
|
2025-02-21 19:53:46 +07:00
|
|
|
|
Group = cfg.group;
|
2025-02-12 12:42:02 +07:00
|
|
|
|
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-12 12:42:02 +07:00
|
|
|
|
};
|
|
|
|
|
|
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
|
2025-03-28 22:25:08 +07:00
|
|
|
|
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
|
2025-03-28 22:25:08 +07:00
|
|
|
|
mkIf (acmeCertNames.all != [ ]) {
|
2025-02-23 09:46:16 +07:00
|
|
|
|
wantedBy = tlsServices ++ [ "multi-user.target" ];
|
|
|
|
|
before = tlsTargets;
|
|
|
|
|
after = tlsServices;
|
|
|
|
|
unitConfig = {
|
2025-03-28 22:25:08 +07:00
|
|
|
|
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 Nginx’s FIXME, there’s 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;
|
|
|
|
|
};
|
2025-02-12 12:42:02 +07:00
|
|
|
|
}
|