diff --git a/docs/setup.md b/docs/setup.md index 78de01bf..da3d3d44 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -67,14 +67,11 @@ Whether anything more is needed depends on The store serves TLS, and on a hive that deploys it you need do nothing: a first-boot unit mints a CA of the store's own plus the two leaves it signs — the store's server certificate and this host's client certificate — and points -`deploy.bao.serverCertFile`, `.serverKeyFile` and `.clientCaFile` at the store's -half, `.clientCertFile`, `.clientKeyFile` and `.serverCaFile` at the reader's. +`deploy.bao.serverCertFile`, `.serverKeyFile` and `.clientCaFile` at them. Those are `mkDefault`s, so naming your own paths wins. Do that when your certificates come from a real internal CA; the store has no opinion about -which. A hive that does **not** deploy the store names the reader's three -itself: that leaf is issued out of band, and it is the one credential the store -cannot hand you, being what opens it. ⚠️ Not the gateway's HTTPS certificates and not the hive CA — this is +which. ⚠️ Not the gateway's HTTPS certificates and not the hive CA — this is **mTLS between services and the store**, a separate trust domain, because a store that took its identity from an authority it will itself distribute could never come up before that authority. diff --git a/docs/swarm/secrets.md b/docs/swarm/secrets.md index 3ce77cf6..d9ed8795 100644 --- a/docs/swarm/secrets.md +++ b/docs/swarm/secrets.md @@ -156,10 +156,6 @@ mints a CA that signs exactly two things, the store's server certificate and a reader's client certificate, and distributes nothing. A deployment with a real internal CA deletes that file and names its own paths in `deploy.bao.serverCertFile` / `clientCaFile`; the store itself has no opinion. -A hive that reads from a store on **another** machine names the reader's half — -`clientCertFile`, `clientKeyFile`, `serverCaFile` — and places that leaf by hand. -It is the one credential that cannot come out of the store, being what opens it; -everything else a hive needs does. ## The constraint that decides where the root lives diff --git a/nix/host-modules/glue-bao-tls.nix b/nix/host-modules/glue-bao-tls.nix index c6a23edb..eab900b2 100644 --- a/nix/host-modules/glue-bao-tls.nix +++ b/nix/host-modules/glue-bao-tls.nix @@ -73,14 +73,6 @@ in serverCertFile = lib.mkDefault "${pkiDir}/server.pem"; serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem"; clientCaFile = lib.mkDefault "${pkiDir}/ca.pem"; - - # A reader on this host, which happens to be the host that mints. Only - # these three are what a reader elsewhere needs placed by hand; that they - # collapse to the same CA file here is a property of self-signing, not of - # the pairing. - clientCertFile = lib.mkDefault "${pkiDir}/client.pem"; - clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem"; - serverCaFile = lib.mkDefault "${pkiDir}/ca.pem"; }; # Idempotent on ABSENCE, never on content. Re-issuing the CA invalidates diff --git a/nix/host-modules/glue-matrix-bao-token.nix b/nix/host-modules/glue-matrix-bao-token.nix index c783bade..fbc703b4 100644 --- a/nix/host-modules/glue-matrix-bao-token.nix +++ b/nix/host-modules/glue-matrix-bao-token.nix @@ -16,11 +16,11 @@ # overwrites it with the swarm's copy when the store has one. A store that is # empty or unreachable leaves a working hive with a local token. # -# 📌 This runs wherever a client identity is configured, NOT only where the -# store is. `deploy.bao.enable` would have been the co-location assumption -# itself; the reader needs a certificate, not a neighbour. On the store's own -# host ./glue-bao-tls.nix supplies one as a `mkDefault` and nothing changes; -# elsewhere an operator places the leaf and names it, and the same unit works. +# 📌 LIMIT, stated rather than hidden: this gates on the store running HERE. +# A hive reading from a store on another machine needs the same unit with that +# machine's address and a client leaf issued out of band — the mechanism is +# identical, only `-address` and the cert's provenance differ. Deferred until +# there is a second hive to test it against, rather than shipped untested. { pkgs, lib, @@ -33,12 +33,11 @@ let baoCfg = hyperhiveCfg.swarm.bao; matrixCfg = hyperhiveCfg.swarm.matrix; - baoDeploy = deployCfg.bao; - - # What decides whether this unit exists at all. A reader is defined by holding - # a certificate the store accepts, and that is true on the store's own host - # and on a hive three networks away for exactly the same reason. - haveClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null; + # Owned by ./glue-bao-tls.nix, which mints them. Named here rather than + # shared through a `let`: a cross-module binding would make these two files + # one file with a gap in the middle, and the whole point of a glue module is + # that it can be deleted on its own. + pkiDir = "/var/lib/swarm-bao-pki"; # Where the token lives in the store. A path, not a convention to guess at: # whoever writes it and whoever reads it must agree, and the agreement @@ -53,19 +52,15 @@ let matrixMachine = "hive-matrix"; in { - config = lib.mkIf (hyperhiveCfg.enable && haveClientIdentity && deployCfg.matrix.enable) { + config = lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable && deployCfg.matrix.enable) { systemd.services.swarm-bao-matrix-token = { description = "fetch the matrix registration token from the swarm secret store"; - # Every one of these names a unit that exists only where the store runs. - # `Requires=` on an absent unit fails the job outright, so the ordering is - # conditional even though the read is not: off-host there is nothing local - # to wait for, and the timeout below is what bounds the attempt instead. - after = lib.optionals baoDeploy.enable [ + after = [ "swarm-bao-pki.service" "container@${baoCfg.machine}.service" ]; - wants = lib.optionals baoDeploy.enable [ "container@${baoCfg.machine}.service" ]; - requires = lib.optionals baoDeploy.enable [ "swarm-bao-pki.service" ]; + wants = [ "container@${baoCfg.machine}.service" ]; + requires = [ "swarm-bao-pki.service" ]; before = [ "container@${matrixMachine}.service" ]; wantedBy = [ "container@${matrixMachine}.service" ]; path = [ @@ -82,13 +77,9 @@ in }; environment = { BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; - BAO_CLIENT_CERT = baoDeploy.clientCertFile; - BAO_CLIENT_KEY = baoDeploy.clientKeyFile; - } - # Absent means the system trust store, which is what a deployment with a - # real CA wants and what a self-signed one must not be left with. - // lib.optionalAttrs (baoDeploy.serverCaFile != null) { - BAO_CACERT = baoDeploy.serverCaFile; + BAO_CACERT = "${pkiDir}/ca.pem"; + BAO_CLIENT_CERT = "${pkiDir}/client.pem"; + BAO_CLIENT_KEY = "${pkiDir}/client-key.pem"; }; script = '' set -euo pipefail diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index a5b3d7c6..501569f8 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -268,54 +268,6 @@ in ''; }; - clientCertFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/swarm-bao/client.pem"; - description = '' - Certificate a **reader on this machine** presents to the store. - - The counterpart to {option}`services.hyperhive.deploy.bao.clientCaFile`, - which is the store's side of the same handshake. Only the store's side - was declarable, so a hive that did not run the store had no way to be - pointed at a certificate even when one was placed for it. - - On a hive that runs the store, a glue module supplies the leaf it minted, - as a `mkDefault`. Everywhere else this is the credential an operator - places by hand — the one secret that cannot come out of the store, - because it is what opens it. - - A path, never a value. - ''; - }; - - clientKeyFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/swarm-bao/client-key.pem"; - description = '' - Private key for {option}`services.hyperhive.deploy.bao.clientCertFile`. - Both or neither — a certificate with no key authenticates nothing. - ''; - }; - - serverCaFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/swarm-bao/ca.pem"; - description = '' - Authority a **reader on this machine** validates the store's certificate - against. - - Not {option}`services.hyperhive.deploy.bao.clientCaFile` with the words - rearranged: that one is the store choosing which readers to trust, this - one is a reader choosing which store to trust. A deployment that - self-signs both ends points them at the same file and reads that as - confirmation they are interchangeable — they are not, and they diverge - the moment either end gets a real CA. - ''; - }; - extraListenAddresses = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 73b49a84..ebc36bee 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -90,18 +90,6 @@ let deploy.bao.enable = true; deploy.matrix.enable = true; }; - # A hive that reads from a store it does not run: no `deploy.bao.enable`, so - # nothing here mints a leaf and the operator names one placed by hand. The - # deployment this pairing exists to serve, and the one that was previously - # inexpressible — the gate asked whether the store was a neighbour. - baoRemoteReader = hive { - deploy.matrix.enable = true; - deploy.bao.clientCertFile = "/etc/pki/bao-client.pem"; - deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem"; - }; - # The same hive with the identity taken away, which separates "a homeserver - # is deployed" from "this host can authenticate to the store". - matrixNoBaoIdentity = hive { deploy.matrix.enable = true; }; # A priority collision is a property of the *option*, not # of the merged value's interior — nix throws the moment the value is @@ -217,48 +205,10 @@ let name = "a store with no homeserver beside it renders no token reader"; ok = !(baoPkcs11.systemd.services ? swarm-bao-matrix-token); } - { - name = "a hive that names a client identity reads from a store it does not run"; - ok = baoRemoteReader.systemd.services ? swarm-bao-matrix-token; - } - { - # Absence arm for the one above, and the reason the gate is the identity - # rather than the homeserver: without it, deploying matrix anywhere would - # render a reader that cannot authenticate. - name = "a homeserver with no way to authenticate to the store renders no token reader"; - ok = !(matrixNoBaoIdentity.systemd.services ? swarm-bao-matrix-token); - } - { - # `Requires=` on a unit that does not exist fails the job, and nothing - # local mints certificates off-host — so this orders against nothing. - # Eval cannot see that failure; only the empty list here stands in for it. - name = "an off-host reader requires no unit the store's host would have provided"; - # Membership first, then the value: indexing a missing unit throws, and a - # table that reports which property broke must not be the thing that dies. - ok = - let - s = baoRemoteReader.systemd.services; - in - s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ ]; - } - { - # Presence control for the case above: the list is conditional, not gone. - name = "a co-located reader still orders after the local pki unit"; - ok = - let - s = baoWithMatrix.systemd.services; - in - s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ "swarm-bao-pki.service" ]; - } ]; bad = builtins.filter (c: !c.ok) cases; - # Escaped, because a name is prose and prose contains apostrophes. Hand-quoting - # broke the builder mid-report on the first such name that failed — and only - # ever on failure, so every green run agreed the reporter was fine. - report = lib.concatMapStringsSep "\n" ( - c: " echo ${lib.escapeShellArg "FAILED: ${c.name}"} >&2" - ) bad; + report = lib.concatMapStringsSep "\n" (c: " echo 'FAILED: ${c.name}' >&2") bad; in # The results are embedded in the builder text on purpose: that is what # makes this derivation's hash depend on them, so a nix-only change that