diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index e720cfe8..17039af3 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -25,6 +25,7 @@ ./hive-priv.nix ./hive-priv-cache.nix ./hive-squid.nix + ./hive-squid-credential.nix ./hive-tls.nix ./otel.nix ./glue-bao-readers-policy-order.nix diff --git a/nix/host-modules/hive-squid-credential.nix b/nix/host-modules/hive-squid-credential.nix new file mode 100644 index 00000000..47be3e2a --- /dev/null +++ b/nix/host-modules/hive-squid-credential.nix @@ -0,0 +1,103 @@ +# The password this hive's squid self-check logs in to squid with, kept where +# only root can read it. +# +# swarm-controller mints it at `swarm/hives//squid-selfcheck` +# (`swarm_secret_client::proxy::selfcheck_path`). hive-c0re reads the same path +# to put its hash in squid's users file; this root oneshot reads it under the +# hive's own store identity and lands it as the systemd credential +# `squid-selfcheck` imports (`/run/credstore/`, root, `0400`, tmpfs). +# +# A hive with no minted credential yet leaves the file alone and the self-check +# reports `credential=absent`. +{ + pkgs, + lib, + config, + ... +}: +let + hyperhiveCfg = config.services.hyperhive; + hiveController = hyperhiveCfg.deploy.hive-controller; + tls = hiveController.tls; + baoCfg = hyperhiveCfg.swarm.bao; + baoDeploy = hyperhiveCfg.deploy.bao; + + enabled = + hiveController.enable + && hiveController.hiveName != null + && tls.baoHiveIdentityCertFile != null + && tls.baoHiveIdentityKeyFile != null; + + credentialFile = "/run/credstore/${hyperhiveCfg.squid.selfcheck.credentialName}"; + + storeRetry = import ./lib/store-retry.nix { }; + storeRefreshTimer = import ./lib/store-refresh-timer.nix { }; +in +{ + config = lib.mkIf enabled { + systemd.services.squid-selfcheck-credential = { + description = "Read the squid self-check's password from the swarm secret store"; + wantedBy = [ "multi-user.target" ]; + before = [ "squid-selfcheck.service" ]; + wants = [ "network-online.target" ]; + after = [ + "network-online.target" + "systemd-tmpfiles-setup.service" + ]; + path = [ + baoDeploy.package + pkgs.coreutils + pkgs.gnugrep + ]; + environment = { + BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; + BAO_CLIENT_CERT = tls.baoHiveIdentityCertFile; + BAO_CLIENT_KEY = tls.baoHiveIdentityKeyFile; + } + // lib.optionalAttrs (baoDeploy.serverCaFile != null) { + BAO_CACERT = baoDeploy.serverCaFile; + }; + # ./lib/store-retry.nix: the login fails for as long as the store is + # sealed or down. + inherit (storeRetry) startLimitBurst startLimitIntervalSec; + serviceConfig = storeRetry.serviceConfig // { + Type = "oneshot"; + UMask = "0077"; + SyslogIdentifier = "squid-selfcheck-credential"; + TimeoutStartSec = "5min"; + }; + script = '' + set -euo pipefail + ${import ./lib/store-seeded-secret.nix { }} + ${import ./lib/atomic-write-secret.nix { }} + ${import ./lib/refresh-consumer.nix { }} + + install -d -m 0700 /run/credstore + store_init + store_login + if [ "$STORE_UP" != 1 ]; then + echo "could not read the squid self-check credential; ${credentialFile} stays as it is." >&2 + exit 1 + fi + if ! value="$(store_bao kv get -field=value ${lib.escapeShellArg "secret/swarm/hives/${hiveController.hiveName}/squid-selfcheck"} 2>"$STORE_ERR")"; then + if grep -qi 'no value found' "$STORE_ERR"; then + echo "the swarm has not minted this hive's squid self-check credential yet; ${credentialFile} stays as it is." + exit 0 + fi + cat "$STORE_ERR" >&2 + exit 1 + fi + if [[ ! "$value" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "the stored squid self-check credential is not one base64url word; ${credentialFile} stays as it is." >&2 + exit 1 + fi + # Written only when the value changed, so the file keeps its mtime. + if secret_differs ${credentialFile} "$value"; then + atomic_write_secret 0400 root:root ${credentialFile} "$value" + fi + ''; + }; + + systemd.timers.squid-selfcheck-credential = storeRefreshTimer "re-read the squid self-check's password"; + }; +} diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 4f66b668..b7f617e0 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -942,12 +942,13 @@ let inherit hiveName; }) hyperhiveCfg.swarm.hives; - # What a hive's gateway-leaf identity may do: issue from its own role, and - # read two of its own cache facts. The root unit on the hive host - # (`hive-priv-cache-facts`, ./hive-priv-cache.nix) reads them under this - # identity: the key hive-priv's downloads must be signed by, and the token it - # pulls with. The cache's URL and name come from the host's configuration, - # not from the store. + # What a hive's gateway-leaf identity may do: issue from its own role, read + # two of its own cache facts, and read its own squid self-check credential. + # The root unit on the hive host (`hive-priv-cache-facts`, + # ./hive-priv-cache.nix) reads the facts under this identity: the key + # hive-priv's downloads must be signed by, and the token it pulls with. The + # cache's URL and name come from the host's configuration, not from the + # store. `squid-selfcheck-credential` (./hive-squid.nix) reads the credential. hiveLeafPolicyText = obj: '' @@ -961,7 +962,8 @@ let [ "public-key" "pull-token" - ]; + ] + + readStanza "${credentialMountPath}/data/swarm/hives/${obj.hiveName}/squid-selfcheck"; # The PKI mount agent client certificates are issued from. Its root is # generated inside the store, so the agent CA's key never exists outside it. diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index 022a51c1..b6e48f1e 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -1496,9 +1496,10 @@ let { # One role, policy and login role per hive in the directory, each # naming only its own hive: the policy text in the unit is exactly the - # `update` on its own role and `read` on its own two cache facts, and each + # `update` on its own role and `read` on its own two cache facts and its own + # squid self-check credential, and each # hive's block names neither the other's role nor its subject. - name = "each hive's gateway-leaf identity may issue from its own role and read its own two cache facts, and nothing else"; + name = "each hive's gateway-leaf identity may issue from its own role and read its own two cache facts and squid self-check credential, and nothing else"; ok = let s = baoGrantTwoHives.systemd.services.swarm-bao-hive-leaf-policy.script; @@ -1536,6 +1537,10 @@ let path = "secret/data/swarm/hives/h1/attic/pull-token"; caps = [ "read" ]; } + { + path = "secret/data/swarm/hives/h1/squid-selfcheck"; + caps = [ "read" ]; + } { path = "pki/issue/swarm-hive-leaf-h2"; caps = [ "update" ]; @@ -1548,6 +1553,10 @@ let path = "secret/data/swarm/hives/h2/attic/pull-token"; caps = [ "read" ]; } + { + path = "secret/data/swarm/hives/h2/squid-selfcheck"; + caps = [ "read" ]; + } ] && issues "h1" "h1.t.local" h1 && issues "h2" "h2.example.org" h2 diff --git a/nix/module-eval/hive-tls.nix b/nix/module-eval/hive-tls.nix index 45f265c7..c59cbdbe 100644 --- a/nix/module-eval/hive-tls.nix +++ b/nix/module-eval/hive-tls.nix @@ -800,6 +800,28 @@ let noService.systemd.services.hive-priv-cache-facts.environment.BAO_CLIENT_CERT == "/etc/pki/bao-hive.pem"; } + { + # Control: the lookup finds the sibling root unit of the same shape. The + # squid self-check's password is read under this hive's own identity and + # lands where only root reads it, as the credential `squid-selfcheck` + # imports. + name = "the squid self-check credential is read by a root oneshot under this hive's own identity into the credstore"; + ok = + let + u = units.squid-selfcheck-credential; + in + lib.hasInfix "secret/swarm/hives/h1/squid-selfcheck" u.script + && !(lib.hasInfix "swarm/hives/h2" u.script) + && lib.hasInfix "/run/credstore/hive-squid.selfcheck" u.script + && lib.hasInfix "atomic_write_secret 0400 root:root" u.script + && u.environment.BAO_CLIENT_CERT == units.hive-priv-cache-facts.environment.BAO_CLIENT_CERT + && u.environment.BAO_CLIENT_KEY == units.hive-priv-cache-facts.environment.BAO_CLIENT_KEY + && u.serviceConfig.Type == "oneshot" + && !(remainsAfterExit u) + && allLocal.systemd.timers ? squid-selfcheck-credential + && !(noIdentity.systemd.services ? squid-selfcheck-credential) + && !(noIdentity.systemd.timers ? squid-selfcheck-credential); + } { name = "hive-priv is told the cache's address by the host's configuration"; ok =