diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index fd80803f..0ff92c8e 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -24,7 +24,6 @@ ./hive-tls.nix ./otel.nix ./glue-bao-tls.nix - ./glue-controller-bao-identity.nix ./glue-matrix-bao-token.nix ./swarm-authelia.nix ./swarm-bao.nix diff --git a/nix/host-modules/glue-bao-tls.nix b/nix/host-modules/glue-bao-tls.nix index 4a4aff4f..0004b2b5 100644 --- a/nix/host-modules/glue-bao-tls.nix +++ b/nix/host-modules/glue-bao-tls.nix @@ -16,8 +16,8 @@ # ⚠️ Not the hive CA and not the swarm CA. The store will eventually # distribute both, and an authority you must already hold a certificate from # cannot be one the store hands out — reach the store to get the CA material, -# need a cert from that CA to reach the store. This CA signs a fixed, short -# list of leaves and distributes nothing, so it cannot enter that cycle. +# need a cert from that CA to reach the store. This CA signs exactly two +# things and distributes nothing, so it cannot enter that cycle. # # ⚠️ Files like this are the only place a `deploy.` value may derive from # a `deploy..enable`. Everywhere else that is forbidden. The exception @@ -126,17 +126,6 @@ in ${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth [ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \ ${lib.escapeShellArg clientCn} "" clientAuth - - # Minted whether or not a controller runs here, because the case it - # serves is the one where it does not: a controller elsewhere needs a - # leaf from this CA and has no way to sign one. Issuing it here turns - # "obtain a certificate out of band" into "copy this file". - # - # Its own CN rather than the reader's above: the controller's policy - # lets it create roles for every hive, and the reader's leaf carries - # this hive's name. - [ -s ${pkiDir}/controller.pem ] || ${signLeaf} ${pkiDir} controller \ - ${lib.escapeShellArg deployCfg.bao.controllerCommonName} "" clientAuth ''; }; }; diff --git a/nix/host-modules/glue-controller-bao-identity.nix b/nix/host-modules/glue-controller-bao-identity.nix deleted file mode 100644 index 049c3746..00000000 --- a/nix/host-modules/glue-controller-bao-identity.nix +++ /dev/null @@ -1,41 +0,0 @@ -# Glue: point the swarm controller at the bao leaf minted for it. -# -# ONE PAIRING PER FILE — controller ← bao, and nothing else. Deleting this -# leaves a controller that takes operator-provided certificate paths, which is -# what every deployment that does not mint its own already does. -# -# ⚠️ The minting is NOT here. ./glue-bao-tls.nix holds the CA and signs the -# leaf, because the thing that owns a private key owns issuing from it; a -# second signer would duplicate that file's `openssl` helper to no benefit. -# What belongs here is the pairing: which paths this host's controller reads. -# -# ⚠️ Gated on the leaf existing, not on the store being enabled. A controller -# on the store's own host is one deployment; a controller three networks away -# holding a leaf issued out of band is another, and both want the same wiring. -# `deploy.bao.enable` would have made the first one the only supported shape. -# -# Everything is `mkDefault`. An operator naming their own paths wins. -{ - lib, - config, - ... -}: -let - hyperhiveCfg = config.services.hyperhive; - deployCfg = hyperhiveCfg.deploy; - baoDeploy = deployCfg.bao; - - # Where ./glue-bao-tls.nix puts the leaves. Derived from the reader's own - # path rather than repeating that file's directory literal: an operator who - # moves the PKI moves both, and the two cannot drift apart. - haveMintedPki = baoDeploy.clientCertFile != null; - pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; -in -{ - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.swarm-controller.enable && haveMintedPki) { - services.hyperhive.deploy.swarm-controller = { - baoClientCertFile = lib.mkDefault "${pkiDir}/controller.pem"; - baoClientKeyFile = lib.mkDefault "${pkiDir}/controller-key.pem"; - }; - }; -} diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 16f067a7..fff2de8c 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -154,9 +154,10 @@ let # policy by spelling it the same way, and is itself named after it. controllerPolicyName = "swarm-controller"; - # An option rather than a literal because whatever mints the controller's - # leaf has to spell it identically, and that lives in another file. - controllerCn = baoDeploy.controllerCommonName; + # The subject the controller's certificate must carry. Cert auth matches on + # the CN, so this is an interface rather than a label: a leaf signed by the + # right CA but minted with any other subject cannot authenticate. + controllerCn = "swarm-controller"; # Derived with `bao write -output-policy`, which short-circuits the request # and needs no server, rather than written from memory. @@ -477,27 +478,6 @@ in ''; }; - controllerCommonName = lib.mkOption { - type = lib.types.str; - default = "swarm-controller"; - example = "swarm-controller.svc"; - description = '' - Subject the store's `swarm-controller` cert-auth role accepts. An - interface, not a label: cert auth matches on the CN, so whatever mints - the controller's leaf has to spell it the same way. - - Lives here because it is a property of the **role**, which this module - writes — not a credential belonging to the controller. Nothing outside - the store needs it except whatever issues that leaf. - - ⚠️ The CA this role trusts also signs each hive's reader leaf, whose CN - is the hive's own name, so a hive named `swarm-controller` would satisfy - the role. Changing this to something outside the hive-name grammar - (`[a-z0-9-]`) rules that out — at the cost of a role rename in any store - that has already run the granting unit. - ''; - }; - serverCaFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index a88b483e..29d445ec 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -467,37 +467,6 @@ in ''; }; - baoClientCertFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/swarm-controller/bao-client.pem"; - description = '' - Certificate this daemon presents to the swarm's secret store. Its - subject has to match - {option}`services.hyperhive.deploy.bao.controllerCommonName`, which is - what the store's cert-auth role matches on. - - Distinct from {option}`services.hyperhive.deploy.bao.clientCertFile` on - purpose, and not an oversight: that one is **this host as a reader** and - carries the hive's name, while the controller's policy lets it create - roles for every hive. One certificate serving both would hand that power - to whatever else reads the store here. - - Defaulted by `glue-controller-bao-identity.nix` where this deployment - mints its own; elsewhere the leaf is issued out of band and named here. - ''; - }; - - baoClientKeyFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/swarm-controller/bao-client-key.pem"; - description = '' - Private key for {option}`services.hyperhive.deploy.swarm-controller.baoClientCertFile`. - Both or neither — a certificate with no key authenticates nothing. - ''; - }; - queue = { clientSecretFile = lib.mkOption { type = lib.types.str; diff --git a/nix/module-eval.nix b/nix/module-eval.nix index e050c80c..0c80c52c 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -254,20 +254,6 @@ let baoGrantNoStore = hive { deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; }; - # Store and controller on one machine, with a CN no default could supply. - # The odd value is what lets the case below tell "both ends read the same - # option" from "both ends happen to say swarm-controller". - baoControllerHere = hive { - deploy.bao.enable = true; - deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; - deploy.bao.controllerCommonName = "cn-marker-not-a-default"; - deploy.swarm-controller.enable = true; - }; - # The controller with no store, which is every spread deployment. Nothing - # mints here, so the pairing must leave the paths unset rather than name - # files this host will never have. - controllerNoStore = hive { deploy.swarm-controller.enable = true; }; - # The store and the token, with no CA to trust. `mkForce` because the PKI # glue supplies one by default here — this is the deployment that brings its # own certificates and has not named the authority yet, and it separates @@ -541,49 +527,6 @@ let && lib.hasInfix "auth/cert/certs/swarm-controller" s && lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s; } - { - # Nothing asserted the PKI script before this, so a third leaf could be - # added to it and every case still passed — measured, not assumed: the - # commit that added one left `module-eval`'s derivation unchanged. - name = "the store mints a leaf for the controller, and the controller is pointed at it"; - ok = - let - m = baoControllerHere; - pki = m.systemd.services.swarm-bao-pki.script; - in - lib.hasInfix "controller.pem" pki - && - m.services.hyperhive.deploy.swarm-controller.baoClientCertFile - == "/var/lib/swarm-bao-pki/controller.pem" - && - m.services.hyperhive.deploy.swarm-controller.baoClientKeyFile - == "/var/lib/swarm-bao-pki/controller-key.pem"; - } - { - # What makes the one above mean something: a controller with no store - # has nothing to be pointed at. Naming a path here would be a file this - # host never gets, which fails at a TLS handshake rather than at eval. - name = "a controller on a host with no store is left without certificate paths"; - ok = - let - c = controllerNoStore.services.hyperhive.deploy.swarm-controller; - in - c.baoClientCertFile == null && c.baoClientKeyFile == null; - } - { - # The CN is an interface between two files: the store writes a role that - # matches it, the PKI mints a leaf that carries it. They read one option, - # and this is what says so — the fixture's value cannot come from a - # default, so matching it in both places is not a coincidence. - name = "the cert-auth role and the minted leaf take their subject from one option"; - ok = - let - m = baoControllerHere; - role = m.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script; - pki = m.systemd.services.swarm-bao-pki.script; - in - lib.hasInfix "cn-marker-not-a-default" role && lib.hasInfix "cn-marker-not-a-default" pki; - } { # The arm that makes the one above mean something. A role's trust anchor # is the CA, so with none named there is nothing to write — and the