From 9bfd954c1f884b6f2b42a3c7fa941d6dc26d0555 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Sun, 15 Mar 2026 15:10:39 +0100 Subject: [PATCH] ssl: make self-signed certs idempotent --- modules/blocks/ssl.nix | 74 ++++++++++++++++++++++++------------------ test/blocks/ssl.nix | 67 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/modules/blocks/ssl.nix b/modules/blocks/ssl.nix index 859d5ee..39b2aaa 100644 --- a/modules/blocks/ssl.nix +++ b/modules/blocks/ssl.nix @@ -399,21 +399,25 @@ in 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} + keyfile="${caCfg.paths.key}" + keydir="$(dirname -- "$keyfile")" + mkdir -p "$keydir" + [ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \ + --generate-privkey \ + --key-type rsa \ + --sec-param High \ + --outfile "$keyfile" + chmod 666 "$keyfile" - 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} + certfile="${caCfg.paths.cert}" + certdir="$(dirname -- "$certfile")" + mkdir -p "$certdir" + [ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \ + --generate-self-signed \ + --load-privkey "$keyfile" \ + --template ca.template \ + --outfile "$certfile" + chmod 666 "$certfile" ''; } ) cfg.cas.selfsigned; @@ -427,6 +431,8 @@ in script = '' mkdir -p /etc/ssl/certs + # This file is automatically idempotent since the source files used are idempotent. + rm -f /etc/ssl/certs/ca-bundle.crt rm -f /etc/ssl/certs/ca-certificates.crt @@ -456,8 +462,8 @@ in let extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); chmod = cert: '' - chown root:${certCfg.group} ${cert} - chmod 640 ${cert} + chown root:${certCfg.group} "${cert}" + chmod 640 "${cert}" ''; in '' @@ -474,23 +480,27 @@ in 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 certCfg.paths.key} + keyfile="${certCfg.paths.key}" + keydir="$(dirname -- "$keyfile")" + mkdir -p "$keydir" + [ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \ + --generate-privkey \ + --key-type rsa \ + --sec-param High \ + --outfile "$keyfile" + ${chmod "$keyfile"} - 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 certCfg.paths.cert} + certfile="${certCfg.paths.cert}" + certdir="$(dirname -- "$certfile")" + mkdir -p "$certdir" + [ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \ + --generate-certificate \ + --load-privkey "$keyfile" \ + --load-ca-privkey "${certCfg.ca.paths.key}" \ + --load-ca-certificate "${certCfg.ca.paths.cert}" \ + --template server.template \ + --outfile "$certfile" + ${chmod "$certfile"} ''; postStart = lib.optionalString (certCfg.reloadServices != [ ]) '' diff --git a/test/blocks/ssl.nix b/test/blocks/ssl.nix index 6eba96e..46780c2 100644 --- a/test/blocks/ssl.nix +++ b/test/blocks/ssl.nix @@ -172,6 +172,32 @@ in if perm != want_perm: raise Exception('Unexpected perm for {}: wanted "{}", got: "{}"'.format(path, want_perm, perm)) + with subtest("Certificates content seem correct"): + myca_key = server.succeed("cat {}".format("${myca.paths.key}")).strip(); + myca_cert = server.succeed("cat {}".format("${myca.paths.cert}")).strip(); + cert1_key = server.succeed("cat {}".format("${cert1.paths.key}")).strip(); + cert1_cert = server.succeed("cat {}".format("${cert1.paths.cert}")).strip(); + cert2_key = server.succeed("cat {}".format("${cert2.paths.key}")).strip(); + cert2_cert = server.succeed("cat {}".format("${cert2.paths.cert}")).strip(); + ca_bundle = server.succeed("cat /etc/ssl/certs/ca-bundle.crt").strip(); + + if myca_cert == "": + raise Exception("CA cert was empty") + if cert1_key == "": + raise Exception("Cert1 key was empty") + if cert1_cert == "": + raise Exception("Cert1 cert was empty") + if cert2_key == "": + raise Exception("Cert2 key was empty") + if cert2_cert == "": + raise Exception("Cert2 cert was empty") + if cert1_key == cert2_key: + raise Exception("Cert1 key and cert2 key are the same") + if cert1_cert == cert2_cert: + raise Exception("Cert1 cert and cert2 cert are the same") + if ca_bundle == "": + raise Exception("CA bundle was empty") + with subtest("Certificate is trusted in curl"): resp = server.succeed("curl --fail-with-body -v https://example.com") if resp != "Top domain": @@ -204,11 +230,52 @@ in assert_perm("${cert2.paths.key}", "640") assert_perm("${cert2.paths.cert}", "640") + with subtest("Certificates content seem correct"): + if cert1_key == "": + raise Exception("Cert1 key was empty") + if cert1_cert == "": + raise Exception("Cert1 cert was empty") + if cert2_key == "": + raise Exception("Cert2 key was empty") + if cert2_cert == "": + raise Exception("Cert2 cert was empty") + if cert1_key == cert2_key: + raise Exception("Cert1 key and cert2 key are the same") + if cert1_cert == cert2_cert: + raise Exception("Cert1 cert and cert2 cert are the same") + 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") server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://example.com") server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://subdomain.example.com") + + with subtest("Idempotency"): + server.succeed("systemctl restart shb-certs-ca-myca") + server.succeed("systemctl restart shb-certs-cert-selfsigned-cert1") + server.succeed("systemctl restart shb-certs-cert-selfsigned-cert2") + + new_myca_key = server.succeed("cat {}".format("${myca.paths.key}")).strip(); + new_myca_cert = server.succeed("cat {}".format("${myca.paths.cert}")).strip(); + new_cert1_key = server.succeed("cat {}".format("${cert1.paths.key}")).strip(); + new_cert1_cert = server.succeed("cat {}".format("${cert1.paths.cert}")).strip(); + new_cert2_key = server.succeed("cat {}".format("${cert2.paths.key}")).strip(); + new_cert2_cert = server.succeed("cat {}".format("${cert2.paths.cert}")).strip(); + new_ca_bundle = server.succeed("cat /etc/ssl/certs/ca-bundle.crt").strip(); + if new_myca_key != myca_key: + raise Exception("New CA key is different from old one.") + if new_myca_cert != myca_cert: + raise Exception("New CA cert is different from old one.") + if new_cert1_key != cert1_key: + raise Exception("New Cert1 key is different from old one.") + if new_cert1_cert != cert1_cert: + raise Exception("New Cert1 cert is different from old one.") + if new_cert2_key != cert2_key: + raise Exception("New Cert2 key is different from old one.") + if new_cert2_cert != cert2_cert: + raise Exception("New Cert2 cert is different from old one.") + if new_ca_bundle != ca_bundle: + raise Exception("New CA bundle is different from old one.") ''; }; }