mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-13 21:25:30 +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: Gems includes for Mruby
|
||||||
# TODO: Recommended options
|
# TODO: Recommended options
|
||||||
let
|
let
|
||||||
cfg = config.services.h2o;
|
cfg = config.services.h2o;
|
||||||
|
inherit (config.security.acme) certs;
|
||||||
|
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
literalExpression
|
literalExpression
|
||||||
|
@ -20,8 +20,62 @@ let
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
|
|
||||||
|
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
|
||||||
|
|
||||||
settingsFormat = pkgs.formats.yaml { };
|
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 (
|
hostsConfig = lib.concatMapAttrs (
|
||||||
name: value:
|
name: value:
|
||||||
let
|
let
|
||||||
|
@ -29,56 +83,88 @@ let
|
||||||
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
|
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
|
||||||
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
|
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
|
||||||
};
|
};
|
||||||
serverName = if value.serverName != null then value.serverName else name;
|
|
||||||
in
|
names = getNames name value;
|
||||||
# HTTP settings
|
|
||||||
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
|
acmeSettings = lib.optionalAttrs (builtins.elem names.cert certNames.dependent) (
|
||||||
"${serverName}:${builtins.toString port.HTTP}" = value.settings // {
|
let
|
||||||
listen.port = port.HTTP;
|
acmePort = 80;
|
||||||
};
|
acmeChallengePath = "/.well-known/acme-challenge";
|
||||||
}
|
in
|
||||||
# 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"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
"${serverName}:${builtins.toString port.TLS}" = value.settings // {
|
"${names.server}:${builtins.toString acmePort}" = {
|
||||||
listen =
|
listen.port = acmePort;
|
||||||
let
|
paths."${acmeChallengePath}/" = {
|
||||||
identity = value.tls.identity;
|
"file.dir" = value.acme.root + acmeChallengePath;
|
||||||
in
|
};
|
||||||
{
|
|
||||||
port = port.TLS;
|
|
||||||
ssl = value.tls.extraSettings or { } // {
|
|
||||||
inherit identity;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
) cfg.hosts;
|
||||||
|
|
||||||
h2oConfig = settingsFormat.generate "h2o.yaml" (
|
h2oConfig = settingsFormat.generate "h2o.yaml" (
|
||||||
lib.recursiveUpdate { hosts = hostsConfig; } cfg.settings
|
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
|
in
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
|
@ -100,7 +186,7 @@ in
|
||||||
package = lib.mkPackageOption pkgs "h2o" {
|
package = lib.mkPackageOption pkgs "h2o" {
|
||||||
example = ''
|
example = ''
|
||||||
pkgs.h2o.override {
|
pkgs.h2o.override {
|
||||||
withMruby = true;
|
withMruby = false;
|
||||||
};
|
};
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -123,21 +209,9 @@ in
|
||||||
example = 8443;
|
example = 8443;
|
||||||
};
|
};
|
||||||
|
|
||||||
mode = mkOption {
|
|
||||||
type =
|
|
||||||
with types;
|
|
||||||
nullOr (enum [
|
|
||||||
"daemon"
|
|
||||||
"master"
|
|
||||||
"worker"
|
|
||||||
"test"
|
|
||||||
]);
|
|
||||||
default = "master";
|
|
||||||
description = "Operating mode of H2O";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = mkOption {
|
settings = mkOption {
|
||||||
type = settingsFormat.type;
|
type = settingsFormat.type;
|
||||||
|
default = { };
|
||||||
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
|
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -189,6 +263,47 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
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 = {
|
||||||
users.${cfg.user} =
|
users.${cfg.user} =
|
||||||
{
|
{
|
||||||
|
@ -201,14 +316,25 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.h2o = {
|
systemd.services.h2o = {
|
||||||
description = "H2O web server service";
|
description = "H2O HTTP server";
|
||||||
wantedBy = [ "multi-user.target" ];
|
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 = {
|
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";
|
ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
RestartSec = "10s";
|
RestartSec = "10s";
|
||||||
RuntimeDirectory = "h2o";
|
RuntimeDirectory = "h2o";
|
||||||
|
@ -242,22 +368,66 @@ in
|
||||||
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
script =
|
preStart = "${h2oExe} --mode 'test'";
|
||||||
let
|
|
||||||
args =
|
|
||||||
[
|
|
||||||
"--conf"
|
|
||||||
"${h2oConfig}"
|
|
||||||
]
|
|
||||||
++ lib.optionals (cfg.mode != null) [
|
|
||||||
"--mode"
|
|
||||||
cfg.mode
|
|
||||||
];
|
|
||||||
in
|
|
||||||
''
|
|
||||||
${lib.getExe cfg.package} ${lib.strings.escapeShellArgs args}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
|
# 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";
|
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 {
|
http = mkOption {
|
||||||
type = types.nullOr (
|
type = types.nullOr (
|
||||||
types.submodule {
|
types.submodule {
|
||||||
|
@ -82,7 +94,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
identity = mkOption {
|
identity = mkOption {
|
||||||
type = types.nonEmptyListOf (
|
type = types.listOf (
|
||||||
types.submodule {
|
types.submodule {
|
||||||
options = {
|
options = {
|
||||||
key-file = mkOption {
|
key-file = mkOption {
|
||||||
|
@ -96,7 +108,7 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
default = null;
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Key / certificate pairs for the virtual host.
|
Key / certificate pairs for the virtual host.
|
||||||
'';
|
'';
|
||||||
|
@ -104,23 +116,21 @@ in
|
||||||
literalExpression
|
literalExpression
|
||||||
# nix
|
# nix
|
||||||
''
|
''
|
||||||
{
|
[
|
||||||
indentities = [
|
{
|
||||||
{
|
key-file = "/path/to/rsa.key";
|
||||||
key-file = "/path/to/rsa.key";
|
certificate-file = "/path/to/rsa.crt";
|
||||||
certificate-file = "/path/to/rsa.crt";
|
}
|
||||||
}
|
{
|
||||||
{
|
key-file = "/path/to/ecdsa.key";
|
||||||
key-file = "/path/to/ecdsa.key";
|
certificate-file = "/path/to/ecdsa.crt";
|
||||||
certificate-file = "/path/to/ecdsa.crt";
|
}
|
||||||
}
|
]
|
||||||
];
|
|
||||||
}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
extraSettings = mkOption {
|
extraSettings = mkOption {
|
||||||
type = types.nullOr types.attrs;
|
type = types.attrs;
|
||||||
default = null;
|
default = { };
|
||||||
description = ''
|
description = ''
|
||||||
Additional TLS/SSL-related configuration options.
|
Additional TLS/SSL-related configuration options.
|
||||||
'';
|
'';
|
||||||
|
@ -140,6 +150,49 @@ in
|
||||||
description = "TLS options for virtual host";
|
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 {
|
settings = mkOption {
|
||||||
type = types.attrs;
|
type = types.attrs;
|
||||||
description = ''
|
description = ''
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ./make-test-python.nix ({ pkgs, ... }:
|
import ./make-test-python.nix (
|
||||||
|
{ pkgs, ... }:
|
||||||
let
|
let
|
||||||
test-certificates = pkgs.runCommandLocal "test-certificates" { } ''
|
test-certificates = pkgs.runCommandLocal "test-certificates" { } ''
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
|
@ -10,83 +11,127 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
name = "step-ca";
|
name = "step-ca";
|
||||||
nodes =
|
nodes = {
|
||||||
{
|
caserver =
|
||||||
caserver =
|
{ config, pkgs, ... }:
|
||||||
{ config, pkgs, ... }: {
|
{
|
||||||
environment.etc.password-file.source = "${test-certificates}/intermediate-password-file";
|
environment.etc.password-file.source = "${test-certificates}/intermediate-password-file";
|
||||||
services.step-ca = {
|
services.step-ca = {
|
||||||
enable = true;
|
enable = true;
|
||||||
address = "[::]";
|
address = "[::]";
|
||||||
port = 8443;
|
port = 8443;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
intermediatePasswordFile = "/etc/${config.environment.etc.password-file.target}";
|
intermediatePasswordFile = "/etc/${config.environment.etc.password-file.target}";
|
||||||
|
settings = {
|
||||||
|
dnsNames = [ "caserver" ];
|
||||||
|
root = "${test-certificates}/root_ca.crt";
|
||||||
|
crt = "${test-certificates}/intermediate_ca.crt";
|
||||||
|
key = "${test-certificates}/intermediate_ca.key";
|
||||||
|
db = {
|
||||||
|
type = "badger";
|
||||||
|
dataSource = "/var/lib/step-ca/db";
|
||||||
|
};
|
||||||
|
authority = {
|
||||||
|
provisioners = [
|
||||||
|
{
|
||||||
|
type = "ACME";
|
||||||
|
name = "acme";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
caclient =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
security.acme.defaults.server = "https://caserver:8443/acme/acme/directory";
|
||||||
|
security.acme.defaults.email = "root@example.org";
|
||||||
|
security.acme.acceptTerms = true;
|
||||||
|
|
||||||
|
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts = {
|
||||||
|
"caclient" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
caclientcaddy =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
|
services.caddy = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."caclientcaddy".extraConfig = ''
|
||||||
|
respond "Welcome to Caddy!"
|
||||||
|
|
||||||
|
tls caddy@example.org {
|
||||||
|
ca https://caserver:8443/acme/acme/directory
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
caclienth2o =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults = {
|
||||||
|
server = "https://caserver:8443/acme/acme/directory";
|
||||||
|
email = "root@example.org";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
|
services.h2o = {
|
||||||
|
enable = true;
|
||||||
|
hosts."caclienth2o" = {
|
||||||
|
tls.policy = "force";
|
||||||
|
acme.enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
dnsNames = [ "caserver" ];
|
paths."/" = {
|
||||||
root = "${test-certificates}/root_ca.crt";
|
"file.file" = "${pkgs.writeTextFile {
|
||||||
crt = "${test-certificates}/intermediate_ca.crt";
|
name = "h2o_welcome.txt";
|
||||||
key = "${test-certificates}/intermediate_ca.key";
|
text = "Welcome to H2O!";
|
||||||
db = {
|
}}";
|
||||||
type = "badger";
|
|
||||||
dataSource = "/var/lib/step-ca/db";
|
|
||||||
};
|
|
||||||
authority = {
|
|
||||||
provisioners = [
|
|
||||||
{
|
|
||||||
type = "ACME";
|
|
||||||
name = "acme";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
caclient =
|
catester =
|
||||||
{ config, pkgs, ... }: {
|
{ config, pkgs, ... }:
|
||||||
security.acme.defaults.server = "https://caserver:8443/acme/acme/directory";
|
{
|
||||||
security.acme.defaults.email = "root@example.org";
|
|
||||||
security.acme.acceptTerms = true;
|
|
||||||
|
|
||||||
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
||||||
|
|
||||||
services.nginx = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts = {
|
|
||||||
"caclient" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
caclientcaddy =
|
|
||||||
{ config, pkgs, ... }: {
|
|
||||||
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
||||||
|
|
||||||
services.caddy = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts."caclientcaddy".extraConfig = ''
|
|
||||||
respond "Welcome to Caddy!"
|
|
||||||
|
|
||||||
tls caddy@example.org {
|
|
||||||
ca https://caserver:8443/acme/acme/directory
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
catester = { config, pkgs, ... }: {
|
|
||||||
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript =
|
testScript = # python
|
||||||
''
|
''
|
||||||
catester.start()
|
catester.start()
|
||||||
caserver.wait_for_unit("step-ca.service")
|
caserver.wait_for_unit("step-ca.service")
|
||||||
|
@ -96,10 +141,13 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||||
catester.succeed("curl https://caclient/ | grep \"Welcome to nginx!\"")
|
catester.succeed("curl https://caclient/ | grep \"Welcome to nginx!\"")
|
||||||
|
|
||||||
caclientcaddy.wait_for_unit("caddy.service")
|
caclientcaddy.wait_for_unit("caddy.service")
|
||||||
|
# It’s hard to know when Caddy has finished the ACME dance with
|
||||||
# It's hard to know when caddy has finished the ACME
|
# step-ca, so we keep trying cURL until success.
|
||||||
# dance with step-ca, so we keep trying to curl
|
|
||||||
# until succeess.
|
|
||||||
catester.wait_until_succeeds("curl https://caclientcaddy/ | grep \"Welcome to Caddy!\"")
|
catester.wait_until_succeeds("curl https://caclientcaddy/ | grep \"Welcome to Caddy!\"")
|
||||||
|
|
||||||
|
caclienth2o.wait_for_unit("acme-finished-caclienth2o.target")
|
||||||
|
caclienth2o.wait_for_unit("h2o.service")
|
||||||
|
catester.succeed("curl https://caclienth2o/ | grep \"Welcome to H2O!\"")
|
||||||
'';
|
'';
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -118,7 +118,6 @@ import ../../make-test-python.nix (
|
||||||
assert "${sawatdi_chao_lok}" in http_hello_world_body
|
assert "${sawatdi_chao_lok}" in http_hello_world_body
|
||||||
|
|
||||||
tls_hello_world_head = server.succeed("curl -v --head --compressed --http2 --tlsv1.3 --fail-with-body 'https://${domain.TLS}:${builtins.toString port.TLS}/hello_world.rst'").lower()
|
tls_hello_world_head = server.succeed("curl -v --head --compressed --http2 --tlsv1.3 --fail-with-body 'https://${domain.TLS}:${builtins.toString port.TLS}/hello_world.rst'").lower()
|
||||||
print(tls_hello_world_head)
|
|
||||||
assert "http/2 200" in tls_hello_world_head
|
assert "http/2 200" in tls_hello_world_head
|
||||||
assert "server: h2o" in tls_hello_world_head
|
assert "server: h2o" in tls_hello_world_head
|
||||||
assert "content-type: text/x-rst" in tls_hello_world_head
|
assert "content-type: text/x-rst" in tls_hello_world_head
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
withMruby ? true,
|
withMruby ? true,
|
||||||
bison,
|
bison,
|
||||||
ruby,
|
ruby,
|
||||||
|
nixosTests,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
|
@ -71,6 +72,10 @@ stdenv.mkDerivation (finalAttrs: {
|
||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
tests = { inherit (nixosTests) h2o; };
|
||||||
|
};
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
description = "Optimized HTTP/1.x, HTTP/2, HTTP/3 server";
|
description = "Optimized HTTP/1.x, HTTP/2, HTTP/3 server";
|
||||||
homepage = "https://h2o.examp1e.net";
|
homepage = "https://h2o.examp1e.net";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue