1
0
Fork 0

add group and reloadServices options to ssl block

This commit is contained in:
ibizaman 2024-01-24 22:41:18 -08:00 committed by Pierre Penninckx
parent 0bfa15fd3c
commit e00a41b086
3 changed files with 119 additions and 3 deletions

View file

@ -87,6 +87,15 @@ in
''; '';
}; };
group = lib.mkOption {
type = lib.types.str;
description = ''
Unix group owning this certificate.
'';
default = "root";
example = "nginx";
};
paths = lib.mkOption { paths = lib.mkOption {
description = '' description = ''
Paths where certs will be located. Paths where certs will be located.
@ -105,6 +114,15 @@ in
type = lib.types.str; type = lib.types.str;
default = "shb-certs-cert-selfsigned-${config._module.args.name}.service"; default = "shb-certs-cert-selfsigned-${config._module.args.name}.service";
}; };
reloadServices = lib.mkOption {
description = ''
The list of systemd services to call `systemctl try-reload-or-restart` on.
'';
type = lib.types.listOf lib.types.str;
default = [];
example = [ "nginx.service" ];
};
}; };
})); }));
}; };
@ -150,12 +168,30 @@ in
}; };
}; };
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Unix group owning this certificate.
'';
default = "acme";
example = "nginx";
};
systemdService = lib.mkOption { systemdService = lib.mkOption {
description = "Systemd oneshot service used to generate the certs."; description = "Systemd oneshot service used to generate the certs.";
type = lib.types.str; type = lib.types.str;
default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service"; default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service";
}; };
reloadServices = lib.mkOption {
description = ''
The list of systemd services to call `systemctl try-reload-or-restart` on.
'';
type = lib.types.listOf lib.types.str;
default = [];
example = [ "nginx.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.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
@ -245,7 +281,6 @@ in
before = [ config.shb.certs.systemdService ]; before = [ config.shb.certs.systemdService ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService; serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService;
# serviceConfig.User = "nextcloud";
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
script = '' script = ''
cd $RUNTIME_DIRECTORY cd $RUNTIME_DIRECTORY
@ -278,6 +313,7 @@ in
} }
) cfg.cas.selfsigned; ) cfg.cas.selfsigned;
} }
# Config for self-signed CA bundle.
{ {
systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) { systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
@ -309,6 +345,11 @@ in
script = script =
let let
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
chmod = cert:
''
chown root:${certCfg.group} ${cert}
chmod 640 ${cert}
'';
in in
'' ''
cd $RUNTIME_DIRECTORY cd $RUNTIME_DIRECTORY
@ -330,7 +371,7 @@ in
--key-type rsa \ --key-type rsa \
--sec-param High \ --sec-param High \
--outfile ${certCfg.paths.key} --outfile ${certCfg.paths.key}
chmod 666 ${certCfg.paths.key} ${chmod certCfg.paths.key}
mkdir -p "$(dirname -- "${certCfg.paths.cert}")" mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
${pkgs.gnutls}/bin/certtool \ ${pkgs.gnutls}/bin/certtool \
@ -340,7 +381,11 @@ in
--load-ca-certificate ${certCfg.ca.paths.cert} \ --load-ca-certificate ${certCfg.ca.paths.cert} \
--template server.template \ --template server.template \
--outfile ${certCfg.paths.cert} --outfile ${certCfg.paths.cert}
chmod 666 ${certCfg.paths.cert} ${chmod certCfg.paths.cert}
'';
postStart = lib.optionalString (certCfg.reloadServices != []) ''
systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs certCfg.reloadServices}
''; '';
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
@ -363,6 +408,7 @@ in
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains; extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
email = certCfg.adminEmail; email = certCfg.adminEmail;
inherit (certCfg) dnsProvider dnsResolver; inherit (certCfg) dnsProvider dnsResolver;
inherit (certCfg) group reloadServices;
credentialsFile = certCfg.credentialsFile; credentialsFile = certCfg.credentialsFile;
enableDebugLogs = certCfg.debug; enableDebugLogs = certCfg.debug;
}; };

View file

