From 54226e027d948d7e6a28bb59dd65359f9dfb32b7 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 9 Oct 2026 09:37:03 +0200 Subject: [PATCH] swarm-bao: build the granter policy units from one helper The six hand-written granter policy units (secret-publisher, matrix-ctl, services-issuer, nats-tls, hive-leaf, operator-viewer) repeated the same skeleton readerPolicyUnit already carried: ordering after the controller's unit, pki requirement, path, granter environment, unseal retry window and oneshot serviceConfig. They differed only in description and the script after the granter login. granterPolicyUnit now owns that skeleton; the six units and readerPolicyUnit pass a description and a script body. Every rendered unit is unchanged byte-for-byte. --- nix/host-modules/swarm-bao.nix | 445 +++++++++++---------------------- 1 file changed, 143 insertions(+), 302 deletions(-) diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index dc33fe88..a2498c03 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -969,18 +969,13 @@ let display_name=${lib.escapeShellArg obj.cn} ''; - # One unit per PRINCIPAL, not one unit for all of them: these names are - # operator-facing strings, the same reason `swarm-bao-secret-publisher-policy` - # above gives for not being more script inside the controller's unit. A hive - # whose queue credential never arrives is diagnosed by the state of the unit named - # after it. - # - # `after` and not `requires`, for the reason the publisher's unit states: the - # controller's and the granter's units create the mounts this one writes - # into, but a failed oneshot still counts as finished, so ordering plus this - # unit's own retry is what converges. - readerPolicyUnit = - description: objects: + # A unit that logs in as the granter and runs `body`, ordered after the + # controller's unit. `after` and not `requires`: the controller's and the + # granter's units create the mounts these write into, but a failed oneshot + # still counts as finished, so ordering plus this unit's own retry is what + # converges. `body` follows the login with no separator. + granterPolicyUnit = + description: body: lib.mkIf haveGranter { inherit description; after = [ @@ -995,8 +990,8 @@ let pkgs.coreutils ]; environment = granterEnv; - # Same unseal wait as its siblings above, for the reason stated there: - # under `seal = "shamir"` a human unseals by hand. + # Same unseal wait as `swarm-bao-controller-policy`, for the reason stated + # there: under `seal = "shamir"` a human unseals by hand. startLimitBurst = 2880; startLimitIntervalSec = 90000; serviceConfig = { @@ -1010,11 +1005,20 @@ let ${granterLogin} '' - + lib.concatMapStrings readerPolicyWrite objects - + "\n" - + lib.concatMapStrings readerRoleWrite objects; + + body; }; + # One unit per PRINCIPAL, not one unit for all of them: these names are + # operator-facing strings, the same reason `swarm-bao-secret-publisher-policy` + # below gives for not being more script inside the controller's unit. A hive + # whose queue credential never arrives is diagnosed by the state of the unit named + # after it. + readerPolicyUnit = + description: objects: + granterPolicyUnit description ( + lib.concatMapStrings readerPolicyWrite objects + "\n" + lib.concatMapStrings readerRoleWrite objects + ); + # Every listener serves the same identity: they differ in which address # they answer on, not in who they are. Client verification is separate and # optional — a store with no `clientCaFile` still serves TLS, it just does @@ -3013,45 +3017,17 @@ in # finished, so `requires` would neither wait for its success nor re-run # this one when its own retry eventually lands. Ordering plus this unit's # own retry is what actually converges. - systemd.services.swarm-bao-secret-publisher-policy = lib.mkIf haveGranter { - description = "write the swarm secret publisher's bao policy and cert-auth role"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its sibling above, for the reason stated there: - # under `seal = "shamir"` a human unseals by hand, which can take a day. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-secret-publisher-policy = granterPolicyUnit "write the swarm secret publisher's bao policy and cert-auth role" '' - ${granterLogin} + printf '%s' ${lib.escapeShellArg secretPublisherPolicyText} | + bao policy write ${lib.escapeShellArg secretPublisherPolicyName} - - printf '%s' ${lib.escapeShellArg secretPublisherPolicyText} | - bao policy write ${lib.escapeShellArg secretPublisherPolicyName} - - - bao write auth/cert/certs/${lib.escapeShellArg secretPublisherPolicyName} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg secretPublisherCn} \ - token_policies=${lib.escapeShellArg secretPublisherPolicyName} \ - display_name=${lib.escapeShellArg secretPublisherCn} - ''; - }; + bao write auth/cert/certs/${lib.escapeShellArg secretPublisherPolicyName} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg secretPublisherCn} \ + token_policies=${lib.escapeShellArg secretPublisherPolicyName} \ + display_name=${lib.escapeShellArg secretPublisherCn} + ''; # A THIRD sibling, for the reason the second one's comment gives: these # unit names are operator-facing strings, and the grant written here @@ -3060,45 +3036,17 @@ in # creates the mounts this one writes into, but a failed oneshot still # counts as finished, so only ordering plus this unit's own retry # converges. - systemd.services.swarm-bao-matrix-ctl-policy = lib.mkIf haveGranter { - description = "write swarm-matrix-ctl's bao policy and cert-auth role"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its two siblings above, for the reason stated - # there: under `seal = "shamir"` a human unseals by hand. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-matrix-ctl-policy = granterPolicyUnit "write swarm-matrix-ctl's bao policy and cert-auth role" '' - ${granterLogin} + printf '%s' ${lib.escapeShellArg matrixCtlPolicyText} | + bao policy write ${lib.escapeShellArg matrixCtlPolicyName} - - printf '%s' ${lib.escapeShellArg matrixCtlPolicyText} | - bao policy write ${lib.escapeShellArg matrixCtlPolicyName} - - - bao write auth/cert/certs/${lib.escapeShellArg matrixCtlPolicyName} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg matrixCtlCn} \ - token_policies=${lib.escapeShellArg matrixCtlPolicyName} \ - display_name=${lib.escapeShellArg matrixCtlCn} - ''; - }; + bao write auth/cert/certs/${lib.escapeShellArg matrixCtlPolicyName} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg matrixCtlCn} \ + token_policies=${lib.escapeShellArg matrixCtlPolicyName} \ + display_name=${lib.escapeShellArg matrixCtlCn} + ''; # 🩸 The principals that each log in with a leaf of their own, not as the # hive. See the block around `perHiveReaders` for what each grant is and @@ -3128,45 +3076,17 @@ in # # The policy text lives beside the role that carries it: a policy and # its role belong in one place, with the principal they grant. - systemd.services.swarm-bao-services-issuer-policy = lib.mkIf haveGranter { - description = "write the swarm services issuer's bao policy and cert-auth role"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its three siblings above, for the reason stated - # there: under `seal = "shamir"` a human unseals by hand. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-services-issuer-policy = granterPolicyUnit "write the swarm services issuer's bao policy and cert-auth role" '' - ${granterLogin} + printf '%s' ${lib.escapeShellArg servicesIssuerPolicyText} | + bao policy write ${lib.escapeShellArg servicesIssuerPolicyName} - - printf '%s' ${lib.escapeShellArg servicesIssuerPolicyText} | - bao policy write ${lib.escapeShellArg servicesIssuerPolicyName} - - - bao write auth/cert/certs/${lib.escapeShellArg servicesIssuerPolicyName} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg servicesIssuerCn} \ - token_policies=${lib.escapeShellArg servicesIssuerPolicyName} \ - display_name=${lib.escapeShellArg servicesIssuerCn} - ''; - }; + bao write auth/cert/certs/${lib.escapeShellArg servicesIssuerPolicyName} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg servicesIssuerCn} \ + token_policies=${lib.escapeShellArg servicesIssuerPolicyName} \ + display_name=${lib.escapeShellArg servicesIssuerCn} + ''; # A FIFTH sibling: the queue's issuing role, its policy and its login # role, together. The `pki` role lives here rather than beside @@ -3176,125 +3096,73 @@ in # The role narrows exactly as `swarm-services` does, to one name. It is # the queue's domain alone, since the same name reaches it from every # hive; no IP SANs, since nothing dials an address. - systemd.services.swarm-bao-nats-tls-policy = lib.mkIf haveGranter { - description = "write the swarm queue's pki role, bao policy and cert-auth role"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its siblings above. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-nats-tls-policy = granterPolicyUnit "write the swarm queue's pki role, bao policy and cert-auth role" '' - ${granterLogin} + bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${natsPkiRoleName}"} \ + allowed_domains=${lib.escapeShellArg hyperhiveCfg.swarm.nats.domain} \ + allow_bare_domains=true \ + allow_subdomains=false \ + allow_glob_domains=false \ + allow_localhost=false \ + allow_any_name=false \ + allow_ip_sans=false \ + enforce_hostnames=true \ + server_flag=true \ + client_flag=false \ + key_type=rsa \ + key_bits=4096 \ + ttl=${servicesPkiLeafTtl} \ + max_ttl=${servicesPkiLeafTtl} - bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${natsPkiRoleName}"} \ - allowed_domains=${lib.escapeShellArg hyperhiveCfg.swarm.nats.domain} \ - allow_bare_domains=true \ - allow_subdomains=false \ - allow_glob_domains=false \ - allow_localhost=false \ - allow_any_name=false \ - allow_ip_sans=false \ - enforce_hostnames=true \ - server_flag=true \ - client_flag=false \ - key_type=rsa \ - key_bits=4096 \ - ttl=${servicesPkiLeafTtl} \ - max_ttl=${servicesPkiLeafTtl} + printf '%s' ${lib.escapeShellArg natsPolicyText} | + bao policy write ${lib.escapeShellArg natsPolicyName} - - printf '%s' ${lib.escapeShellArg natsPolicyText} | - bao policy write ${lib.escapeShellArg natsPolicyName} - - - bao write auth/cert/certs/${lib.escapeShellArg natsPolicyName} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg natsCn} \ - token_policies=${lib.escapeShellArg natsPolicyName} \ - display_name=${lib.escapeShellArg natsCn} - ''; - }; + bao write auth/cert/certs/${lib.escapeShellArg natsPolicyName} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg natsCn} \ + token_policies=${lib.escapeShellArg natsPolicyName} \ + display_name=${lib.escapeShellArg natsCn} + ''; # A SIXTH sibling, the nats unit's shape once per hive: each hive's # gateway-leaf role, its policy and its login role. The role allows the # hive's domain and `*.` that domain, and no IP SANs or localhost. - systemd.services.swarm-bao-hive-leaf-policy = lib.mkIf haveGranter { - description = "write the per-hive gateway-leaf pki roles, bao policies and cert-auth roles"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its siblings above. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-hive-leaf-policy = + granterPolicyUnit "write the per-hive gateway-leaf pki roles, bao policies and cert-auth roles" + ( + lib.concatMapStrings (obj: '' - ${granterLogin} - '' - + lib.concatMapStrings (obj: '' + bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${obj.name}"} \ + allowed_domains=${lib.escapeShellArg "${obj.domain},*.${obj.domain}"} \ + allow_bare_domains=true \ + allow_subdomains=false \ + allow_glob_domains=true \ + allow_wildcard_certificates=true \ + allow_localhost=false \ + allow_any_name=false \ + allow_ip_sans=false \ + enforce_hostnames=true \ + server_flag=true \ + client_flag=false \ + key_type=rsa \ + key_bits=4096 \ + ttl=${servicesPkiLeafTtl} \ + max_ttl=${servicesPkiLeafTtl} - bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${obj.name}"} \ - allowed_domains=${lib.escapeShellArg "${obj.domain},*.${obj.domain}"} \ - allow_bare_domains=true \ - allow_subdomains=false \ - allow_glob_domains=true \ - allow_wildcard_certificates=true \ - allow_localhost=false \ - allow_any_name=false \ - allow_ip_sans=false \ - enforce_hostnames=true \ - server_flag=true \ - client_flag=false \ - key_type=rsa \ - key_bits=4096 \ - ttl=${servicesPkiLeafTtl} \ - max_ttl=${servicesPkiLeafTtl} + printf '%s' ${lib.escapeShellArg '' + path "${servicesPkiMountPath}/issue/${obj.name}" { + capabilities = ["update"] + } + ''} | + bao policy write ${lib.escapeShellArg obj.name} - - printf '%s' ${lib.escapeShellArg '' - path "${servicesPkiMountPath}/issue/${obj.name}" { - capabilities = ["update"] - } - ''} | - bao policy write ${lib.escapeShellArg obj.name} - - - bao write auth/cert/certs/${lib.escapeShellArg obj.name} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg obj.cn} \ - token_policies=${lib.escapeShellArg obj.name} \ - display_name=${lib.escapeShellArg obj.cn} - '') hiveLeafIssuers; - }; + bao write auth/cert/certs/${lib.escapeShellArg obj.name} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg obj.cn} \ + token_policies=${lib.escapeShellArg obj.name} \ + display_name=${lib.escapeShellArg obj.cn} + '') hiveLeafIssuers + ); # The agent PKI mount: its root, its one role, the host's copy of the # root's certificate, and the listener's trust in it. @@ -3469,76 +3337,49 @@ in # this writes the policy and stops with exit 0: nothing is broken, the UI # still takes a token, and a day of retries would change nothing. # `swarm-bao-granter-role` restarts this unit each time it succeeds. - systemd.services.swarm-bao-operator-viewer-policy = lib.mkIf haveGranter { - description = "write the bao UI's OIDC login: the operator viewer policy, the oidc config and its role"; - after = [ - "container@${cfg.machine}.service" - "swarm-bao-controller-policy.service" - ] - ++ granterAfter; - requires = [ "swarm-bao-pki.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ - baoCli - pkgs.coreutils - ]; - environment = granterEnv; - # Same unseal wait as its siblings above, for the reason stated there. - startLimitBurst = 2880; - startLimitIntervalSec = 90000; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - Restart = "on-failure"; - RestartSec = 30; - }; - script = '' - set -euo pipefail + systemd.services.swarm-bao-operator-viewer-policy = granterPolicyUnit "write the bao UI's OIDC login: the operator viewer policy, the oidc config and its role" '' - ${granterLogin} + printf '%s' ${lib.escapeShellArg operatorViewerPolicyText} | + bao policy write ${lib.escapeShellArg operatorViewerPolicyName} - - printf '%s' ${lib.escapeShellArg operatorViewerPolicyText} | - bao policy write ${lib.escapeShellArg operatorViewerPolicyName} - + # Asks the granter's own token, through the `default` policy's + # `sys/capabilities-self`, rather than probing the mount: the granter + # holds no `sys/auth` to list mounts with. + caps="$(bao token capabilities auth/oidc/config)" + case "$caps" in + *update*) ;; + *) + echo "the granter may not configure auth/oidc yet (capabilities: $caps), so the UI's OIDC login stays off and token login is unchanged." >&2 + echo "one-time step, as root on this host (docs/getting-started/setup.md):" >&2 + ${lib.concatMapStringsSep "\n" (l: "echo ${lib.escapeShellArg " ${l}"} >&2") granterSetupSteps} + echo "swarm-bao-granter-role re-runs this unit once it succeeds." >&2 + exit 0 + ;; + esac - # Asks the granter's own token, through the `default` policy's - # `sys/capabilities-self`, rather than probing the mount: the granter - # holds no `sys/auth` to list mounts with. - caps="$(bao token capabilities auth/oidc/config)" - case "$caps" in - *update*) ;; - *) - echo "the granter may not configure auth/oidc yet (capabilities: $caps), so the UI's OIDC login stays off and token login is unchanged." >&2 - echo "one-time step, as root on this host (docs/getting-started/setup.md):" >&2 - ${lib.concatMapStringsSep "\n" (l: "echo ${lib.escapeShellArg " ${l}"} >&2") granterSetupSteps} - echo "swarm-bao-granter-role re-runs this unit once it succeeds." >&2 - exit 0 - ;; - esac + # Published by ./swarm-secret-publisher.nix on authelia's host, + # which this boot does not wait on, so absence is retried. + if ! secret="$(bao kv get -field=value ${lib.escapeShellArg "${credentialMountPath}/${baoUiClientLeaf}"} 2>"$err")"; then + echo ${lib.escapeShellArg "the store did not return ${credentialMountPath}/${baoUiClientLeaf}: the UI's OIDC client secret is not published yet."} >&2 + cat "$err" >&2 + exit 1 + fi + if [ -z "$secret" ]; then + echo ${lib.escapeShellArg "the store returned an empty ${credentialMountPath}/${baoUiClientLeaf}."} >&2 + exit 1 + fi - # Published by ./swarm-secret-publisher.nix on authelia's host, - # which this boot does not wait on, so absence is retried. - if ! secret="$(bao kv get -field=value ${lib.escapeShellArg "${credentialMountPath}/${baoUiClientLeaf}"} 2>"$err")"; then - echo ${lib.escapeShellArg "the store did not return ${credentialMountPath}/${baoUiClientLeaf}: the UI's OIDC client secret is not published yet."} >&2 - cat "$err" >&2 - exit 1 - fi - if [ -z "$secret" ]; then - echo ${lib.escapeShellArg "the store returned an empty ${credentialMountPath}/${baoUiClientLeaf}."} >&2 - exit 1 - fi + # bao fetches authelia's discovery document on this write, so an + # unreachable or untrusted authelia fails it and the unit retries. + printf '%s' "$secret" | + bao write auth/oidc/config ${ + lib.concatMapStringsSep " " lib.escapeShellArg operatorViewerOidcConfig + } - # bao fetches authelia's discovery document on this write, so an - # unreachable or untrusted authelia fails it and the unit retries. - printf '%s' "$secret" | - bao write auth/oidc/config ${ - lib.concatMapStringsSep " " lib.escapeShellArg operatorViewerOidcConfig - } - - # A JSON body on stdin, because `bound_claims` is a map. - printf '%s' ${lib.escapeShellArg operatorViewerRole} | - bao write auth/oidc/role/${lib.escapeShellArg operatorViewerPolicyName} - - ''; - }; + # A JSON body on stdin, because `bound_claims` is a map. + printf '%s' ${lib.escapeShellArg operatorViewerRole} | + bao write auth/oidc/role/${lib.escapeShellArg operatorViewerPolicyName} - + ''; # The CA bind source is written at runtime by a host unit, so the # container has to start after it — otherwise nspawn sets up a mount