Skip to content

Commit

Permalink
add group and reloadServices options to ssl block
Browse files Browse the repository at this point in the history
  • Loading branch information
ibizaman authored and ibizaman committed Jan 25, 2024
1 parent 0bfa15f commit e00a41b
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 3 deletions.
52 changes: 49 additions & 3 deletions modules/blocks/ssl.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
description = ''
Paths where certs will be located.
Expand All @@ -105,6 +114,15 @@ in
type = lib.types.str;
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" ];
};
};
}));
};
Expand Down Expand Up @@ -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 {
description = "Systemd oneshot service used to generate the certs.";
type = lib.types.str;
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 {
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;
Expand Down Expand Up @@ -245,7 +281,6 @@ in
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
Expand Down Expand Up @@ -278,6 +313,7 @@ in
}
) cfg.cas.selfsigned;
}
# Config for self-signed CA bundle.
{
systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
wantedBy = [ "multi-user.target" ];
Expand Down Expand Up @@ -309,6 +345,11 @@ in
script =
let
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
chmod = cert:
''
chown root:${certCfg.group} ${cert}
chmod 640 ${cert}
'';
in
''
cd $RUNTIME_DIRECTORY
Expand All @@ -330,7 +371,7 @@ in
--key-type rsa \
--sec-param High \
--outfile ${certCfg.paths.key}
chmod 666 ${certCfg.paths.key}
${chmod certCfg.paths.key}
mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
${pkgs.gnutls}/bin/certtool \
Expand All @@ -340,7 +381,11 @@ in
--load-ca-certificate ${certCfg.ca.paths.cert} \
--template server.template \
--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";
Expand All @@ -363,6 +408,7 @@ in
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
email = certCfg.adminEmail;
inherit (certCfg) dnsProvider dnsResolver;
inherit (certCfg) group reloadServices;
credentialsFile = certCfg.credentialsFile;
enableDebugLogs = certCfg.debug;
};
Expand Down
11 changes: 11 additions & 0 deletions modules/blocks/ssl/docs/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

- `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.key`: Path to the key file.
- `systemdService`: Systemd oneshot service used to generate the certificate.
Expand Down Expand Up @@ -53,15 +57,21 @@ shb.certs.certs.selfsigned = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
group = "nginx";
reloadServices = [ "nginx.service" ];
};
"www.example.com" = {
ca = config.shb.certs.cas.selfsigned.myca;
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}

Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
Expand All @@ -71,6 +81,7 @@ We can ask Let's Encrypt to generate a certificate with:
```nix
shb.certs.certs.letsencrypt."example.com" = {
domain = "example.com";
group = "nginx";
dnsProvider = "linode";
adminEmail = "[email protected]";
credentialsFile = /path/to/secret/file;
Expand Down
59 changes: 59 additions & 0 deletions test/vm/ssl.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
../../modules/blocks/ssl.nix
];

users.users = {
user1 = {
group = "group1";
isSystemUser = true;
};
user2 = {
group = "group2";
isSystemUser = true;
};
};
users.groups = {
group1 = {};
group2 = {};
};

shb.certs = {
cas.selfsigned = {
myca = {
Expand All @@ -22,17 +37,32 @@
ca = config.shb.certs.cas.selfsigned.myca;

domain = "example.com";
group = "nginx";
};
subdomain = {
ca = config.shb.certs.cas.selfsigned.myca;

domain = "subdomain.example.com";
group = "nginx";
};
multi = {
ca = config.shb.certs.cas.selfsigned.myca;

domain = "multi1.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";
};
};
};
Expand Down Expand Up @@ -81,6 +111,9 @@
top = nodes.server.shb.certs.certs.selfsigned.top;
subdomain = nodes.server.shb.certs.certs.selfsigned.subdomain;
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
''
start_all()
Expand All @@ -96,12 +129,27 @@
server.wait_for_file("${subdomain.paths.cert}")
server.wait_for_file("${multi.paths.key}")
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.wait_for_unit("nginx")
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"):
resp = server.succeed("curl --fail-with-body -v https://example.com")
if resp != "Top domain":
Expand All @@ -123,6 +171,17 @@
if resp != "multi3":
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"):
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")
Expand Down

0 comments on commit e00a41b

Please sign in to comment.