···
# TODO: Gems includes for Mruby
# TODO: Recommended options
cfg = config.services.h2o;
12
+
inherit (config.security.acme) certs;
···
23
+
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
settingsFormat = pkgs.formats.yaml { };
27
+
getNames = name: vhostSettings: rec {
28
+
server = if vhostSettings.serverName != null then vhostSettings.serverName else name;
30
+
if lib.attrByPath [ "acme" "useHost" ] null vhostSettings == null then
33
+
vhostSettings.acme.useHost;
36
+
# Attrset with the virtual hosts relevant to ACME configuration
37
+
acmeEnabledHostsConfigs = lib.foldlAttrs (
39
+
if value.acme == null || (!value.acme.enable && value.acme.useHost == null) then
43
+
names = getNames name value;
44
+
virtualHostConfig = value // {
45
+
serverName = names.server;
46
+
certName = names.cert;
49
+
acc ++ [ virtualHostConfig ]
52
+
# Attrset with the ACME certificate names split by whether or not they depend
53
+
# on H2O serving challenges.
59
+
inherit (vhostSettings) certName;
60
+
isDependent = certs.${certName}.dnsProvider == null;
62
+
if isDependent && !(builtins.elem certName acc.dependent) then
63
+
acc // { dependent = acc.dependent ++ [ certName ]; }
64
+
else if !isDependent && !(builtins.elem certName acc.independent) then
65
+
acc // { independent = acc.independent ++ [ certName ]; }
69
+
certNames' = lib.lists.foldl partition {
72
+
} acmeEnabledHostsConfigs;
76
+
all = certNames'.dependent ++ certNames'.independent;
hostsConfig = lib.concatMapAttrs (
···
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
32
-
serverName = if value.serverName != null then value.serverName else name;
35
-
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
36
-
"${serverName}:${builtins.toString port.HTTP}" = value.settings // {
37
-
listen.port = port.HTTP;
41
-
// lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
42
-
"${serverName}:${builtins.toString port.HTTP}" = {
43
-
listen.port = port.HTTP;
46
-
status = value.tls.redirectCode;
47
-
url = "https://${serverName}:${builtins.toString port.TLS}";
87
+
names = getNames name value;
89
+
acmeSettings = lib.optionalAttrs (builtins.elem names.cert certNames.dependent) (
92
+
acmeChallengePath = "/.well-known/acme-challenge";
95
+
"${names.server}:${builtins.toString acmePort}" = {
96
+
listen.port = acmePort;
97
+
paths."${acmeChallengePath}/" = {
98
+
"file.dir" = value.acme.root + acmeChallengePath;
105
+
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
106
+
"${names.server}:${builtins.toString port.HTTP}" = value.settings // {
107
+
listen.port = port.HTTP;
110
+
// lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
111
+
"${names.server}:${builtins.toString port.HTTP}" = {
112
+
listen.port = port.HTTP;
115
+
status = value.tls.redirectCode;
116
+
url = "https://${names.server}:${builtins.toString port.TLS}";
57
-
&& builtins.elem value.tls.policy [
64
-
"${serverName}:${builtins.toString port.TLS}" = value.settings // {
67
-
identity = value.tls.identity;
71
-
ssl = value.tls.extraSettings or { } // {
126
+
&& builtins.elem value.tls.policy [
133
+
"${names.server}:${builtins.toString port.TLS}" = value.settings // {
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";
145
+
ssl = value.tls.extraSettings // {
152
+
# With a high likelihood of HTTP & ACME challenges being on the same port,
153
+
# 80, do a recursive update to merge the 2 settings together
154
+
(lib.recursiveUpdate acmeSettings httpSettings) // tlsSettings
h2oConfig = settingsFormat.generate "h2o.yaml" (
lib.recursiveUpdate { hosts = hostsConfig; } cfg.settings
161
+
# Executing H2O with our generated configuration; `mode` added as needed
162
+
h2oExe = ''${lib.getExe cfg.package} ${
163
+
lib.strings.escapeShellArgs [
···
package = lib.mkPackageOption pkgs "h2o" {
···
135
-
default = "master";
136
-
description = "Operating mode of H2O";
type = settingsFormat.type;
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
···
config = mkIf cfg.enable {
270
+
!(builtins.hasAttr "hosts" h2oConfig)
274
+
hasKeyPlusCert = attrs: (attrs.key-file or "") != "" && (attrs.certificate-file or "") != "";
277
+
(lib.attrByPath [ "listen" "ssl" ] null host == null)
278
+
# TLS identity property
280
+
builtins.hasAttr "identity" host
281
+
&& builtins.length host.identity > 0
282
+
&& builtins.all hasKeyPlusCert host.listen.ssl.identity
284
+
# TLS short-hand (was manually specified)
285
+
|| (hasKeyPlusCert host.listen.ssl)
286
+
) (lib.attrValues h2oConfig.hosts);
288
+
TLS support will require at least one non-empty certificate & key
289
+
file. Use services.h2o.hosts.<name>.acme.enable,
290
+
services.h2o.hosts.<name>.acme.useHost,
291
+
services.h2o.hosts.<name>.tls.identity, or
292
+
services.h2o.hosts.<name>.tls.extraSettings.
298
+
mkCertOwnershipAssertion {
299
+
cert = certs.${name};
300
+
groups = config.users.groups;
302
+
config.systemd.services.h2o
303
+
] ++ lib.optional (certNames.all != [ ]) config.systemd.services.h2o-config-reload;
···
204
-
description = "H2O web server service";
319
+
description = "H2O HTTP server";
wantedBy = [ "multi-user.target" ];
206
-
after = [ "network.target" ];
321
+
wants = lib.concatLists (map (certName: [ "acme-finished-${certName}.target" ]) certNames.all);
322
+
# Since H2O will be hosting the challenges, H2O must be started
323
+
before = builtins.map (certName: "acme-${certName}.service") certNames.dependent;
325
+
[ "network.target" ]
326
+
++ builtins.map (certName: "acme-selfsigned-${certName}.service") certNames.all
327
+
++ builtins.map (certName: "acme-${certName}.service") certNames.independent; # avoid loading self-signed key w/ real cert, or vice-versa
209
-
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
330
+
ExecStart = "${h2oExe} --mode 'master'";
332
+
"${h2oExe} --mode 'test'"
333
+
"${pkgs.coreutils}/bin/kill -HUP $MAINPID"
ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
RuntimeDirectory = "h2o";
···
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
252
-
++ lib.optionals (cfg.mode != null) [
258
-
${lib.getExe cfg.package} ${lib.strings.escapeShellArgs args}
371
+
preStart = "${h2oExe} --mode 'test'";
374
+
# This service waits for all certificates to be available before reloading
375
+
# H2O configuration. `tlsTargets` are added to `wantedBy` + `before` which
376
+
# allows the `acme-finished-$cert.target` to signify the successful updating
377
+
# of certs end-to-end.
378
+
systemd.services.h2o-config-reload =
380
+
tlsTargets = map (certName: "acme-${certName}.target") certNames.all;
381
+
tlsServices = map (certName: "acme-${certName}.service") certNames.all;
383
+
mkIf (certNames.all != [ ]) {
384
+
wantedBy = tlsServices ++ [ "multi-user.target" ];
385
+
before = tlsTargets;
386
+
after = tlsServices;
388
+
ConditionPathExists = map (certName: "${certs.${certName}.directory}/fullchain.pem") certNames.all;
389
+
# Disable rate limiting for this since it may be triggered quickly
390
+
# a bunch of times if a lot of certificates are renewed in quick
391
+
# succession. The reload itself is cheap, so even doing a lot of them
392
+
# in a short burst is fine.
394
+
# FIXME: like Nginx’s FIXME, there’s probably a better way to do
396
+
StartLimitIntervalSec = 0;
401
+
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active h2o.service";
402
+
ExecStart = "/run/current-system/systemd/bin/systemctl reload h2o.service";
406
+
security.acme.certs =
409
+
acc: vhostSettings:
410
+
if vhostSettings.acme.useHost == null then
412
+
hasRoot = vhostSettings.acme.root != null;
416
+
"${vhostSettings.serverName}" = {
417
+
group = mkDefault cfg.group;
418
+
# If `acme.root` is `null`, inherit `config.security.acme`.
419
+
# Since `config.security.acme.certs.<cert>.webroot`’s own
420
+
# default value should take precedence set priority higher than
422
+
webroot = lib.mkOverride (if hasRoot then 1000 else 2000) vhostSettings.acme.root;
423
+
# Also nudge dnsProvider to null in case it is inherited
424
+
dnsProvider = lib.mkOverride (if hasRoot then 1000 else 2000) null;
425
+
extraDomainNames = vhostSettings.serverAliases;
431
+
lib.lists.foldl mkCerts { } acmeEnabledHostsConfigs;