@ -20,6 +20,10 @@ The contract for this block is defined in [`/modules/contracts/ssl.nix`](@REPO@/
Every module implementing this contract provides the following options: Every module implementing this contract provides the following options:
- `domain`: Domain to generate the certificate for.
- `extraDomains`: Other domains the certificate should be generated for.
- `group`: The unix group owning this certificate.
- `reloadServices`: Systemd services to reload when the certificate gets renewed.
- `paths.cert`: Path to the cert file. - `paths.cert`: Path to the cert file.
- `paths.key`: Path to the key file. - `paths.key`: Path to the key file.
- `systemdService`: Systemd oneshot service used to generate the certificate. - `systemdService`: Systemd oneshot service used to generate the certificate.
@ -53,15 +57,21 @@ shb.certs.certs.selfsigned = {
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com"; domain = "example.com";
group = "nginx";
reloadServices = [ "nginx.service" ];
}; };
"www.example.com" = { "www.example.com" = {
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "www.example.com"; domain = "www.example.com";
group = "nginx";
}; };
}; };
``` ```
The group has been chosen to be `nginx` to be consistent with the examples further down in this
document.
### Let's Encrypt {#ssl-block-impl-lets-encrypt} ### Let's Encrypt {#ssl-block-impl-lets-encrypt}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix). Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
@ -71,6 +81,7 @@ We can ask Let's Encrypt to generate a certificate with:
```nix ```nix
shb.certs.certs.letsencrypt."example.com" = { shb.certs.certs.letsencrypt."example.com" = {
domain = "example.com"; domain = "example.com";
group = "nginx";
dnsProvider = "linode"; dnsProvider = "linode";
adminEmail = "admin@example.com"; adminEmail = "admin@example.com";
credentialsFile = /path/to/secret/file; credentialsFile = /path/to/secret/file;

View file

@ -8,6 +8,21 @@
../../modules/blocks/ssl.nix ../../modules/blocks/ssl.nix
]; ];
users.users = {
user1 = {
group = "group1";
isSystemUser = true;
};
user2 = {
group = "group2";
isSystemUser = true;
};
};
users.groups = {
group1 = {};
group2 = {};
};
shb.certs = { shb.certs = {
cas.selfsigned = { cas.selfsigned = {
myca = { myca = {
@ -22,17 +37,32 @@
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com"; domain = "example.com";
group = "nginx";
}; };
subdomain = { subdomain = {
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "subdomain.example.com"; domain = "subdomain.example.com";
group = "nginx";
}; };
multi = { multi = {
ca = config.shb.certs.cas.selfsigned.myca; ca = config.shb.certs.cas.selfsigned.myca;
domain = "multi1.example.com"; domain = "multi1.example.com";
extraDomains = [ "multi2.example.com" "multi3.example.com" ]; extraDomains = [ "multi2.example.com" "multi3.example.com" ];
group = "nginx";
};
cert1 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert1.example.com";
};
cert2 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert2.example.com";
group = "group2";
}; };
}; };
}; };
@ -81,6 +111,9 @@
top = nodes.server.shb.certs.certs.selfsigned.top; top = nodes.server.shb.certs.certs.selfsigned.top;
subdomain = nodes.server.shb.certs.certs.selfsigned.subdomain; subdomain = nodes.server.shb.certs.certs.selfsigned.subdomain;
multi = nodes.server.shb.certs.certs.selfsigned.multi; multi = nodes.server.shb.certs.certs.selfsigned.multi;
cert1 = nodes.server.shb.certs.certs.selfsigned.cert1;
cert2 = nodes.server.shb.certs.certs.selfsigned.cert2;
cert3 = nodes.server.shb.certs.certs.selfsigned.cert3;
in in
'' ''
start_all() start_all()
@ -96,12 +129,27 @@
server.wait_for_file("${subdomain.paths.cert}") server.wait_for_file("${subdomain.paths.cert}")
server.wait_for_file("${multi.paths.key}") server.wait_for_file("${multi.paths.key}")
server.wait_for_file("${multi.paths.cert}") server.wait_for_file("${multi.paths.cert}")
server.wait_for_file("${cert1.paths.key}")
server.wait_for_file("${cert1.paths.cert}")
server.wait_for_file("${cert2.paths.key}")
server.wait_for_file("${cert2.paths.cert}")
server.require_unit_state("${nodes.server.shb.certs.systemdService}", "inactive") server.require_unit_state("${nodes.server.shb.certs.systemdService}", "inactive")
server.wait_for_unit("nginx") server.wait_for_unit("nginx")
server.wait_for_open_port(443) server.wait_for_open_port(443)
def assert_owner(path, user, group):
owner = server.succeed("stat --format '%U:%G' {}".format(path)).strip();
want_owner = user + ":" + group
if owner != want_owner:
raise Exception('Unexpected owner for {}: wanted "{}", got: "{}"'.format(path, want_owner, owner))
def assert_perm(path, want_perm):
perm = server.succeed("stat --format '%a' {}".format(path)).strip();
if perm != want_perm:
raise Exception('Unexpected perm for {}: wanted "{}", got: "{}"'.format(path, want_perm, perm))
with subtest("Certificate is trusted in curl"): with subtest("Certificate is trusted in curl"):
resp = server.succeed("curl --fail-with-body -v https://example.com") resp = server.succeed("curl --fail-with-body -v https://example.com")
if resp != "Top domain": if resp != "Top domain":
@ -123,6 +171,17 @@
if resp != "multi3": if resp != "multi3":
raise Exception('Unexpected response, got: {}'.format(resp)) raise Exception('Unexpected response, got: {}'.format(resp))
with subtest("Certificate has correct permission"):
assert_owner("${cert1.paths.key}", "root", "root")
assert_owner("${cert1.paths.cert}", "root", "root")
assert_perm("${cert1.paths.key}", "640")
assert_perm("${cert1.paths.cert}", "640")
assert_owner("${cert2.paths.key}", "root", "group2")
assert_owner("${cert2.paths.cert}", "root", "group2")
assert_perm("${cert2.paths.key}", "640")
assert_perm("${cert2.paths.cert}", "640")
with subtest("Fail if certificate is not in CA bundle"): with subtest("Fail if certificate is not in CA bundle"):
server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://example.com") server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://example.com")
server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://subdomain.example.com") server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://subdomain.example.com")