mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-12 20:55:31 +03:00
nixos/h2o: ACME support + fixups; h2o: add passthru.tests (#383282)
This commit is contained in:
commit
e4ee61d0f4
5 changed files with 442 additions and 167 deletions
|
@ -5,11 +5,11 @@
|
|||
...
|
||||
}:
|
||||
|
||||
# TODO: ACME
|
||||
# TODO: Gems includes for Mruby
|
||||
# TODO: Recommended options
|
||||
let
|
||||
cfg = config.services.h2o;
|
||||
inherit (config.security.acme) certs;
|
||||
|
||||
inherit (lib)
|
||||
literalExpression
|
||||
|
@ -20,8 +20,62 @@ let
|
|||
types
|
||||
;
|
||||
|
||||
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
|
||||
|
||||
settingsFormat = pkgs.formats.yaml { };
|
||||
|
||||
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.
|
||||
certNames =
|
||||
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 {
|
||||
dependent = [ ];
|
||||
independent = [ ];
|
||||
} acmeEnabledHostsConfigs;
|
||||
in
|
||||
certNames'
|
||||
// {
|
||||
all = certNames'.dependent ++ certNames'.independent;
|
||||
};
|
||||
|
||||
hostsConfig = lib.concatMapAttrs (
|
||||
name: value:
|
||||
let
|
||||
|
@ -29,56 +83,88 @@ let
|
|||
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
|
||||
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
|
||||
};
|
||||
serverName = if value.serverName != null then value.serverName else name;
|
||||
in
|
||||
# HTTP settings
|
||||
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
|
||||
"${serverName}:${builtins.toString port.HTTP}" = value.settings // {
|
||||
listen.port = port.HTTP;
|
||||
};
|
||||
}
|
||||
# Redirect settings
|
||||
// lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
|
||||
"${serverName}:${builtins.toString port.HTTP}" = {
|
||||
listen.port = port.HTTP;
|
||||
paths."/" = {
|
||||
redirect = {
|
||||
status = value.tls.redirectCode;
|
||||
url = "https://${serverName}:${builtins.toString port.TLS}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
# TLS settings
|
||||
//
|
||||
lib.optionalAttrs
|
||||
(
|
||||
value.tls != null
|
||||
&& builtins.elem value.tls.policy [
|
||||
"add"
|
||||
"only"
|
||||
"force"
|
||||
]
|
||||
)
|
||||
|
||||
names = getNames name value;
|
||||
|
||||
acmeSettings = lib.optionalAttrs (builtins.elem names.cert certNames.dependent) (
|
||||
let
|
||||
acmePort = 80;
|
||||
acmeChallengePath = "/.well-known/acme-challenge";
|
||||
in
|
||||
{
|
||||
"${serverName}:${builtins.toString port.TLS}" = value.settings // {
|
||||
listen =
|
||||
let
|
||||
identity = value.tls.identity;
|
||||
in
|
||||
{
|
||||
port = port.TLS;
|
||||
ssl = value.tls.extraSettings or { } // {
|
||||
inherit identity;
|
||||
};
|
||||
};
|
||||
"${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}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
tlsSettings =
|
||||
lib.optionalAttrs
|
||||
(
|
||||
value.tls != null
|
||||
&& builtins.elem value.tls.policy [
|
||||
"add"
|
||||
"only"
|
||||
"force"
|
||||
]
|
||||
)
|
||||
{
|
||||
"${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";
|
||||
};
|
||||
in
|
||||
{
|
||||
port = port.TLS;
|
||||
ssl = value.tls.extraSettings // {
|
||||
inherit identity;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
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
|
||||
);
|
||||
|
||||
# Executing H2O with our generated configuration; `mode` added as needed
|
||||
h2oExe = ''${lib.getExe cfg.package} ${
|
||||
lib.strings.escapeShellArgs [
|
||||
"--conf"
|
||||
"${h2oConfig}"
|
||||
]
|
||||
}'';
|
||||
in
|
||||
{
|
||||
options = {
|
||||
|
@ -100,7 +186,7 @@ in
|
|||
package = lib.mkPackageOption pkgs "h2o" {
|
||||
example = ''
|
||||
pkgs.h2o.override {
|
||||
withMruby = true;
|
||||
withMruby = false;
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
@ -123,21 +209,9 @@ in
|
|||
example = 8443;
|
||||
};
|
||||
|
||||
mode = mkOption {
|
||||
type =
|
||||
with types;
|
||||
nullOr (enum [
|
||||
"daemon"
|
||||
"master"
|
||||
"worker"
|
||||
"test"
|
||||
]);
|
||||
default = "master";
|
||||
description = "Operating mode of H2O";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
|
||||
};
|
||||
|
||||
|
@ -189,6 +263,47 @@ in
|
|||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
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 (certNames.all != [ ]) config.systemd.services.h2o-config-reload;
|
||||
}
|
||||
) certNames.all;
|
||||
|
||||
users = {
|
||||
users.${cfg.user} =
|
||||
{
|
||||
|
@ -201,14 +316,25 @@ in
|
|||
};
|
||||
|
||||
systemd.services.h2o = {
|
||||
description = "H2O web server service";
|
||||
description = "H2O HTTP server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
wants = lib.concatLists (map (certName: [ "acme-finished-${certName}.target" ]) certNames.all);
|
||||
# Since H2O will be hosting the challenges, H2O must be started
|
||||
before = builtins.map (certName: "acme-${certName}.service") certNames.dependent;
|
||||
after =
|
||||
[ "network.target" ]
|
||||
++ builtins.map (certName: "acme-selfsigned-${certName}.service") certNames.all
|
||||
++ builtins.map (certName: "acme-${certName}.service") certNames.independent; # avoid loading self-signed key w/ real cert, or vice-versa
|
||||
|
||||
serviceConfig = {
|
||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||
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";
|
||||
|
@ -242,22 +368,66 @@ in
|
|||
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
||||
};
|
||||
|
||||
script =
|
||||
let
|
||||
args =
|
||||
[
|
||||
"--conf"
|
||||
"${h2oConfig}"
|
||||
]
|
||||
++ lib.optionals (cfg.mode != null) [
|
||||
"--mode"
|
||||
cfg.mode
|
||||
];
|
||||
in
|
||||
''
|
||||
${lib.getExe cfg.package} ${lib.strings.escapeShellArgs args}
|
||||
'';
|
||||
preStart = "${h2oExe} --mode 'test'";
|
||||
};
|
||||
};
|
||||
|
||||
# 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") certNames.all;
|
||||
tlsServices = map (certName: "acme-${certName}.service") certNames.all;
|
||||
in
|
||||
mkIf (certNames.all != [ ]) {
|
||||
wantedBy = tlsServices ++ [ "multi-user.target" ];
|
||||
before = tlsTargets;
|
||||
after = tlsServices;
|
||||
unitConfig = {
|
||||
ConditionPathExists = map (certName: "${certs.${certName}.directory}/fullchain.pem") certNames.all;
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,6 +19,18 @@ in
|
|||
example = "example.org";
|
||||
};
|
||||
|
||||
serverAliases = mkOption {
|
||||
type = types.listOf types.nonEmptyStr;
|
||||
default = [ ];
|
||||
example = [
|
||||
"www.example.org"
|
||||
"example.org"
|
||||
];
|
||||
description = ''
|
||||
Additional names of virtual hosts served by this virtual host configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
http = mkOption {
|
||||
type = types.nullOr (
|
||||
types.submodule {
|
||||
|
@ -82,7 +94,7 @@ in
|
|||
'';
|
||||
};
|
||||
identity = mkOption {
|
||||
type = types.nonEmptyListOf (
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
key-file = mkOption {
|
||||
|
@ -96,7 +108,7 @@ in
|
|||
};
|
||||
}
|
||||
);
|
||||
default = null;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Key / certificate pairs for the virtual host.
|
||||
'';
|
||||
|
@ -104,23 +116,21 @@ in
|
|||
literalExpression
|
||||
# nix
|
||||
''
|
||||
{
|
||||
indentities = [
|
||||
{
|
||||
key-file = "/path/to/rsa.key";
|
||||
certificate-file = "/path/to/rsa.crt";
|
||||
}
|
||||
{
|
||||
key-file = "/path/to/ecdsa.key";
|
||||
certificate-file = "/path/to/ecdsa.crt";
|
||||
}
|
||||
];
|
||||
}
|
||||
[
|
||||
{
|
||||
key-file = "/path/to/rsa.key";
|
||||
certificate-file = "/path/to/rsa.crt";
|
||||
}
|
||||
{
|
||||
key-file = "/path/to/ecdsa.key";
|
||||
certificate-file = "/path/to/ecdsa.crt";
|
||||
}
|
||||
]
|
||||
'';
|
||||
};
|
||||
extraSettings = mkOption {
|
||||
type = types.nullOr types.attrs;
|
||||
default = null;
|
||||
type = types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
Additional TLS/SSL-related configuration options.
|
||||
'';
|
||||
|
@ -140,6 +150,49 @@ in
|
|||
description = "TLS options for virtual host";
|
||||
};
|
||||
|
||||
acme = mkOption {
|
||||
type = types.nullOr (
|
||||
types.addCheck (types.submodule {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to ask Let’s Encrypt to sign a certificate for this
|
||||
virtual host. Alternatively, an existing host can be used thru
|
||||
{option}`acme.useHost`.
|
||||
'';
|
||||
};
|
||||
useHost = mkOption {
|
||||
type = types.nullOr types.nonEmptyStr;
|
||||
default = null;
|
||||
description = ''
|
||||
An existing Let’s Encrypt certificate to use for this virtual
|
||||
host. This is useful if you have many subdomains and want to
|
||||
avoid hitting the [rate
|
||||
limit](https://letsencrypt.org/docs/rate-limits). Alternately,
|
||||
you can generate a certificate through {option}`acme.enable`.
|
||||
Note that this option neither creates any certificates nor does
|
||||
it add subdomains to existing ones — you will need to create
|
||||
them manually using [](#opt-security.acme.certs).
|
||||
'';
|
||||
};
|
||||
root = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = "/var/lib/acme/acme-challenge";
|
||||
description = ''
|
||||
Directory for the ACME challenge, which is **public**. Don’t put
|
||||
certs or keys in here. Set to `null` to inherit from
|
||||
config.security.acme.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}) (a: (a.enable || a.useHost != null) && !(a.enable && a.useHost != null))
|
||||
);
|
||||
default = null;
|
||||
description = "ACME options for virtual host.";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue