diff --git a/clanServices/certificates/README.md b/clanServices/certificates/README.md new file mode 100644 index 000000000..16fb4b048 --- /dev/null +++ b/clanServices/certificates/README.md @@ -0,0 +1,32 @@ +This service sets up a certificate authority (CA) that can issue certificates to +other machines in your clan. For this the `ca` role is used. +It additionally provides a `default` role, that can be applied to all machines +in your clan and will make sure they trust your CA. + +## Example Usage + +The following configuration would add a CA for the top level domain `.foo`. If +the machine `server` now hosts a webservice at `https://something.foo`, it will +get a certificate from `ca` which is valid inside your clan. The machine +`client` will trust this certificate if it makes a request to +`https://something.foo`. + +This clan service can be combined with the `coredns` service for easy to deploy, +SSL secured clan-internal service hosting. + +```nix +inventory = { + machines.ca = { }; + machines.client = { }; + machines.server = { }; + + instances."certificates" = { + module.name = "certificates"; + module.input = "self"; + + roles.ca.machines.ca.settings.tlds = [ "foo" ]; + roles.default.machines.client = { }; + roles.default.machines.server = { }; + }; +}; +``` diff --git a/clanServices/certificates/default.nix b/clanServices/certificates/default.nix new file mode 100644 index 000000000..0d9f5344b --- /dev/null +++ b/clanServices/certificates/default.nix @@ -0,0 +1,245 @@ +{ ... }: +{ + _class = "clan.service"; + manifest.name = "certificates"; + manifest.description = "Sets up a certificates internal to your Clan"; + manifest.categories = [ "Network" ]; + manifest.readme = builtins.readFile ./README.md; + + roles.ca = { + + interface = + { lib, ... }: + { + + options.acmeEmail = lib.mkOption { + type = lib.types.str; + default = "none@none.tld"; + description = '' + Email address for account creation and correspondence from the CA. + It is recommended to use the same email for all certs to avoid account + creation limits. + ''; + }; + + options.tlds = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "Top level domain for this CA. Certificates will be issued and trusted for *."; + }; + + options.expire = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "When the certificate should expire."; + default = "8760h"; + example = "8760h"; + }; + }; + + perInstance = + { settings, ... }: + { + nixosModule = + { + config, + pkgs, + lib, + ... + }: + let + domains = map (tld: "ca.${tld}") settings.tlds; + in + { + security.acme.defaults.email = settings.acmeEmail; + security.acme = { + certs = builtins.listToAttrs ( + map (domain: { + name = domain; + value = { + server = "https://${domain}:1443/acme/acme/directory"; + }; + }) domains + ); + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.nginx = { + enable = true; + recommendedProxySettings = true; + virtualHosts = builtins.listToAttrs ( + map (domain: { + name = domain; + value = { + addSSL = true; + enableACME = true; + locations."/".proxyPass = "https://localhost:1443"; + locations."= /ca.crt".alias = + config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path; + }; + }) domains + ); + }; + + clan.core.vars.generators = { + + # Intermediate key generator + "step-intermediate-key" = { + files."intermediate.key" = { + secret = true; + deploy = true; + owner = "step-ca"; + group = "step-ca"; + }; + runtimeInputs = [ pkgs.step-cli ]; + script = '' + step crypto keypair --kty EC --curve P-256 --no-password --insecure $out/intermediate.pub $out/intermediate.key + ''; + }; + + # Intermediate certificate generator + "step-intermediate-cert" = { + files."intermediate.crt".secret = false; + dependencies = [ + "step-ca" + "step-intermediate-key" + ]; + runtimeInputs = [ pkgs.step-cli ]; + script = '' + # Create intermediate certificate + step certificate create \ + --ca $in/step-ca/ca.crt \ + --ca-key $in/step-ca/ca.key \ + --ca-password-file /dev/null \ + --key $in/step-intermediate-key/intermediate.key \ + --template ${pkgs.writeText "intermediate.tmpl" '' + { + "subject": {{ toJson .Subject }}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + }, + "nameConstraints": { + "critical": true, + "permittedDNSDomains": [${ + (lib.strings.concatStringsSep "," (map (tld: ''"${tld}"'') settings.tlds)) + }] + } + } + ''} ${lib.optionalString (settings.expire != null) "--not-after ${settings.expire}"} \ + --not-before=-12h \ + --no-password --insecure \ + "Clan Intermediate CA" \ + $out/intermediate.crt + ''; + }; + }; + + services.step-ca = { + enable = true; + intermediatePasswordFile = "/dev/null"; + address = "0.0.0.0"; + port = 1443; + settings = { + root = config.clan.core.vars.generators.step-ca.files."ca.crt".path; + crt = config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path; + key = config.clan.core.vars.generators.step-intermediate-key.files."intermediate.key".path; + dnsNames = domains; + logger.format = "text"; + db = { + type = "badger"; + dataSource = "/var/lib/step-ca/db"; + }; + authority = { + provisioners = [ + { + type = "ACME"; + name = "acme"; + forceCN = true; + } + ]; + claims = { + maxTLSCertDuration = "2160h"; + defaultTLSCertDuration = "2160h"; + }; + backdate = "1m0s"; + }; + tls = { + cipherSuites = [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + ]; + minVersion = 1.2; + maxVersion = 1.3; + renegotiation = false; + }; + }; + }; + }; + }; + }; + + # Empty role, so we can add non-ca machins to the instance to trust the CA + roles.default = { + interface = + { lib, ... }: + { + options.acmeEmail = lib.mkOption { + type = lib.types.str; + default = "none@none.tld"; + description = '' + Email address for account creation and correspondence from the CA. + It is recommended to use the same email for all certs to avoid account + creation limits. + ''; + }; + }; + + perInstance = + { settings, ... }: + { + nixosModule.security.acme.defaults.email = settings.acmeEmail; + }; + }; + + # All machines (independent of role) will trust the CA + perMachine.nixosModule = + { pkgs, config, ... }: + { + # Root CA generator + clan.core.vars.generators = { + "step-ca" = { + share = true; + files."ca.key" = { + secret = true; + deploy = false; + }; + files."ca.crt".secret = false; + runtimeInputs = [ pkgs.step-cli ]; + script = '' + step certificate create --template ${pkgs.writeText "root.tmpl" '' + { + "subject": {{ toJson .Subject }}, + "issuer": {{ toJson .Subject }}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + } + ''} "Clan Root CA" $out/ca.crt $out/ca.key \ + --kty EC --curve P-256 \ + --not-after=8760h \ + --not-before=-12h \ + --no-password --insecure + ''; + }; + }; + security.pki.certificateFiles = [ config.clan.core.vars.generators."step-ca".files."ca.crt".path ]; + environment.systemPackages = [ pkgs.openssl ]; + security.acme.acceptTerms = true; + }; +} diff --git a/clanServices/certificates/flake-module.nix b/clanServices/certificates/flake-module.nix new file mode 100644 index 000000000..a58fec290 --- /dev/null +++ b/clanServices/certificates/flake-module.nix @@ -0,0 +1,21 @@ +{ + self, + lib, + ... +}: +let + module = lib.modules.importApply ./default.nix { + inherit (self) packages; + }; +in +{ + clan.modules.certificates = module; + perSystem = + { ... }: + { + clan.nixosTests.certificates = { + imports = [ ./tests/vm/default.nix ]; + clan.modules.certificates = module; + }; + }; +} diff --git a/clanServices/certificates/tests/vm/default.nix b/clanServices/certificates/tests/vm/default.nix new file mode 100644 index 000000000..3ae6079d0 --- /dev/null +++ b/clanServices/certificates/tests/vm/default.nix @@ -0,0 +1,84 @@ +{ + name = "certificates"; + + clan = { + directory = ./.; + inventory = { + + machines.ca = { }; # 192.168.1.1 + machines.client = { }; # 192.168.1.2 + machines.server = { }; # 192.168.1.3 + + instances."certificates" = { + module.name = "certificates"; + module.input = "self"; + + roles.ca.machines.ca.settings.tlds = [ "foo" ]; + roles.default.machines.client = { }; + roles.default.machines.server = { }; + }; + }; + }; + + nodes = + let + hostConfig = '' + 192.168.1.1 ca.foo + 192.168.1.3 test.foo + ''; + in + { + + client.networking.extraHosts = hostConfig; + ca.networking.extraHosts = hostConfig; + + server = { + + networking.extraHosts = hostConfig; + + # TODO: Could this be set automatically? + # I would like to get this information from the coredns module, but we + # cannot model dependencies yet + security.acme.certs."test.foo".server = "https://ca.foo/acme/acme/directory"; + + # Host a simple service on 'server', with SSL provided via our CA. 'client' + # should be able to curl it via https and accept the certificates + # presented + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.nginx = { + enable = true; + virtualHosts."test.foo" = { + enableACME = true; + forceSSL = true; + locations."/" = { + return = "200 'test server response'"; + extraConfig = "add_header Content-Type text/plain;"; + }; + }; + }; + }; + }; + + testScript = '' + start_all() + + import time + + time.sleep(3) + ca.succeed("systemctl restart acme-order-renew-ca.foo.service ") + + time.sleep(3) + server.succeed("systemctl restart acme-test.foo.service") + + # It takes a while for the correct certs to appear (before that self-signed + # are presented by nginx) so we wait for a bit. + client.wait_until_succeeds("curl -v https://test.foo") + + # Show certificate information for debugging + client.succeed("openssl s_client -connect test.foo:443 -servername test.foo /dev/null | openssl x509 -text -noout 1>&2") + ''; +} diff --git a/clanServices/certificates/tests/vm/sops/machines/ca/key.json b/clanServices/certificates/tests/vm/sops/machines/ca/key.json new file mode 100755 index 000000000..8a1bf93d0 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/machines/ca/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43", + "type": "age" + } +] diff --git a/clanServices/certificates/tests/vm/sops/machines/client/key.json b/clanServices/certificates/tests/vm/sops/machines/client/key.json new file mode 100755 index 000000000..26557f316 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/machines/client/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1js225d8jc507sgcg0fdfv2x3xv3asm4ds5c6s4hp37nq8spxu95sc5x3ce", + "type": "age" + } +] diff --git a/clanServices/certificates/tests/vm/sops/machines/server/key.json b/clanServices/certificates/tests/vm/sops/machines/server/key.json new file mode 100755 index 000000000..033453e0d --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/machines/server/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1nwuh8lc604mnz5r8ku8zswyswnwv02excw237c0cmtlejp7xfp8sdrcwfa", + "type": "age" + } +] diff --git a/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/secret b/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/secret new file mode 100644 index 000000000..be8c04004 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/secret @@ -0,0 +1,15 @@ +{ + "data": "ENC[AES256_GCM,data:6+XilULKRuWtAZ6B8Lj9UqCfi1T6dmqrDqBNXqS4SvBwM1bIWiL6juaT1Q7ByOexzID7tY740gmQBqTey54uLydh8mW0m4ZtUqw=,iv:9kscsrMPBGkutTnxrc5nrc7tQXpzLxw+929pUDKqTu0=,tag:753uIjm8ZRs0xsjiejEY8g==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1d3kycldZRXhmR0FqTXJp\nWWU0MDBYNmxxbFE5M2xKYm5KWnQ0MXBHNEM4CjN4RFFVcFlkd3pjTFVDQ3Vackdj\nVTVhMWoxdFpsWHp5S1p4L05kYk5LUkkKLS0tIENtZFZZTjY2amFVQmZLZFplQzBC\nZm1vWFI4MXR1ZHIxTTQ5VXdSYUhvOTQKte0bKjXQ0xA8FrpuChjDUvjVqp97D8kT\n3tVh6scdjxW48VSBZP1GRmqcMqCdj75GvJTbWeNEV4PDBW7GI0UW+Q==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-09-02T08:42:39Z", + "mac": "ENC[AES256_GCM,data:AftMorrH7qX5ctVu5evYHn5h9pC4Mmm2VYaAV8Hy0PKTc777jNsL6DrxFVV3NVqtecpwrzZFWKgzukcdcRJe4veVeBrusmoZYtifH0AWZTEVpVlr2UXYYxCDmNZt1WHfVUo40bT//X6QM0ye6a/2Y1jYPbMbryQNcGmnpk9PDvU=,iv:5nk+d8hzA05LQp7ZHRbIgiENg2Ha6J6YzyducM6zcNU=,tag:dy1hqWVzMu/+fSK57h9ZCA==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/users/admin b/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/users/admin new file mode 120000 index 000000000..9e21a9938 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/ca-age.key/users/admin @@ -0,0 +1 @@ +../../../users/admin \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/sops/secrets/client-age.key/secret b/clanServices/certificates/tests/vm/sops/secrets/client-age.key/secret new file mode 100644 index 000000000..316f5c069 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/client-age.key/secret @@ -0,0 +1,15 @@ +{ + "data": "ENC[AES256_GCM,data:jdTuGQUYvT1yXei1RHKsOCsABmMlkcLuziHDVhA7NequZeNu0fSbrJTXQDCHsDGhlYRcjU5EsEDT750xdleXuD3Gs9zWvPVobI4=,iv:YVow3K1j6fzRF9bRfIEpuOkO/nRpku/UQxWNGC+UJQQ=,tag:cNLM5R7uu6QpwPB9K6MYzg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvOVF2WXRSL0NpQzFZR01I\nNU85TGcyQmVDazN1dmpuRFVTZEg5NDRKTGhrCk1IVjFSU1V6WHBVRnFWcHkyVERr\nTjFKbW1mQ2FWOWhjN2VPamMxVEQ5VkkKLS0tIENVUGlhanhuWGtDKzBzRmk2dE4v\nMXZBRXNMa3IrOTZTNHRUWVE3UXEwSWMK2cBLoL/H/Vxd/klVrqVLdX9Mww5j7gw/\nEWc5/hN+km6XoW+DiJxVG4qaJ7qqld6u5ZnKgJT+2h9CfjA04I2akg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-09-02T08:42:51Z", + "mac": "ENC[AES256_GCM,data:zOBQVM2Ydu4v0+Fw3p3cEU+5+7eKaadV0tKro1JVOxclG1Vs6Myq57nw2eWf5JxIl0ulL+FavPKY26qOQ3aqcGOT3PMRlCda9z+0oSn9Im9bE/DzAGmoH/bp76kFkgTTOCZTMUoqJ+UJqv0qy1BH/92sSSKmYshEX6d1vr5ISrw=,iv:i9ZW4sLxOCan4UokHlySVr1CW39nCTusG4DmEPj/gIw=,tag:iZBDPHDkE3Vt5mFcFu1TPQ==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/clanServices/certificates/tests/vm/sops/secrets/client-age.key/users/admin b/clanServices/certificates/tests/vm/sops/secrets/client-age.key/users/admin new file mode 120000 index 000000000..9e21a9938 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/client-age.key/users/admin @@ -0,0 +1 @@ +../../../users/admin \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/sops/secrets/server-age.key/secret b/clanServices/certificates/tests/vm/sops/secrets/server-age.key/secret new file mode 100644 index 000000000..1d44d50b7 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/server-age.key/secret @@ -0,0 +1,15 @@ +{ + "data": "ENC[AES256_GCM,data:5CJuHcxJMXZJ8GqAeG3BrbWtT1kade4kxgJsn1cRpmr1UgN0ZVYnluPEiBscClNSOzcc6vcrBpfTI3dj1tASKTLP58M+GDBFQDo=,iv:gsK7XqBGkYCoqAvyFlIXuJ27PKSbTmy7f6cgTmT2gow=,tag:qG5KejkBvy9ytfhGXa/Mnw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxbzVqYkplTzJKN1pwS3VM\naFFIK2VsR3lYUVExYW9ieERBL0tlcFZtVzJRCkpiLzdmWmFlOUZ5QUJ4WkhXZ2tQ\nZm92YXBCV0RpYnIydUdEVTRiamI4bjAKLS0tIG93a2htS1hFcjBOeVFnNCtQTHVr\na2FPYjVGbWtORjJVWXE5bndPU1RWcXMKikMEB7X+kb7OtiyqXn3HRpLYkCdoayDh\n7cjGnplk17q25/lRNHM4JVS5isFfuftCl01enESqkvgq+cwuFwa9DQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-09-02T08:42:59Z", + "mac": "ENC[AES256_GCM,data:xybV2D0xukZnH2OwRpIugPnS7LN9AbgGKwFioPJc1FQWx9TxMUVDwgMN6V5WrhWkXgF2zP4krtDYpEz4Vq+LbOjcnTUteuCc+7pMHubuRuip7j+M32MH1kuf4bVZuXbCfvm7brGxe83FzjoioLqzA8g/X6Q1q7/ErkNeFjluC3Q=,iv:QEW3EUKSRZY3fbXlP7z+SffWkQeXwMAa5K8RQW7NvPE=,tag:DhFxY7xr7H1Wbd527swD0Q==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/clanServices/certificates/tests/vm/sops/secrets/server-age.key/users/admin b/clanServices/certificates/tests/vm/sops/secrets/server-age.key/users/admin new file mode 120000 index 000000000..9e21a9938 --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/secrets/server-age.key/users/admin @@ -0,0 +1 @@ +../../../users/admin \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/sops/users/admin/key.json b/clanServices/certificates/tests/vm/sops/users/admin/key.json new file mode 100644 index 000000000..e408aa96b --- /dev/null +++ b/clanServices/certificates/tests/vm/sops/users/admin/key.json @@ -0,0 +1,4 @@ +{ + "publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "type": "age" +} diff --git a/clanServices/certificates/tests/vm/vars/per-machine/ca/state-version/version/value b/clanServices/certificates/tests/vm/vars/per-machine/ca/state-version/version/value new file mode 100644 index 000000000..115ab7a6a --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/ca/state-version/version/value @@ -0,0 +1 @@ +25.11 \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-cert/intermediate.crt/value b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-cert/intermediate.crt/value new file mode 100644 index 000000000..8662199e4 --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-cert/intermediate.crt/value @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBsDCCAVegAwIBAgIQbT1Ivm+uwyf0HNkJfan2BTAKBggqhkjOPQQDAjAXMRUw +EwYDVQQDEwxDbGFuIFJvb3QgQ0EwHhcNMjUwOTAxMjA0MzAzWhcNMjYwOTAyMDg0 +MzAzWjAfMR0wGwYDVQQDExRDbGFuIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABDXCNrUIotju9P1U6JxLV43sOxLlRphQJS4dM+lvjTZc +aQ+HwQg0AHVlQNRwS3JqKrJJtJVyKbZklh6eFaDPoj6jfTB7MA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRKHaccHgP2ccSWVBWN +zGoDdTg7aTAfBgNVHSMEGDAWgBSfsnz4phMJx9su/kgeF/FbZQCBgzAVBgNVHR4B +Af8ECzAJoAcwBYIDZm9vMAoGCCqGSM49BAMCA0cAMEQCICiUDk1zGNzpS/iVKLfW +zUGaCagpn2mCx4xAXQM9UranAiAn68nVYGWjkzhU31wyCAupxOjw7Bt96XXqIAz9 +hLLtMA== +-----END CERTIFICATE----- diff --git a/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/machines/ca b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/machines/ca new file mode 120000 index 000000000..d353872a0 --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/machines/ca @@ -0,0 +1 @@ +../../../../../../sops/machines/ca \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/secret b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/secret new file mode 100644 index 000000000..421bbcb41 --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/secret @@ -0,0 +1,19 @@ +{ + "data": "ENC[AES256_GCM,data:Auonh9fa7jSkld1Zyxw74x5ydj6Xc+0SOgiqumVETNCfner9K96Rmv1PkREuHNGWPsnzyEM3pRT8ijvu3QoKvy9QPCCewyT07Wqe4G74+bk1iMeAHsV3To6kHs6M8OISvE+CmG0+hlLmdfRSabTzyWPLHbOjvFTEEuA5G7xiryacSYOE++eeEHdn+oUDh/IMTcfLjCGMjsXFikx1Hb+ofeRTlCg47+0w4MXVvQkOzQB5V2C694jZXvZ19jd/ioqr8YASz2xatGvqwW6cpZxqOWyZJ0UAj/6yFk6tZWifqVB3wgU=,iv:ITFCrDkeWl4GWCebVq15ei9QmkOLDwUIYojKZ2TU6JU=,tag:8k4iYbCIusUykY79H86WUQ==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsT25UbjJTQ2tzbnQyUm9p\neWx1UlZIeVpocnBqUCt0YnFlN2FOU25Lb0hNCmdXUUsyalRTbHRRQ0NLSGc1YllV\nUXRwaENhaXU1WmdnVDE0UWprUUUyeDAKLS0tIHV3dHU3aG5JclM0V3FadzN0SU14\ndFptbEJUNXQ4QVlqbkJ1TjAvdDQwSGsKcKPWUjhK7wzIpdIdksMShF2fpLdDTUBS\nZiU7P1T+3psxad9qhapvU0JrAY+9veFaYVEHha2aN/XKs8HqUcTp3A==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjZFVteVZwVGVmRE9NT3hG\nNGMyS3FSaXluM1FpeUp6SDVMUEpwYzg5SmdvCkRPU0QyU1JicGNkdlMyQWVkT0k3\nL2YrbDhWeGk4WFhxcUFmTmhZQ0pEQncKLS0tIG85Ui9rKzBJQ2VkMFBUQTMvSTlu\nbm8rZ09Wa24rQkNvTTNtYTZBN3MrZlkK7cjNhlUKZdOrRq/nKUsbUQgNTzX8jO+0\nzADpz6WCMvsJ15xazc10BGh03OtdMWl5tcoWMaZ71HWtI9Gip5DH0w==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-09-02T08:42:42Z", + "mac": "ENC[AES256_GCM,data:9xlO5Yis8DG/y8GjvP63NltD4xEL7zqdHL2cQE8gAoh/ZamAmK5ZL0ld80mB3eIYEPKZYvmUYI4Lkrge2ZdqyDoubrW+eJ3dxn9+StxA9FzXYwUE0t+bbsNJfOOp/kDojf060qLGsu0kAGKd2ca4WiDccR0Cieky335C7Zzhi/Q=,iv:bWQ4wr0CJHSN+6ipUbkYTDWZJyFQjDKszfpVX9EEUsY=,tag:kADIFgJBEGCvr5fPbbdEDA==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/users/admin b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/users/admin new file mode 120000 index 000000000..ca714e122 --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/ca/step-intermediate-key/intermediate.key/users/admin @@ -0,0 +1 @@ +../../../../../../sops/users/admin \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/vars/per-machine/client/state-version/version/value b/clanServices/certificates/tests/vm/vars/per-machine/client/state-version/version/value new file mode 100644 index 000000000..115ab7a6a --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/client/state-version/version/value @@ -0,0 +1 @@ +25.11 \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/vars/per-machine/server/state-version/version/value b/clanServices/certificates/tests/vm/vars/per-machine/server/state-version/version/value new file mode 100644 index 000000000..115ab7a6a --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/per-machine/server/state-version/version/value @@ -0,0 +1 @@ +25.11 \ No newline at end of file diff --git a/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.crt/value b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.crt/value new file mode 100644 index 000000000..51c82a51f --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.crt/value @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBcTCCARigAwIBAgIRAIix99+AE7Y+uyiLGaRHEhUwCgYIKoZIzj0EAwIwFzEV +MBMGA1UEAxMMQ2xhbiBSb290IENBMB4XDTI1MDkwMTIwNDI1N1oXDTI2MDkwMjA4 +NDI1N1owFzEVMBMGA1UEAxMMQ2xhbiBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZI +zj0DAQcDQgAEk7nn9kzxI+xkRmNMlxD+7T78UqV3aqus0foJh6uu1CHC+XaebMcw +JN95nAe3oYA3yZG6Mnq9nCxsYha4EhzGYqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFJ+yfPimEwnH2y7+SB4X8VtlAIGD +MAoGCCqGSM49BAMCA0cAMEQCIBId/CcbT5MPFL90xa+XQz+gVTdRwsu6Bg7ehMso +Bj0oAiBjSlttd5yeuZGXBm+O0Gl+WdKV60QlrWutNewXFS4UpQ== +-----END CERTIFICATE----- diff --git a/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/secret b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/secret new file mode 100644 index 000000000..2ae82715b --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/secret @@ -0,0 +1,15 @@ +{ + "data": "ENC[AES256_GCM,data:PnEXteU3I7U0OKgE+oR3xjHdLWYTpJjM/jlzxtGU0uP2pUBuQv3LxtEz+cP0ZsafHLNq2iNJ7xpUEE0g4d3M296S56oSocK3fREWBiJFiaC7SAEUiil1l3UCwHn7LzmdEmn8Kq7T+FK89wwqtVWIASLo2gZC/yHE5eEanEATTchGLSNiHJRzZ8n0Ekm8EFUA6czOqA5nPQHaSmeLzu1g80lSSi1ICly6dJksa6DVucwOyVFYFEeq8Dfyc1eyP8L1ee0D7QFYBMduYOXTKPtNnyDmdaQMj7cMMvE7fn04idIiAqw=,iv:nvLmAfFk2GXnnUy+Afr648R60Ou13eu9UKykkiA8Y+4=,tag:lTTAxfG0EDCU6u7xlW6xSQ==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjNWUm5NbktQeTRWRjJE\nWWFZc2Rsa3I5aitPSno1WnhORENNcng5OHprCjNUQVhBVHFBcWFjaW5UdmxKTnZw\nQlI4MDk5Wkp0RElCeWgzZ2dFQkF2dkkKLS0tIDVreTkydnJ0RDdHSHlQeVV6bGlP\nTmpJOVBSb2dkVS9TZG5SRmFjdnQ1b3cKQ5XvwH1jD4XPVs5RzOotBDq8kiE6S5k2\nDBv6ugjsM5qV7/oGP9H69aSB4jKPZjEn3yiNw++Oorc8uXd5kSGh7w==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-09-02T08:43:00Z", + "mac": "ENC[AES256_GCM,data:3jFf66UyZUWEtPdPu809LCS3K/Hc6zbnluystl3eXS+KGI+dCoYmN9hQruRNBRxf6jli2RIlArmmEPBDQVt67gG/qugTdT12krWnYAZ78iocmOnkf44fWxn/pqVnn4JYpjEYRgy8ueGDnUkwvpGWVZpcXw5659YeDQuYOJ2mq0U=,iv:3k7fBPrABdLItQ2Z+Mx8Nx0eIEKo93zG/23K+Q5Hl3I=,tag:aehAObdx//DEjbKlOeM7iQ==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/users/admin b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/users/admin new file mode 120000 index 000000000..f14859ae0 --- /dev/null +++ b/clanServices/certificates/tests/vm/vars/shared/step-ca/ca.key/users/admin @@ -0,0 +1 @@ +../../../../../sops/users/admin \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6b1616709..22265bd99 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -94,6 +94,7 @@ nav: - reference/clanServices/index.md - reference/clanServices/admin.md - reference/clanServices/borgbackup.md + - reference/clanServices/certificates.md - reference/clanServices/coredns.md - reference/clanServices/data-mesher.md - reference/clanServices/dyndns.md diff --git a/lib/clanTest/flake-module.nix b/lib/clanTest/flake-module.nix index ad55dac56..66c2137f7 100644 --- a/lib/clanTest/flake-module.nix +++ b/lib/clanTest/flake-module.nix @@ -87,6 +87,8 @@ in relativeDir = removePrefix "${self}/" (toString config.clan.directory); update-vars = hostPkgs.writeShellScriptBin "update-vars" '' + set -x + export PRJ_ROOT=$(git rev-parse --show-toplevel) ${update-vars-script} $PRJ_ROOT/${relativeDir} ${testName} ''; diff --git a/lib/test/container-test-driver/test_driver/__init__.py b/lib/test/container-test-driver/test_driver/__init__.py index ead6e9723..26f61326c 100644 --- a/lib/test/container-test-driver/test_driver/__init__.py +++ b/lib/test/container-test-driver/test_driver/__init__.py @@ -268,8 +268,14 @@ class Machine: ) def nsenter_command(self, command: str) -> list[str]: + nsenter = shutil.which("nsenter") + + if not nsenter: + msg = "nsenter command not found" + raise RuntimeError(msg) + return [ - "nsenter", + nsenter, "--target", str(self.container_pid), "--mount", @@ -326,6 +332,7 @@ class Machine: return subprocess.run( self.nsenter_command(command), + env={}, timeout=timeout, check=False, stdout=subprocess.PIPE, diff --git a/pkgs/generate-test-vars/generate_test_vars/cli.py b/pkgs/generate-test-vars/generate_test_vars/cli.py index cd608e2e9..e5dc44b4f 100755 --- a/pkgs/generate-test-vars/generate_test_vars/cli.py +++ b/pkgs/generate-test-vars/generate_test_vars/cli.py @@ -51,10 +51,13 @@ class TestFlake(Flake): clan-core#checks.. """ - def __init__(self, check_attr: str, *args: Any, **kwargs: Any) -> None: + def __init__( + self, check_attr: str, test_dir: Path, *args: Any, **kwargs: Any + ) -> None: """Initialize the TestFlake with the check attribute.""" super().__init__(*args, **kwargs) self.check_attr = check_attr + self.test_dir = test_dir @override def precache(self, selectors: list[str]) -> None: @@ -62,6 +65,10 @@ class TestFlake(Flake): # TODO @DavHau pls fix! pass + @property + def path(self) -> Path: + return self.test_dir + def select_machine(self, machine_name: str, selector: str) -> Any: """Select a nix attribute for a specific machine. @@ -189,7 +196,7 @@ def main() -> None: if system.endswith("-darwin"): test_system = system.rstrip("darwin") + "linux" - flake = TestFlake(opts.check_attr, str(opts.repo_root)) + flake = TestFlake(opts.check_attr, test_dir, str(opts.repo_root)) machine_names = get_machine_names( opts.repo_root, opts.check_attr, @@ -203,6 +210,7 @@ def main() -> None: ) # This hack is necessary because the sops store uses flake.path to find the machine keys + # This hack does not work because flake.invalidate_cache resets _path flake._path = opts.test_dir # noqa: SLF001 machines = [ @@ -211,6 +219,7 @@ def main() -> None: user = "admin" admin_key_path = Path(test_dir.resolve() / "sops" / "users" / user / "key.json") admin_key_path.parent.mkdir(parents=True, exist_ok=True) + os.environ["SOPS_AGE_KEY_FILE"] = str(admin_key_path) admin_key_path.write_text( json.dumps( {