add group and reloadServices options to ssl block
This commit is contained in:
parent
0bfa15fd3c
commit
e00a41b086
3 changed files with 119 additions and 3 deletions
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue