diff --git a/docs/networking/network.md b/docs/networking/network.md index 4c889e70..cb0b2214 100644 --- a/docs/networking/network.md +++ b/docs/networking/network.md @@ -225,7 +225,7 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. credential (HTTP Basic, the agent's name and its secret). `swarm-controller` mints one per agent at `swarm/agents//proxy-credential`, and never replaces it. `hive-c0re` hashes the credentials of this hive's agents, and - one more for the self-check, into `services.hyperhive.squid.usersFile` + one more for the self-check, into `services.hyperhive.deploy.hive-controller.squid.usersFile` every 30 seconds. Squid's `basic_ncsa_auth` helper re-reads the file when it changes. A stopped agent stays in the file. Squid caches a good login for a minute. Nothing rotates a credential: to replace one, delete its leaf in the @@ -238,7 +238,7 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. - **Agents and the proxy.** An agent uses the proxy only when its hive says this host's squid is ready for it. `hive-c0re` puts two facts in every container's `/etc/hyperhive-hive-facts`, only on a hive that holds a store - identity: `HIVE_SQUID_PROXY` (`services.hyperhive.squid.listen`, the bridge IP + identity: `HIVE_SQUID_PROXY` (`services.hyperhive.deploy.hive-controller.squid.listen`, the bridge IP and port) and `HIVE_EGRESS_NO_PROXY` (the direct hosts below). A hive that predates them writes neither, and its agents go direct. `hive-agent-proxy-credential.service` (`nix/agent-modules/egress-proxy.nix`) @@ -272,7 +272,7 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. has no CIDR support, so the list holds the bridge as an address. - **Two lists.** The credential decides the policy. `hive-c0re` writes a - second file beside the users file, `services.hyperhive.squid.unrestrictedFile` + second file beside the users file, `services.hyperhive.deploy.hive-controller.squid.unrestrictedFile` (`/var/lib/hive-squid/unrestricted`, `hive-core:squid`, `0640`): the names of the users whose agent holds the `unrestricted_internet` flag in the permissions the hive last read from the swarm, one per line, rewritten when @@ -289,7 +289,7 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. restricted. Agents must not start sending credentials until every hive runs the `hive-c0re` that writes it, or a legacy agent loses general internet. -- **Base list.** `services.hyperhive.squid.baseAllowedDomains` is open to every +- **Base list.** `services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains` is open to every authenticated user, whatever its flag: what the agent harness needs off the swarm. The default is the hosts Claude Code requires (): `api.anthropic.com` for @@ -300,8 +300,8 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. agent's own swarm entries (see _Swarm allow-list_ below). The list omits the plugin marketplace clone from `github.com`, since a failed add isn't fatal. The rules below still refuse an entry that resolves to a private address. -- **Allow-list.** `services.hyperhive.squid.allowedDomains` defaults to empty, - plus the self-check canary (`services.hyperhive.squid.selfcheck.canary`), so +- **Allow-list.** `services.hyperhive.deploy.hive-controller.squid.allowedDomains` defaults to empty, + plus the self-check canary (`services.hyperhive.deploy.hive-controller.squid.selfcheck.canary`), so the canary widens the restricted list by that one name. The swarm's own service names aren't on it: they resolve to the bridge or a private address, which the rules below refuse, and agents reach them directly. Each entry @@ -313,8 +313,8 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. ([allowed domains](../swarm/ui.md#allowed-domains)); the swarm controller keeps them in one value of the `egress-allow` bucket, each for every agent or for one agent. Every 30 seconds `hive-c0re` reads it and writes two files - beside the users file, `services.hyperhive.squid.allowedFile` - (`/var/lib/hive-squid/allowed`) and `services.hyperhive.squid.agentAllowFile` + beside the users file, `services.hyperhive.deploy.hive-controller.squid.allowedFile` + (`/var/lib/hive-squid/allowed`) and `services.hyperhive.deploy.hive-controller.squid.agentAllowFile` (`/var/lib/hive-squid/agent-allow.conf`), `hive-core:squid`, `0640`, and logs `squid allow list: 2 swarm, 3 agent entries` when they change. The swarm-wide names join the allow ACL, so a restricted user reaches them. A @@ -387,12 +387,12 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. client's source address. - **Self-check.** `squid-selfcheck.timer` runs `squid-selfcheck.service` every minute from the bridge IP, logging in as `selfcheck.squid`: `CONNECT` - to `services.hyperhive.squid.selfcheck.canary` (default: `cache.nixos.org`, + to `services.hyperhive.deploy.hive-controller.squid.selfcheck.canary` (default: `cache.nixos.org`, a public name that's also on the allow-list) must answer 200, to `deny-canary.invalid` 403, and to the canary without a credential 407. It then logs in as `selfcheck-unrestricted.squid`, which has the same secret and is always on the unrestricted list, and a `CONNECT` to - `services.hyperhive.squid.selfcheck.unrestrictedCanary` (default: + `services.hyperhive.deploy.hive-controller.squid.selfcheck.unrestrictedCanary` (default: `example.com`, a name outside the allow-list) must answer 200. It writes one `squid-selfcheck ok=… allow_status=… deny_status=… anon_status=… credential=present|absent unrestricted_canary=… unrestricted_status=…` line diff --git a/docs/swarm/credentials-reference.md b/docs/swarm/credentials-reference.md index c0024fdc..1c384549 100644 --- a/docs/swarm/credentials-reference.md +++ b/docs/swarm/credentials-reference.md @@ -149,16 +149,16 @@ pieces fit, and the reasoning behind them, is in [`credentials.md`](credentials. ### Hive-level — one of each per hive -| secret | generated by | lives at | -| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| hive leaf certs | `hive-tls.nix`, issued by the secret store's `pki` mount | `/.pem` | -| the hive's own store identity | `glue-bao-tls.nix` on the store's host; placed by hand on every other host ([`ca.md`](ca.md#the-gateway-leaf-comes-from-the-secret-store)) | `deploy.hive-controller.tls.baoHiveIdentityCertFile` / `baoHiveIdentityKeyFile` | -| the cache's public key and pull token, for hive-priv | `swarm-controller` copies them to `swarm/hives//attic/{public-key,pull-token}` (see the cache-facts row above) | `hive-priv-cache-facts` (root) reads those two under the hive's own store identity every ten minutes and writes `/run/hyperhive-priv/attic/public-key` and `netrc` (tmpfs, root, each `0600`, directory `0700`); the cache's address comes from `services.hyperhive.swarm.attic.domain` and `cache`, not from the store. A store that holds no facts, or that doesn't answer, leaves the files as they stand, and a hive that never got them has `hive-priv` answer `not available`. | -| the forge's copy of its OIDC secret | `swarm-bao-forge-oidc.service` reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs | `/run/hive-forge-oidc/.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` | -| the homeserver's copy of its OIDC secret | `swarm-bao-matrix-oidc.service` reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs | `/run/hive-matrix-oidc/.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel through `LoadCredential` | -| this hive's forge read token | `swarm-controller`, into the secret store at `swarm/hives//forge/token`, for the `hive-` account; a pass at start and every five minutes re-mints a missing or stale token | `hive-c0re` reads it under the hive's own store identity, and copies it (account on the first line, token on the second) to `/run/hyperhive/forge-hive-token` (tmpfs, `0600` `hive-core`) for nix's `git`, which `hive-priv` also runs. Each `internal/meta` read and each boot rewrites the copy when it differs; where they differ after a rotation, a build that starts before the next read fails to authenticate, and a retry reads the new token | -| squid's users file | `hive-c0re` (`workers::squid_users`), every 30 seconds, from the proxy credentials of its agents and the self-check | `services.hyperhive.squid.usersFile` (`/var/lib/hive-squid/users`, `hive-core:squid`, `0640`): `:` lines, SHA-512 crypt hashes only, renamed into place when it changed. `unrestricted` beside it lists the users whose agent holds the swarm's `unrestricted_internet` flag, names only; it lists every agent the hive never read as off `systemd-tmpfiles` creates it empty so squid's helper never sees a missing file. A hive whose agents have no credential yet (an older controller) has an empty file: every login fails and the self-check reports `ok=false`; an agent without one sends none, so it goes direct | -| the squid self-check's password | `swarm-controller` mints `swarm/hives//squid-selfcheck`; `squid-selfcheck-credential` (root, every 10 minutes) reads it as the hive | `/run/credstore/hive-squid.selfcheck` (tmpfs, root, `0400`), imported by `squid-selfcheck.service` as a systemd credential and handed to `curl` on its stdin. Both self-check users (`selfcheck.squid` and the always-unrestricted `selfcheck-unrestricted.squid`) use it, so whoever holds it has unrestricted egress through this squid Missing, or different from the users file: the self-check logs `credential=absent` or a 407 and fails, and nothing else breaks | +| secret | generated by | lives at | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| hive leaf certs | `hive-tls.nix`, issued by the secret store's `pki` mount | `/.pem` | +| the hive's own store identity | `glue-bao-tls.nix` on the store's host; placed by hand on every other host ([`ca.md`](ca.md#the-gateway-leaf-comes-from-the-secret-store)) | `deploy.hive-controller.tls.baoHiveIdentityCertFile` / `baoHiveIdentityKeyFile` | +| the cache's public key and pull token, for hive-priv | `swarm-controller` copies them to `swarm/hives//attic/{public-key,pull-token}` (see the cache-facts row above) | `hive-priv-cache-facts` (root) reads those two under the hive's own store identity every ten minutes and writes `/run/hyperhive-priv/attic/public-key` and `netrc` (tmpfs, root, each `0600`, directory `0700`); the cache's address comes from `services.hyperhive.swarm.attic.domain` and `cache`, not from the store. A store that holds no facts, or that doesn't answer, leaves the files as they stand, and a hive that never got them has `hive-priv` answer `not available`. | +| the forge's copy of its OIDC secret | `swarm-bao-forge-oidc.service` reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs | `/run/hive-forge-oidc/.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` | +| the homeserver's copy of its OIDC secret | `swarm-bao-matrix-oidc.service` reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs | `/run/hive-matrix-oidc/.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel through `LoadCredential` | +| this hive's forge read token | `swarm-controller`, into the secret store at `swarm/hives//forge/token`, for the `hive-` account; a pass at start and every five minutes re-mints a missing or stale token | `hive-c0re` reads it under the hive's own store identity, and copies it (account on the first line, token on the second) to `/run/hyperhive/forge-hive-token` (tmpfs, `0600` `hive-core`) for nix's `git`, which `hive-priv` also runs. Each `internal/meta` read and each boot rewrites the copy when it differs; where they differ after a rotation, a build that starts before the next read fails to authenticate, and a retry reads the new token | +| squid's users file | `hive-c0re` (`workers::squid_users`), every 30 seconds, from the proxy credentials of its agents and the self-check | `services.hyperhive.deploy.hive-controller.squid.usersFile` (`/var/lib/hive-squid/users`, `hive-core:squid`, `0640`): `:` lines, SHA-512 crypt hashes only, renamed into place when it changed. `unrestricted` beside it lists the users whose agent holds the swarm's `unrestricted_internet` flag, names only; it lists every agent the hive never read as off `systemd-tmpfiles` creates it empty so squid's helper never sees a missing file. A hive whose agents have no credential yet (an older controller) has an empty file: every login fails and the self-check reports `ok=false`; an agent without one sends none, so it goes direct | +| the squid self-check's password | `swarm-controller` mints `swarm/hives//squid-selfcheck`; `squid-selfcheck-credential` (root, every 10 minutes) reads it as the hive | `/run/credstore/hive-squid.selfcheck` (tmpfs, root, `0400`), imported by `squid-selfcheck.service` as a systemd credential and handed to `curl` on its stdin. Both self-check users (`selfcheck.squid` and the always-unrestricted `selfcheck-unrestricted.squid`) use it, so whoever holds it has unrestricted egress through this squid Missing, or different from the users file: the self-check logs `credential=absent` or a 407 and fails, and nothing else breaks | ### Store identities, one per principal diff --git a/hive-c0re/src/workers/squid_allow.rs b/hive-c0re/src/workers/squid_allow.rs index 915be187..4e97cdf9 100644 --- a/hive-c0re/src/workers/squid_allow.rs +++ b/hive-c0re/src/workers/squid_allow.rs @@ -15,7 +15,7 @@ //! Squid reads both when it parses its configuration, so the path unit in //! `nix/host-modules/hive-squid.nix` reloads it when either changes. //! -//! The names `services.hyperhive.squid.allowedDomains` lists ([`ENV_SEED_FILE`]) +//! The names `services.hyperhive.deploy.hive-controller.squid.allowedDomains` lists ([`ENV_SEED_FILE`]) //! are always allowed: the swarm-wide names written are those plus the swarm's, //! with the per-agent rules on top. Until the swarm has a list the files hold //! just those names. A file from the swarm's list starts with [`SWARM_MARKER`]; @@ -46,14 +46,14 @@ pub const INTERVAL: Duration = Duration::from_secs(30); const FAILURES_BEFORE_WARNING: u32 = 3; /// The swarm-wide names file. Set by `nix/host-modules/hive-c0re` from -/// `services.hyperhive.squid.allowedFile`. +/// `services.hyperhive.deploy.hive-controller.squid.allowedFile`. pub const ENV_ALLOWED_FILE: &str = "HIVE_SQUID_ALLOWED_FILE"; /// The per-agent squid configuration file. Set from -/// `services.hyperhive.squid.agentAllowFile`. +/// `services.hyperhive.deploy.hive-controller.squid.agentAllowFile`. pub const ENV_AGENT_ALLOW_FILE: &str = "HIVE_SQUID_AGENT_ALLOW_FILE"; -/// The file of names `services.hyperhive.squid.allowedDomains` lists, one per +/// The file of names `services.hyperhive.deploy.hive-controller.squid.allowedDomains` lists, one per /// line, which every rendering of the swarm-wide names includes. pub const ENV_SEED_FILE: &str = "HIVE_SQUID_ALLOWED_SEED_FILE"; diff --git a/hive-c0re/src/workers/squid_users.rs b/hive-c0re/src/workers/squid_users.rs index 17f1f250..585833b3 100644 --- a/hive-c0re/src/workers/squid_users.rs +++ b/hive-c0re/src/workers/squid_users.rs @@ -42,11 +42,11 @@ pub const INTERVAL: Duration = Duration::from_secs(30); const FAILURES_BEFORE_WARNING: u32 = 3; /// The environment variable naming the file. Set by `nix/host-modules/hive-c0re` -/// from `services.hyperhive.squid.usersFile`; unset means this host runs no squid. +/// from `services.hyperhive.deploy.hive-controller.squid.usersFile`; unset means this host runs no squid. pub const ENV_USERS_FILE: &str = "HIVE_SQUID_USERS_FILE"; /// The environment variable naming the unrestricted-users list. Set by -/// `nix/host-modules/hive-c0re` from `services.hyperhive.squid.unrestrictedFile`. +/// `nix/host-modules/hive-c0re` from `services.hyperhive.deploy.hive-controller.squid.unrestrictedFile`. pub const ENV_UNRESTRICTED_FILE: &str = "HIVE_SQUID_UNRESTRICTED_FILE"; /// Raw salt bytes: twelve of them encode to the sixteen characters `crypt()` diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 6c583347..fac8a867 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -254,6 +254,45 @@ in [ "services" "hyperhive" "c0re" "adminUsers" ] [ "services" "hyperhive" "deploy" "hive-controller" "adminUsers" ] ) + + # The egress proxy runs only on a hive host, so its settings sit with the + # controller's. The read-only ones moved without a forward. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "squid" "allowedDomains" ] + [ "services" "hyperhive" "deploy" "hive-controller" "squid" "allowedDomains" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "squid" "baseAllowedDomains" ] + [ "services" "hyperhive" "deploy" "hive-controller" "squid" "baseAllowedDomains" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "squid" "usersFile" ] + [ "services" "hyperhive" "deploy" "hive-controller" "squid" "usersFile" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "squid" "selfcheck" "unrestrictedCanary" ] + [ + "services" + "hyperhive" + "deploy" + "hive-controller" + "squid" + "selfcheck" + "unrestrictedCanary" + ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "squid" "selfcheck" "canary" ] + [ + "services" + "hyperhive" + "deploy" + "hive-controller" + "squid" + "selfcheck" + "canary" + ] + ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "otel" "extraResourceAttributes" ] [ "services" "hyperhive" "deploy" "hive-controller" "otel" "extraResourceAttributes" ] diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 8ee272a6..522e286d 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -349,7 +349,7 @@ in # `-` so a host without the squid users directory still starts the daemon. ReadWritePaths = [ "/var/lib/hive-gateway/conf" - "-${builtins.dirOf config.services.hyperhive.squid.usersFile}" + "-${builtins.dirOf config.services.hyperhive.deploy.hive-controller.squid.usersFile}" ]; ProtectKernelTunables = true; # no sysctl writes ProtectKernelLogs = true; # no /dev/kmsg access needed diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 692e1ba7..8f395357 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -290,21 +290,23 @@ in # credentials for squid, and the list of the users whose agent holds the # swarm's `unrestricted_internet` flag; it reads the credentials from the # store under the identity above. - HIVE_SQUID_USERS_FILE = config.services.hyperhive.squid.usersFile; - HIVE_SQUID_UNRESTRICTED_FILE = config.services.hyperhive.squid.unrestrictedFile; + HIVE_SQUID_USERS_FILE = config.services.hyperhive.deploy.hive-controller.squid.usersFile; + HIVE_SQUID_UNRESTRICTED_FILE = + config.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile; # The swarm's egress allow-list, which `workers::squid_allow` renders into # the two files; the seed is the list the files hold until the swarm has one, # and the always-allowed names are those it never repeats. - HIVE_SQUID_ALLOWED_FILE = config.services.hyperhive.squid.allowedFile; - HIVE_SQUID_AGENT_ALLOW_FILE = config.services.hyperhive.squid.agentAllowFile; - HIVE_SQUID_ALLOWED_SEED_FILE = config.services.hyperhive.squid.allowedSeedFile; - HIVE_SQUID_ALWAYS_ALLOWED = lib.concatStringsSep " " config.services.hyperhive.squid.alwaysAllowed; + HIVE_SQUID_ALLOWED_FILE = config.services.hyperhive.deploy.hive-controller.squid.allowedFile; + HIVE_SQUID_AGENT_ALLOW_FILE = config.services.hyperhive.deploy.hive-controller.squid.agentAllowFile; + HIVE_SQUID_ALLOWED_SEED_FILE = + config.services.hyperhive.deploy.hive-controller.squid.allowedSeedFile; + HIVE_SQUID_ALWAYS_ALLOWED = lib.concatStringsSep " " config.services.hyperhive.deploy.hive-controller.squid.alwaysAllowed; # The egress proxy facts every container is handed (`HiveFacts` in # `hive-priv-sock`). They sit with the users file above because that is what # makes squid's credential check and per-agent policy real on this host: an # agent sends nothing through the proxy unless it reads both. Absent on a # hive that predates them, so its agents go direct. - HIVE_SQUID_PROXY = config.services.hyperhive.squid.listen; + HIVE_SQUID_PROXY = config.services.hyperhive.deploy.hive-controller.squid.listen; HIVE_EGRESS_NO_PROXY = lib.concatStringsSep "," egressDirectHosts; } // lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) { diff --git a/nix/host-modules/hive-squid-credential.nix b/nix/host-modules/hive-squid-credential.nix index 47be3e2a..36587477 100644 --- a/nix/host-modules/hive-squid-credential.nix +++ b/nix/host-modules/hive-squid-credential.nix @@ -28,7 +28,7 @@ let && tls.baoHiveIdentityCertFile != null && tls.baoHiveIdentityKeyFile != null; - credentialFile = "/run/credstore/${hyperhiveCfg.squid.selfcheck.credentialName}"; + credentialFile = "/run/credstore/${hyperhiveCfg.deploy.hive-controller.squid.selfcheck.credentialName}"; storeRetry = import ./lib/store-retry.nix { }; storeRefreshTimer = import ./lib/store-refresh-timer.nix { }; diff --git a/nix/host-modules/hive-squid.nix b/nix/host-modules/hive-squid.nix index 3909a9fe..6631b879 100644 --- a/nix/host-modules/hive-squid.nix +++ b/nix/host-modules/hive-squid.nix @@ -18,7 +18,7 @@ }: let hyperhiveCfg = config.services.hyperhive; - cfg = hyperhiveCfg.squid; + cfg = hyperhiveCfg.deploy.hive-controller.squid; netCfg = hyperhiveCfg.network; port = 3128; @@ -232,7 +232,7 @@ let }; in { - options.services.hyperhive.squid = { + options.services.hyperhive.deploy.hive-controller.squid = { listen = lib.mkOption { type = lib.types.str; default = listen; @@ -492,7 +492,7 @@ in { assertion = !(builtins.elem cfg.selfcheck.unrestrictedCanary (allowed ++ base)); message = '' - services.hyperhive.squid.selfcheck.unrestrictedCanary must be a name outside + services.hyperhive.deploy.hive-controller.squid.selfcheck.unrestrictedCanary must be a name outside allowedDomains and baseAllowedDomains, or the self-check cannot tell an unrestricted user from a restricted one. ''; diff --git a/nix/host-modules/lib/squid-base-domains.nix b/nix/host-modules/lib/squid-base-domains.nix index b63e4f5b..8eb4240e 100644 --- a/nix/host-modules/lib/squid-base-domains.nix +++ b/nix/host-modules/lib/squid-base-domains.nix @@ -1,4 +1,4 @@ -# The default of `services.hyperhive.squid.baseAllowedDomains`, and the names +# The default of `services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains`, and the names # swarm-controller refuses as allow-list entries because squid already allows # them to every authenticated user: the hosts Claude Code requires # (). diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index a2636047..374363da 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -867,8 +867,8 @@ in # The names squid allows every authenticated agent by default. The # allow-list route refuses them as entries # (swarm-controller/src/egress_allow.rs). A hive that overrides - # `squid.baseAllowedDomains` is not reflected here; its hive-c0re - # skips the entries its own list covers. + # `deploy.hive-controller.squid.baseAllowedDomains` is not reflected + # here; its hive-c0re skips the entries its own list covers. SWARM_CONTROLLER_SQUID_BASE_DOMAINS = lib.concatStringsSep " " ( import ./lib/squid-base-domains.nix ); diff --git a/nix/module-eval/bao-controller.nix b/nix/module-eval/bao-controller.nix index f32fbf8a..893f30b7 100644 --- a/nix/module-eval/bao-controller.nix +++ b/nix/module-eval/bao-controller.nix @@ -256,7 +256,7 @@ let name = "the controller is handed the base list squid defaults to"; ok = let - base = baoControllerHere.services.hyperhive.squid.baseAllowedDomains; + base = baoControllerHere.services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains; in base != [ ] && diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 44eff677..68415294 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -1621,6 +1621,73 @@ let && (locs h.swarm.forge.domain)."/".proxyPass == "http://127.0.0.1:3000/" && !allLocal.containers.hive-matrix.config.services.matrix-tuwunel.settings.global.allow_encryption; } + { + # Each old spelling lands on the new path, with its own rename warning; + # the squid.conf it renders equals the one the new spelling renders, and + # differs from the default's (the control that the values reach it). + name = "services.hyperhive.squid.* settings still apply, with a rename warning each"; + ok = + let + old = hiveWithController { + squid = { + allowedDomains = [ "legacy.example" ]; + baseAllowedDomains = [ "base.example" ]; + usersFile = "/var/lib/legacy-squid/users"; + selfcheck.unrestrictedCanary = "legacy-u.example"; + selfcheck.canary = "legacy-c.example"; + }; + }; + new = hiveWithController { + deploy.hive-controller.squid = { + allowedDomains = [ "legacy.example" ]; + baseAllowedDomains = [ "base.example" ]; + usersFile = "/var/lib/legacy-squid/users"; + selfcheck.unrestrictedCanary = "legacy-u.example"; + selfcheck.canary = "legacy-c.example"; + }; + }; + sq = old.services.hyperhive.deploy.hive-controller.squid; + warned = + m: path: + lib.any ( + w: lib.hasInfix "services.hyperhive.squid.${path}" w && lib.hasInfix "has been renamed" w + ) m.warnings; + in + sq.allowedDomains == [ "legacy.example" ] + && sq.baseAllowedDomains == [ "base.example" ] + && sq.usersFile == "/var/lib/legacy-squid/users" + && sq.allowedFile == "/var/lib/legacy-squid/allowed" + && sq.selfcheck.unrestrictedCanary == "legacy-u.example" + && sq.selfcheck.canary == "legacy-c.example" + && lib.all (warned old) [ + "allowedDomains" + "baseAllowedDomains" + "usersFile" + "selfcheck.unrestrictedCanary" + "selfcheck.canary" + ] + && !(warned bare "allowedDomains") + && old.systemd.services.hive-c0re.environment.HIVE_SQUID_USERS_FILE == "/var/lib/legacy-squid/users" + && old.services.squid.configText == new.services.squid.configText + && lib.hasInfix "legacy-c.example" old.services.squid.configText + && !(lib.hasInfix "legacy-c.example" bare.services.squid.configText); + } + { + # The default render, pinned so moving the options changes none of it: + # the listen address squid binds and hive-c0re hands out, and the users + # file hive-c0re writes. + name = "squid's default listen address and users file reach squid.conf and hive-c0re unchanged"; + ok = + let + e = bare.systemd.services.hive-c0re.environment; + conf = bare.services.squid.configText; + in + e.HIVE_SQUID_PROXY == "${bare.services.hyperhive.network.bridgeIp}:3128" + && e.HIVE_SQUID_USERS_FILE == "/var/lib/hive-squid/users" + && lib.hasInfix "http_port ${e.HIVE_SQUID_PROXY}" conf + && lib.hasInfix "basic_ncsa_auth /var/lib/hive-squid/users" conf + && lib.hasInfix "acl allowed_dst dstdomain cache.nixos.org" conf; + } { # A hive's rendered identity, pinned so moving the option changes none of # it: the coordinator's name and client, the dashboard name, the CN of the diff --git a/nix/module-eval/hive-otel.nix b/nix/module-eval/hive-otel.nix index b085d702..1b4d8e69 100644 --- a/nix/module-eval/hive-otel.nix +++ b/nix/module-eval/hive-otel.nix @@ -507,7 +507,7 @@ let unrestricted = at "http_access allow unrestricted"; authed = at "http_access deny !authed"; # The generated per-agent `http_access allow` rules sit in this file. - agentRules = at "include ${hiveOtel.services.hyperhive.squid.agentAllowFile}"; + agentRules = at "include ${hiveOtel.services.hyperhive.deploy.hive-controller.squid.agentAllowFile}"; firstAllow = lib.min agentRules (lib.min base (lib.min unrestricted allow)); in allow != null @@ -550,9 +550,10 @@ let acl = lib.findFirst (l: lib.hasPrefix "acl allowed_dst dstdomain " l) "" (map lib.trim lines); baseAcl = lib.findFirst (l: lib.hasPrefix "acl base_dst dstdomain " l) "" (map lib.trim lines); in - acl == "acl allowed_dst dstdomain ${hiveOtel.services.hyperhive.squid.selfcheck.canary}" + acl + == "acl allowed_dst dstdomain ${hiveOtel.services.hyperhive.deploy.hive-controller.squid.selfcheck.canary}" && baseAcl == "acl base_dst dstdomain api.anthropic.com claude.ai platform.claude.com" - && hiveOtel.services.hyperhive.squid.selfcheck.canary == "cache.nixos.org" + && hiveOtel.services.hyperhive.deploy.hive-controller.squid.selfcheck.canary == "cache.nixos.org" && !(builtins.elem hiveOtel.services.hyperhive.swarm.forge.domain (lib.splitString " " acl)); } { @@ -592,7 +593,7 @@ let conf = hiveOtel.services.squid.configText; lines = lib.splitString "\n" conf; at = l: lib.lists.findFirstIndex (x: x == l) null lines; - list = hiveOtel.services.hyperhive.squid.unrestrictedFile; + list = hiveOtel.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile; in at "http_access allow unrestricted" != null && at "http_access deny !allowed_dst" != null @@ -609,15 +610,15 @@ let name = "squid is reloaded when its unrestricted-users list changes"; ok = let - list = hiveOtel.services.hyperhive.squid.unrestrictedFile; + list = hiveOtel.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile; rules = hiveOtel.systemd.tmpfiles.rules; path = hiveOtel.systemd.paths.squid-unrestricted-reload; svc = hiveOtel.systemd.services.squid-unrestricted-reload.serviceConfig; in path.pathConfig.PathChanged == [ list - hiveOtel.services.hyperhive.squid.allowedFile - hiveOtel.services.hyperhive.squid.agentAllowFile + hiveOtel.services.hyperhive.deploy.hive-controller.squid.allowedFile + hiveOtel.services.hyperhive.deploy.hive-controller.squid.agentAllowFile ] && hiveOtel.systemd.services.squid-unrestricted-reload.startLimitIntervalSec == 0 && builtins.elem "paths.target" path.wantedBy @@ -635,7 +636,7 @@ let name = "squid reads the swarm's allow-list from a file seeded with the hive's own names"; ok = let - sq = hiveOtelAllLocal.services.hyperhive.squid; + sq = hiveOtelAllLocal.services.hyperhive.deploy.hive-controller.squid; lines = lib.splitString "\n" hiveOtelAllLocal.services.squid.configText; rules = hiveOtelAllLocal.systemd.tmpfiles.rules; env = hiveOtelAllLocal.systemd.services.hive-c0re.environment; @@ -675,7 +676,7 @@ let let conf = hiveOtel.services.squid.configText; lines = lib.splitString "\n" conf; - users = hiveOtel.services.hyperhive.squid.usersFile; + users = hiveOtel.services.hyperhive.deploy.hive-controller.squid.usersFile; rules = hiveOtel.systemd.tmpfiles.rules; sc = hiveOtel.systemd.services.squid-selfcheck.serviceConfig; in @@ -686,7 +687,9 @@ let && users == "/var/lib/hive-squid/users" && builtins.elem "d /var/lib/hive-squid 2750 hive-core squid - -" rules && builtins.elem "f ${users} 0640 hive-core squid - -" rules - && sc.ImportCredential == hiveOtel.services.hyperhive.squid.selfcheck.credentialName + && + sc.ImportCredential + == hiveOtel.services.hyperhive.deploy.hive-controller.squid.selfcheck.credentialName && hiveOtelAllLocal.systemd.services.hive-c0re.environment.HIVE_SQUID_USERS_FILE == users; } { diff --git a/nix/vm-tests/hive-squid.nix b/nix/vm-tests/hive-squid.nix index f0f71599..adc0cfda 100644 --- a/nix/vm-tests/hive-squid.nix +++ b/nix/vm-tests/hive-squid.nix @@ -175,13 +175,13 @@ pkgs.testers.runNixOSTest { networking.firewall.interfaces.eth1.allowedTCPPorts = [ 443 ]; # Renders the users file the way hive-c0re does. environment.systemPackages = [ pkgs.mkpasswd ]; - services.hyperhive.squid.selfcheck.unrestrictedCanary = "other.t.local"; - services.hyperhive.squid.selfcheck.canary = "cache.t.local"; - services.hyperhive.squid.baseAllowedDomains = [ + services.hyperhive.deploy.hive-controller.squid.selfcheck.unrestrictedCanary = "other.t.local"; + services.hyperhive.deploy.hive-controller.squid.selfcheck.canary = "cache.t.local"; + services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains = [ "api.t.local" "basebridge.t.local" ]; - services.hyperhive.squid.allowedDomains = [ + services.hyperhive.deploy.hive-controller.squid.allowedDomains = [ "forge.t.local" "loop.t.local" "bridge.t.local" diff --git a/swarm-controller/src/egress_allow.rs b/swarm-controller/src/egress_allow.rs index 9d16b9ad..00859e21 100644 --- a/swarm-controller/src/egress_allow.rs +++ b/swarm-controller/src/egress_allow.rs @@ -6,7 +6,7 @@ //! reads it and renders the entries for the agents it runs: a swarm-wide entry //! joins the proxy's allow list for everyone, an entry with an agent is that //! agent's alone. A hive always allows the names its own nix configuration -//! lists (`services.hyperhive.squid.allowedDomains`) as well, so these entries +//! lists (`services.hyperhive.deploy.hive-controller.squid.allowedDomains`) as well, so these entries //! only add to them. //! //! Each name goes into the proxy's configuration, so it is checked here with