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