1
0
Fork 0

use contract for ssl block

This commit is contained in:
ibizaman 2024-01-11 23:22:46 -08:00 committed by Pierre Penninckx
parent 909ebe405a
commit adc09acc49
21 changed files with 790 additions and 144 deletions

View file

@ -26,6 +26,10 @@ Self Host Blocks provides at least one implementation for each block and allows
implementation if you want to, as long as it passes the tests. You can then use blocks to improve implementation if you want to, as long as it passes the tests. You can then use blocks to improve
services you already have deployed. services you already have deployed.
```{=include=} chapters html:into-file=//blocks-ssl.html
modules/blocks/ssl/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-backup.html ```{=include=} chapters html:into-file=//blocks-backup.html
modules/blocks/backup/docs/default.md modules/blocks/backup/docs/default.md
``` ```

View file

@ -68,21 +68,15 @@ let
optionsDocs = buildOptionsDocs { optionsDocs = buildOptionsDocs {
modules = allModules ++ [ scrubbedModule ]; modules = allModules ++ [ scrubbedModule ];
variablelistId = "selfhostblocks-block-backup-options";
includeModuleSystemOptions = false;
};
backupOptionsDocs = buildOptionsDocs {
modules = [ ../modules/blocks/backup.nix scrubbedModule ];
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
includeModuleSystemOptions = false; includeModuleSystemOptions = false;
}; };
nextcloudOptionsDocs = buildOptionsDocs { individualModuleOptionsDocs = path: (buildOptionsDocs {
modules = [ ../modules/services/nextcloud-server.nix scrubbedModule ]; modules = [ path scrubbedModule ];
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
includeModuleSystemOptions = false; includeModuleSystemOptions = false;
}; }).optionsJSON;
nmd = import nmdsrc { nmd = import nmdsrc {
inherit lib; inherit lib;
@ -135,22 +129,27 @@ in stdenv.mkDerivation {
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${optionsDocs.optionsJSON}/share/doc/nixos/options.json ${optionsDocs.optionsJSON}/share/doc/nixos/options.json
substituteInPlace ./modules/blocks/ssl/docs/default.md \
--replace \
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs ../modules/blocks/ssl.nix}/share/doc/nixos/options.json
substituteInPlace ./modules/blocks/backup/docs/default.md \ substituteInPlace ./modules/blocks/backup/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${backupOptionsDocs.optionsJSON}/share/doc/nixos/options.json ${individualModuleOptionsDocs ../modules/blocks/backup.nix}/share/doc/nixos/options.json
substituteInPlace ./modules/services/nextcloud-server/docs/default.md \ substituteInPlace ./modules/services/nextcloud-server/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${nextcloudOptionsDocs.optionsJSON}/share/doc/nixos/options.json ${individualModuleOptionsDocs ../modules/services/nextcloud-server.nix}/share/doc/nixos/options.json
find . -name "*.md" -print0 | \ find . -name "*.md" -print0 | \
while IFS= read -r -d ''' f; do while IFS= read -r -d ''' f; do
substituteInPlace "''${f}" \ substituteInPlace "''${f}" \
--replace \ --replace \
'@REPO@' \ '@REPO@' \
"${ghRoot}" "${ghRoot}" 2>/dev/null
done done
nixos-render-docs manual html \ nixos-render-docs manual html \

View file

@ -58,6 +58,8 @@
release = "0.0.1"; release = "0.0.1";
}; };
lib.contracts = pkgs.callPackage ./modules/contracts {};
checks = checks =
let let
importFiles = files: importFiles = files:
@ -96,6 +98,7 @@
// (vm_test "postgresql" ./test/vm/postgresql.nix) // (vm_test "postgresql" ./test/vm/postgresql.nix)
// (vm_test "monitoring" ./test/vm/monitoring.nix) // (vm_test "monitoring" ./test/vm/monitoring.nix)
// (vm_test "nextcloud" ./test/vm/nextcloud.nix) // (vm_test "nextcloud" ./test/vm/nextcloud.nix)
// (vm_test "ssl" ./test/vm/ssl.nix)
); );
} }
); );

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.authelia; cfg = config.shb.authelia;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
autheliaCfg = config.services.authelia.instances.${fqdn}; autheliaCfg = config.services.authelia.instances.${fqdn};
@ -35,6 +37,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
ldapEndpoint = lib.mkOption { ldapEndpoint = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Endpoint for LDAP authentication backend."; description = "Endpoint for LDAP authentication backend.";
@ -293,9 +301,9 @@ in
lib.mkBefore (lib.concatStringsSep "\n" (map mkCfg cfg.oidcClients)); lib.mkBefore (lib.concatStringsSep "\n" (map mkCfg cfg.oidcClients));
services.nginx.virtualHosts.${fqdn} = { services.nginx.virtualHosts.${fqdn} = {
forceSSL = lib.mkIf config.shb.ssl.enable true; forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem"; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
# Taken from https://github.com/authelia/authelia/issues/178 # Taken from https://github.com/authelia/authelia/issues/178
# TODO: merge with config from https://matwick.ca/authelia-nginx-sso/ # TODO: merge with config from https://matwick.ca/authelia-nginx-sso/
locations."/".extraConfig = '' locations."/".extraConfig = ''

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.ldap; cfg = config.shb.ldap;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
in in
{ {
@ -33,6 +35,12 @@ in
default = 3890; default = 3890;
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
webUIListenPort = lib.mkOption { webUIListenPort = lib.mkOption {
type = lib.types.port; type = lib.types.port;
description = "Port on which the web UI is exposed."; description = "Port on which the web UI is exposed.";
@ -69,9 +77,9 @@ in
enable = true; enable = true;
virtualHosts.${fqdn} = { virtualHosts.${fqdn} = {
forceSSL = lib.mkIf config.shb.ssl.enable true; forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem"; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
locations."/" = { locations."/" = {
extraConfig = '' extraConfig = ''
proxy_set_header Host $host; proxy_set_header Host $host;

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.monitoring; cfg = config.shb.monitoring;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
in in
{ {
@ -21,6 +23,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
grafanaPort = lib.mkOption { grafanaPort = lib.mkOption {
type = lib.types.port; type = lib.types.port;
description = "Port where Grafana listens to HTTP requests."; description = "Port where Grafana listens to HTTP requests.";
@ -362,9 +370,10 @@ in
enable = true; enable = true;
virtualHosts.${fqdn} = { virtualHosts.${fqdn} = {
forceSSL = lib.mkIf config.shb.ssl.enable true; forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem"; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
locations."/" = { locations."/" = {
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}"; proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
proxyWebsockets = true; proxyWebsockets = true;

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.nginx; cfg = config.shb.nginx;
contracts = pkgs.callPackage ../contracts {};
fqdn = c: "${c.subdomain}.${c.domain}"; fqdn = c: "${c.subdomain}.${c.domain}";
autheliaConfig = lib.types.submodule { autheliaConfig = lib.types.submodule {
@ -19,6 +21,12 @@ let
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
authEndpoint = lib.mkOption { authEndpoint = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Auth endpoint for SSO."; description = "Auth endpoint for SSO.";
@ -102,9 +110,9 @@ in
let let
vhostCfg = c: { vhostCfg = c: {
${fqdn c} = { ${fqdn c} = {
forceSSL = lib.mkIf config.shb.ssl.enable true; forceSSL = !(isNull c.ssl);
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${c.domain}/cert.pem"; sslCertificate = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.cert;
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${c.domain}/key.pem"; sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key;
# Taken from https://github.com/authelia/authelia/issues/178 # Taken from https://github.com/authelia/authelia/issues/178
locations."/".extraConfig = '' locations."/".extraConfig = ''

View file

@ -1,26 +1,148 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.shb.ssl; cfg = config.shb.certs;
contracts = pkgs.callPackage ../contracts {};
in in
{ {
options.shb.ssl = { options.shb.certs = {
enable = lib.mkEnableOption "selfhostblocks.ssl"; systemdService = lib.mkOption {
description = ''
Systemd oneshot service used to generate the Certificate Authority bundle.
'';
type = lib.types.str;
default = "shb-ca-bundle.service";
};
cas.selfsigned = lib.mkOption {
description = "Generate a self-signed Certificate Authority.";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: {
options = {
name = lib.mkOption {
type = lib.types.str;
description = ''
Certificate Authority Name. You can put what you want here, it will be displayed by the
browser.
'';
default = "Self Host Blocks Certificate";
};
paths = lib.mkOption {
description = ''
Paths where CA certs will be located.
This option is the contract output of the `shb.certs.cas` SSL block.
'';
type = contracts.ssl.certs-paths;
default = rec {
key = "/var/lib/certs/cas/${config._module.args.name}.key";
cert = "/var/lib/certs/cas/${config._module.args.name}.cert";
};
};
systemdService = lib.mkOption {
description = "Systemd oneshot service used to generate the certs.";
type = lib.types.str;
default = "shb-certs-ca-${config._module.args.name}.service";
};
};
}));
};
certs.selfsigned = lib.mkOption {
description = "Generate self-signed certificates signed by a Certificate Authority.";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
options = {
ca = lib.mkOption {
type = lib.types.nullOr contracts.ssl.cas;
description = ''
CA used to generate this certificate. Only used for self-signed.
This contract input takes the contract output of the `shb.certs.cas` SSL block.
'';
default = null;
};
domain = lib.mkOption { domain = lib.mkOption {
description = "Domain to ask a wildcard certificate for.";
type = lib.types.str; type = lib.types.str;
example = "domain.com"; description = ''
Domain to generate a certificate for. This can be a wildcard domain like
`*.example.com`.
'';
example = "example.com";
};
paths = lib.mkOption {
description = ''
Paths where certs will be located.
This option is the contract output of the `shb.certs.certs` SSL block.
'';
type = contracts.ssl.certs-paths;
default = rec {
key = "/var/lib/certs/selfsigned/${config._module.args.name}.key";
cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert";
};
};
systemdService = lib.mkOption {
description = "Systemd oneshot service used to generate the certs.";
type = lib.types.str;
default = "shb-certs-cert-selfsigned-${config._module.args.name}.service";
};
};
}));
};
certs.letsencrypt = lib.mkOption {
description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
options = {
domain = lib.mkOption {
type = lib.types.str;
description = ''
Domain to generate a certificate for. This can be a wildcard domain like
`*.example.com`.
'';
example = "example.com";
};
paths = lib.mkOption {
description = ''
Paths where certs will be located.
This option is the contract output of the `shb.certs.certs` SSL block.
'';
type = contracts.ssl.certs-paths;
default = {
key = "/var/lib/acme/${config._module.args.name}/key.pem";
cert = "/var/lib/acme/${config._module.args.name}/cert.pem";
};
};
systemdService = lib.mkOption {
description = "Systemd oneshot service used to generate the certs.";
type = lib.types.str;
default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service";
}; };
dnsProvider = lib.mkOption { dnsProvider = lib.mkOption {
description = "DNS provider to use. See https://go-acme.github.io/lego/dns/ for the list of supported providers."; description = "DNS provider to use. See https://go-acme.github.io/lego/dns/ for the list of supported providers.";
type = lib.types.str; type = lib.types.nullOr lib.types.str;
default = null;
example = "linode"; example = "linode";
}; };
dnsResolver = lib.mkOption {
description = "IP of a DNS server used to resolve hostnames.";
type = lib.types.str;
default = "8.8.8.8";
};
credentialsFile = lib.mkOption { credentialsFile = lib.mkOption {
type = lib.types.path; type = lib.types.nullOr lib.types.path;
description = '' description = ''
Credentials file location for the chosen DNS provider. Credentials file location for the chosen DNS provider.
@ -35,16 +157,18 @@ in
You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead. You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead.
''; '';
example = "/run/secrets/ssl"; example = "/run/secrets/ssl";
default = null;
}; };
additionalCfg = lib.mkOption { additionalEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str; type = lib.types.attrsOf lib.types.str;
description = ''Additional environment variables used to configure the DNS provider. description = ''
Additional environment variables used to configure the DNS provider.
For secrets, use shb.ssl.credentialsFile instead. For secrets, use shb.ssl.credentialsFile instead.
See the chose provider's [documentation](https://go-acme.github.io/lego/dns/) for available See the chosen provider's [documentation](https://go-acme.github.io/lego/dns/) for
options. available options.
''; '';
example = lib.literalExpression '' example = lib.literalExpression ''
{ {
@ -54,10 +178,12 @@ in
''; '';
}; };
dnsResolver = lib.mkOption { makeAvailableToUser = lib.mkOption {
description = "IP of a DNS server used to resolve hostnames."; type = lib.types.nullOr lib.types.str;
type = lib.types.str; description = ''
default = "8.8.8.8"; Make all certificates available to given user.
'';
default = null;
}; };
adminEmail = lib.mkOption { adminEmail = lib.mkOption {
@ -71,28 +197,142 @@ in
default = false; default = false;
}; };
}; };
}));
config = lib.mkIf cfg.enable {
users.users.${config.services.nginx.user} = {
isSystemUser = true;
group = "nginx";
extraGroups = [ config.security.acme.defaults.group ];
};
users.groups.nginx = {};
security.acme = {
acceptTerms = true;
certs."${cfg.domain}" = {
extraDomainNames = ["*.${cfg.domain}"];
};
defaults = {
email = cfg.adminEmail;
inherit (cfg) dnsProvider dnsResolver;
credentialsFile = cfg.credentialsFile;
enableDebugLogs = cfg.debug;
}; };
}; };
systemd.services."acme-${cfg.domain}".environment = cfg.additionalCfg; config =
let
filterProvider = provider: lib.attrsets.filterAttrs (k: i: i.provider == provider);
serviceName = lib.strings.removeSuffix ".service";
in
lib.mkMerge [
# Config for self-signed CA.
{
systemd.services = lib.mapAttrs' (_name: caCfg:
lib.nameValuePair (serviceName caCfg.systemdService) {
wantedBy = [ "multi-user.target" ];
wants = [ config.shb.certs.systemdService ];
before = [ config.shb.certs.systemdService ];
serviceConfig.Type = "oneshot";
serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService;
# serviceConfig.User = "nextcloud";
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
script = ''
cd $RUNTIME_DIRECTORY
cat >ca.template <<EOF
organization = "${caCfg.name}"
cn = "${caCfg.name}"
expiration_days = 365
ca
cert_signing_key
crl_signing_key
EOF
mkdir -p "$(dirname -- "${caCfg.paths.key}")"
${pkgs.gnutls}/bin/certtool \
--generate-privkey \
--key-type rsa \
--sec-param High \
--outfile ${caCfg.paths.key}
chmod 666 ${caCfg.paths.key}
mkdir -p "$(dirname -- "${caCfg.paths.cert}")"
${pkgs.gnutls}/bin/certtool \
--generate-self-signed \
--load-privkey ${caCfg.paths.key} \
--template ca.template \
--outfile ${caCfg.paths.cert}
chmod 666 ${caCfg.paths.cert}
'';
}
) cfg.cas.selfsigned;
}
{
systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /etc/ssl/certs
rm -f /etc/ssl/certs/ca-certificates.crt
for file in ${lib.concatStringsSep " " (lib.mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)}; do
cat "$file" >> /etc/ssl/certs/ca-certificates.crt
done
'';
});
}
# Config for self-signed cert.
{
systemd.services = lib.mapAttrs' (_name: certCfg:
lib.nameValuePair (serviceName certCfg.systemdService) {
after = [ certCfg.ca.systemdService ];
requires = [ certCfg.ca.systemdService ];
wantedBy = [ "multi-user.target" ];
serviceConfig.RuntimeDirectory = serviceName certCfg.systemdService;
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
script = ''
cd $RUNTIME_DIRECTORY
# server cert template
cat >server.template <<EOF
organization = "An example company"
cn = "${certCfg.domain}"
expiration_days = 30
dns_name = "${certCfg.domain}"
encryption_key
signing_key
EOF
mkdir -p "$(dirname -- "${certCfg.paths.key}")"
${pkgs.gnutls}/bin/certtool \
--generate-privkey \
--key-type rsa \
--sec-param High \
--outfile ${certCfg.paths.key}
chmod 666 ${certCfg.paths.key}
mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
${pkgs.gnutls}/bin/certtool \
--generate-certificate \
--load-privkey ${certCfg.paths.key} \
--load-ca-privkey ${certCfg.ca.paths.key} \
--load-ca-certificate ${certCfg.ca.paths.cert} \
--template server.template \
--outfile ${certCfg.paths.cert}
chmod 666 ${certCfg.paths.cert}
'';
serviceConfig.Type = "oneshot";
# serviceConfig.User = "nextcloud";
}
) cfg.certs.selfsigned;
}
# Config for Let's Encrypt cert.
{
users.users = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
config.security.acme.defaults.group
];
}) cfg.certs.letsencrypt);
security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true;
security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
"${name}" = {
extraDomainNames = [ certCfg.domain ];
email = certCfg.adminEmail;
inherit (certCfg) dnsProvider dnsResolver;
credentialsFile = certCfg.credentialsFile;
enableDebugLogs = certCfg.debug;
}; };
}) cfg.certs.letsencrypt);
systemd.services = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
"acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
}) cfg.certs.letsencrypt);
}
];
} }

View file

@ -0,0 +1,159 @@
# SSL Block {#ssl-block}
This NixOS module is a block that provides a contract to generate TLS certificates.
It is implemented by:
- [`shb.certs.cas.selfsigned`][10] and [`shb.certs.certs.selfsigned`][11]: Generates self-signed certificates,
including self-signed CA thanks to the [certtool][1] package.
- [`shb.certs.certs.letsencrypt`][12]: Requests certificates from [Let's Encrypt][2].
[1]: https://search.nixos.org/packages?channel=23.11&show=gnutls&from=0&size=50&sort=relevance&type=packages&query=certtool
[2]: https://letsencrypt.org/
[10]: blocks-ssl.html#blocks-ssl-options-shb.certs.cas.selfsigned
[11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned
[12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt
## Contract {#ssl-block-contract}
The contract for this block is defined in [`/modules/contracts/ssl.nix`](@REPO@/modules/contracts/ssl.nix).
Every module implementing this contract provides the following options:
- `paths.cert`: Path to the cert file.
- `paths.key`: Path to the key file.
- `systemdService`: Systemd oneshot service used to generate the certificate.
The Systemd service file name must include the `.service` suffix. Downstream users of the
certificate can use this option to wait for the certificate to be generated.
## Implementations {#ssl-block-impl}
This sections explains how to generate certificates using the SSL block implementations.
### Self-Signed Certificates {#ssl-block-impl-self-signed}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
To use self-signed certificates, we must first generate at least one Certificate Authority (CA):
```nix
shb.certs.cas.selfsigned.myca = {
name = "My CA";
};
```
Every CA defined this way will be concatenated into the file `/etc/ssl/certs/ca-certificates.cert`
which means those CAs and all certificates generated by those CAs will be automatically trusted.
We can then generate one or more certificates signed by that CA:
```nix
shb.certs.certs.selfsigned = {
"example.com" = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
};
"www.example.com" = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "www.example.com";
};
};
```
### Let's Encrypt {#ssl-block-impl-lets-encrypt}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
We can ask Let's Encrypt to generate a certificate with:
```nix
shb.certs.certs.letsencrypt."example.com" = {
domain = "example.com";
dnsProvider = "linode";
adminEmail = "admin@example.com";
credentialsFile = /path/to/secret/file;
additionalEnvironment = {
LINODE_HTTP_TIMEOUT = "10";
LINODE_POLLING_INTERVAL = "10";
LINODE_PROPAGATION_TIMEOUT = "240";
};
};
```
The credential file's content would be a key-value pair:
```yaml
LINODE_TOKEN=XYZ...
```
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
## Usage {#ssl-block-usage}
To use either a self-signed certificates or a Let's Encrypt generated one, we can reference the path
where the certificate and the private key are located:
```nix
config.shb.certs.<implementation>.<name>.paths.cert
config.shb.certs.<implementation>.<name>.paths.key
```
For example:
```nix
config.shb.certs.selfsigned."example.com".paths.cert
config.shb.certs.selfsigned."example.com".paths.key
```
We can then configure Nginx to use those certificates:
```nix
services.nginx.virtualHosts."example.com" =
let
cert = config.shb.certs.selfsigned."example.com";
in
{
onlySSL = true;
sslCertificate = cert.paths.cert;
sslCertificateKey = cert.paths.key;
locations."/".extraConfig = ''
add_header Content-Type text/plain;
return 200 'It works!';
'';
};
```
To make sure the Nginx webserver can find the generated file, we will make it wait for the
certificate to the generated:
```nix
systemd.services.nginx = {
after = [ config.shb.certs.selfsigned."example.com".systemdService ];
requires = [ config.shb.certs.selfsigned."example.com".systemdService ];
};
```
If needed, we can also wait on the CA bundle to be generated by waiting for the Systemd service:
```nix
config.shb.certs.systemdService
```
## Debug {#ssl-block-debug}
Each CA and Cert is generated by a systemd service whose name can be seen in `systemdService`
options below. You can then see the latest errors messages using `journalctl`.
## Tests {#ssl-block-tests}
This block is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
## Options Reference {#ssl-block-options}
```{=include=} options
id-prefix: blocks-ssl-options-
list-id: selfhostblocks-options
source: @OPTIONS_JSON@
```

View file

@ -0,0 +1,4 @@
{ lib }:
{
ssl = import ./ssl.nix { inherit lib; };
}

58
modules/contracts/ssl.nix Normal file
View file

@ -0,0 +1,58 @@
{ lib }:
rec {
certs-paths = lib.types.submodule {
freeformType = lib.types.anything;
options = {
cert = lib.mkOption {
type = lib.types.path;
description = "Path to the cert file.";
};
key = lib.mkOption {
type = lib.types.path;
description = "Path to the key file.";
};
};
};
cas = lib.types.submodule {
freeformType = lib.types.anything;
options = {
paths = lib.mkOption {
description = ''
Paths where the files for the CA will be located.
This option is the contract output of the `shb.certs.cas` SSL block.
'';
type = certs-paths;
};
systemdService = lib.mkOption {
description = "Systemd oneshot service used to generate the CA.";
type = lib.types.str;
};
};
};
certs = lib.types.submodule {
freeformType = lib.types.anything;
options = {
paths = lib.mkOption {
description = ''
Paths where the files for the certificate will be located.
This option is the contract output of the `shb.certs.certs` SSL block.
'';
type = certs-paths;
};
systemdService = lib.mkOption {
description = ''
Systemd oneshot service used to generate the certificate. The name must include the
`.service` suffix.
'';
type = lib.types.str;
};
};
};
}

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.arr; cfg = config.shb.arr;
contracts = pkgs.callPackage ../contracts {};
apps = { apps = {
radarr = { radarr = {
defaultPort = 7001; defaultPort = 7001;
@ -146,6 +148,12 @@ let
example = "example.com"; example = "example.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
description = "Port on which ${name} listens to incoming requests."; description = "Port on which ${name} listens to incoming requests.";
@ -304,7 +312,7 @@ config.xml" templatedSettings) "${config.services.radarr.dataDir}/config.xml" (
c = cfg.${name}; c = cfg.${name};
in in
lib.mkIf (c.authEndpoint != null) { lib.mkIf (c.authEndpoint != null) {
inherit (c) subdomain domain authEndpoint; inherit (c) subdomain domain authEndpoint ssl;
upstream = "http://127.0.0.1:${toString c.port}"; upstream = "http://127.0.0.1:${toString c.port}";
autheliaRules = [ autheliaRules = [
{ {

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.deluge; cfg = config.shb.deluge;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
in in
{ {
@ -21,6 +23,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
daemonPort = lib.mkOption { daemonPort = lib.mkOption {
type = lib.types.int; type = lib.types.int;
description = "Deluge daemon port"; description = "Deluge daemon port";
@ -256,7 +264,7 @@ in
shb.nginx.autheliaProtect = lib.mkIf config.shb.authelia.enable [ shb.nginx.autheliaProtect = lib.mkIf config.shb.authelia.enable [
{ {
inherit (cfg) subdomain domain authEndpoint; inherit (cfg) subdomain domain authEndpoint ssl;
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}"; upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
autheliaRules = [{ autheliaRules = [{
domain = fqdn; domain = fqdn;

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.hledger; cfg = config.shb.hledger;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
in in
{ {
@ -21,6 +23,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.int; type = lib.types.int;
description = "HLedger port"; description = "HLedger port";
@ -74,7 +82,7 @@ in
shb.nginx.autheliaProtect = [ shb.nginx.autheliaProtect = [
{ {
inherit (cfg) subdomain domain authEndpoint; inherit (cfg) subdomain domain authEndpoint ssl;
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}"; upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
autheliaRules = [{ autheliaRules = [{
domain = fqdn; domain = fqdn;

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.home-assistant; cfg = config.shb.home-assistant;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
ldap_auth_script_repo = pkgs.fetchFromGitHub { ldap_auth_script_repo = pkgs.fetchFromGitHub {
@ -33,6 +35,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
ldap = lib.mkOption { ldap = lib.mkOption {
description = '' description = ''
LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html) LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
@ -193,10 +201,12 @@ in
}; };
services.nginx.virtualHosts."${fqdn}" = { services.nginx.virtualHosts."${fqdn}" = {
forceSSL = lib.mkIf config.shb.ssl.enable true;
http2 = true; http2 = true;
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
extraConfig = '' extraConfig = ''
proxy_buffering off; proxy_buffering off;
''; '';

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.jellyfin; cfg = config.shb.jellyfin;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
template = file: newPath: replacements: template = file: newPath: replacements:
@ -33,6 +35,12 @@ in
example = "domain.com"; example = "domain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
ldapHost = lib.mkOption { ldapHost = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "host serving the LDAP server"; description = "host serving the LDAP server";
@ -108,10 +116,12 @@ in
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex # Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
services.nginx.virtualHosts."${fqdn}" = { services.nginx.virtualHosts."${fqdn}" = {
forceSSL = true; forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
http2 = true; http2 = true;
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
extraConfig = '' extraConfig = ''
# The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. # The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
client_max_body_size 20M; client_max_body_size 20M;

View file

@ -5,6 +5,8 @@ let
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
contracts = pkgs.callPackage ../contracts {};
# Make sure to bump both nextcloudPkg and nextcloudApps at the same time. # Make sure to bump both nextcloudPkg and nextcloudApps at the same time.
nextcloudPkg = pkgs.nextcloud27; nextcloudPkg = pkgs.nextcloud27;
nextcloudApps = pkgs.nextcloud27Packages.apps; nextcloudApps = pkgs.nextcloud27Packages.apps;
@ -27,6 +29,12 @@ in
example = "domain.com"; example = "domain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
externalFqdn = lib.mkOption { externalFqdn = lib.mkOption {
description = "External fqdn used to access Nextcloud. Defaults to <subdomain>.<domain>. This should only be set if you include the port when accessing Nextcloud."; description = "External fqdn used to access Nextcloud. Defaults to <subdomain>.<domain>. This should only be set if you include the port when accessing Nextcloud.";
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
@ -146,6 +154,12 @@ in
default = "oo"; default = "oo";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
localNetworkIPRange = lib.mkOption { localNetworkIPRange = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Local network range, to restrict access to Open Office to only those IPs."; description = "Local network range, to restrict access to Open Office to only those IPs.";
@ -363,14 +377,14 @@ in
webfinger = true; webfinger = true;
# Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security # Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security
https = config.shb.ssl.enable; https = !(isNull cfg.ssl);
extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps; extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps;
extraAppsEnable = true; extraAppsEnable = true;
appstoreEnable = true; appstoreEnable = true;
extraOptions = let extraOptions = let
protocol = if config.shb.ssl.enable then "https" else "http"; protocol = if !(isNull cfg.ssl) then "https" else "http";
in { in {
"overwrite.cli.url" = "${protocol}://${fqdn}"; "overwrite.cli.url" = "${protocol}://${fqdn}";
"overwritehost" = if (isNull cfg.externalFqdn) then fqdn else cfg.externalFqdn; "overwritehost" = if (isNull cfg.externalFqdn) then fqdn else cfg.externalFqdn;
@ -382,6 +396,9 @@ in
"overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work. "overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work.
"debug" = cfg.debug; "debug" = cfg.debug;
"filelocking.debug" = cfg.debug; "filelocking.debug" = cfg.debug;
# Use persistent SQL connections.
"dbpersistent" = "true";
}; };
phpOptions = { phpOptions = {
@ -396,7 +413,7 @@ in
"opcache.max_accelerated_files" = "10000"; "opcache.max_accelerated_files" = "10000";
"opcache.memory_consumption" = "128"; "opcache.memory_consumption" = "128";
"opcache.revalidate_freq" = "1"; "opcache.revalidate_freq" = "1";
"openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt"; "openssl.cafile" = "/etc/ssl/certs/ca-certificates.cert";
short_open_tag = "Off"; short_open_tag = "Off";
output_buffering = "Off"; output_buffering = "Off";
@ -424,9 +441,9 @@ in
services.nginx.virtualHosts.${fqdn} = { services.nginx.virtualHosts.${fqdn} = {
# listen = [ { addr = "0.0.0.0"; port = 443; } ]; # listen = [ { addr = "0.0.0.0"; port = 443; } ];
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem"; forceSSL = !(isNull cfg.ssl);
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
forceSSL = lib.mkIf config.shb.ssl.enable true; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
# From [1] this should fix downloading of big files. [2] seems to indicate that buffering # From [1] this should fix downloading of big files. [2] seems to indicate that buffering
# happens at multiple places anyway, so disabling one place should be okay. # happens at multiple places anyway, so disabling one place should be okay.
@ -491,9 +508,10 @@ in
}; };
services.nginx.virtualHosts."${cfg.apps.onlyoffice.subdomain}.${cfg.domain}" = { services.nginx.virtualHosts."${cfg.apps.onlyoffice.subdomain}.${cfg.domain}" = {
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem"; forceSSL = !(isNull cfg.ssl);
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem"; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
forceSSL = lib.mkIf config.shb.ssl.enable true; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
locations."/" = { locations."/" = {
extraConfig = '' extraConfig = ''
allow ${cfg.apps.onlyoffice.localNetworkIPRange}; allow ${cfg.apps.onlyoffice.localNetworkIPRange};

View file

@ -3,6 +3,8 @@
let let
cfg = config.shb.vaultwarden; cfg = config.shb.vaultwarden;
contracts = pkgs.callPackage ../contracts {};
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
template = file: newPath: replacements: template = file: newPath: replacements:
@ -33,6 +35,12 @@ in
example = "mydomain.com"; example = "mydomain.com";
}; };
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
description = "Port on which vaultwarden service listens."; description = "Port on which vaultwarden service listens.";
@ -164,7 +172,7 @@ in
shb.nginx.autheliaProtect = [ shb.nginx.autheliaProtect = [
{ {
inherit (cfg) subdomain domain authEndpoint; inherit (cfg) subdomain domain authEndpoint ssl;
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}"; upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
autheliaRules = [ autheliaRules = [
{ {

View file

@ -89,6 +89,7 @@ in
authEndpoint = "https://oidc.example.com"; authEndpoint = "https://oidc.example.com";
subdomain = "radarr"; subdomain = "radarr";
upstream = "http://127.0.0.1:7001"; upstream = "http://127.0.0.1:7001";
ssl = null;
} }
]; ];
users.users.radarr.extraGroups = [ "media" ]; users.users.radarr.extraGroups = [ "media" ];
@ -162,6 +163,7 @@ in
authEndpoint = "https://oidc.example.com"; authEndpoint = "https://oidc.example.com";
subdomain = "radarr"; subdomain = "radarr";
upstream = "http://127.0.0.1:7001"; upstream = "http://127.0.0.1:7001";
ssl = null;
} }
]; ];
users.users.radarr.extraGroups = [ "media" ]; users.users.radarr.extraGroups = [ "media" ];

View file

@ -18,9 +18,11 @@ let
services = anyOpt {}; services = anyOpt {};
shb.authelia = anyOpt {}; shb.authelia = anyOpt {};
shb.backup = anyOpt {}; shb.backup = anyOpt {};
shb.ssl = anyOpt {}; systemd = anyOpt {};
users = anyOpt {};
}; };
} }
../../modules/blocks/ssl.nix
../../modules/blocks/nginx.nix ../../modules/blocks/nginx.nix
m m
]; ];
@ -46,36 +48,32 @@ in
testAuth = { testAuth = {
expected = { expected = {
shb.backup = {}; nginx.enable = true;
shb.nginx = { nginx.virtualHosts."my.example.com" = {
accessLog = false;
autheliaProtect = [{
authEndpoint = "hello";
autheliaRules = [{}];
subdomain = "my";
domain = "example.com";
upstream = "http://127.0.0.1:1234";
}];
debugLog = false;
};
services.nginx.enable = true;
services.nginx.virtualHosts."my.example.com" = {
forceSSL = true; forceSSL = true;
locations."/" = {}; locations."/" = {};
locations."/authelia" = {}; locations."/authelia" = {};
sslCertificate = "/var/lib/acme/example.com/cert.pem"; sslCertificate = "/var/lib/certs/selfsigned/example.com.cert";
sslCertificateKey = "/var/lib/acme/example.com/key.pem"; sslCertificateKey = "/var/lib/certs/selfsigned/example.com.key";
}; };
}; };
expr = testConfig { expr = (testConfig ({ config, ... }: {
shb.ssl.enable = true; shb.certs.cas.selfsigned.myca = {};
shb.certs.certs.selfsigned."example.com" = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
};
shb.nginx.autheliaProtect = [{ shb.nginx.autheliaProtect = [{
subdomain = "my"; subdomain = "my";
domain = "example.com"; domain = "example.com";
ssl = config.shb.certs.certs.selfsigned."example.com";
upstream = "http://127.0.0.1:1234"; upstream = "http://127.0.0.1:1234";
authEndpoint = "hello"; authEndpoint = "hello";
autheliaRules = [{}]; autheliaRules = [{}];
}]; }];
}; })).services;
}; };
} }

76
test/vm/ssl.nix Normal file
View file

@ -0,0 +1,76 @@
{ pkgs, lib, ... }:
{
test = pkgs.nixosTest {
name = "ssl-test";
nodes.server = { config, pkgs, ... }: {
imports = [
../../modules/blocks/ssl.nix
];
shb.certs = {
cas.selfsigned = {
myca = {
name = "My CA";
};
myotherca = {
name = "My Other CA";
};
};
certs.selfsigned = {
mycert = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
};
};
};
# The configuration below is to create a webserver that uses the server certificate.
networking.hosts."127.0.0.1" = [ "example.com" ];
services.nginx.enable = true;
services.nginx.virtualHosts."example.com" =
{
onlySSL = true;
sslCertificate = config.shb.certs.certs.selfsigned.mycert.paths.cert;
sslCertificateKey = config.shb.certs.certs.selfsigned.mycert.paths.key;
locations."/".extraConfig = ''
add_header Content-Type text/plain;
return 200 'It works!';
'';
};
systemd.services.nginx = {
after = [ config.shb.certs.certs.selfsigned.mycert.systemdService ];
requires = [ config.shb.certs.certs.selfsigned.mycert.systemdService ];
};
};
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
testScript = { nodes, ... }:
let
myca = nodes.server.shb.certs.cas.selfsigned.myca;
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;
mycert = nodes.server.shb.certs.certs.selfsigned.mycert;
in
''
start_all()
# Make sure certs are generated.
server.wait_for_file("${myca.paths.key}")
server.wait_for_file("${myca.paths.cert}")
server.wait_for_file("${myotherca.paths.key}")
server.wait_for_file("${myotherca.paths.cert}")
server.wait_for_file("${mycert.paths.key}")
server.wait_for_file("${mycert.paths.cert}")
# Wait for jkkk
server.require_unit_state("${nodes.server.shb.certs.systemdService}", "inactive")
with subtest("Certificate is trusted in curl"):
machine.wait_for_unit("nginx")
machine.wait_for_open_port(443)
machine.succeed("curl --fail-with-body -v https://example.com")
'';
};
}