Watch
0
0
Fork
You've already forked hyperhive
0

move the squid host options to deploy.hive-controller.squid

This commit is contained in:
atlas 2026-10-29 01:26:02 +01:00 • committed by mara
commit da16487800
16 changed files with 168 additions and 57 deletions

View file

@ -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` credential (HTTP Basic, the agent's name and its secret). `swarm-controller`
mints one per agent at `swarm/agents/<agent>/proxy-credential`, and never mints one per agent at `swarm/agents/<agent>/proxy-credential`, and never
replaces it. `hive-c0re` hashes the credentials of this hive's agents, and 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 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 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 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 - **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 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 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 and port) and `HIVE_EGRESS_NO_PROXY` (the direct hosts below). A hive that
predates them writes neither, and its agents go direct. predates them writes neither, and its agents go direct.
`hive-agent-proxy-credential.service` (`nix/agent-modules/egress-proxy.nix`) `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. has no CIDR support, so the list holds the bridge as an address.
- **Two lists.** The credential decides the policy. `hive-c0re` writes a - **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 (`/var/lib/hive-squid/unrestricted`, `hive-core:squid`, `0640`): the names of
the users whose agent holds the `unrestricted_internet` flag in the the users whose agent holds the `unrestricted_internet` flag in the
permissions the hive last read from the swarm, one per line, rewritten when 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 restricted. Agents must not start sending credentials until every hive runs
the `hive-c0re` that writes it, or a legacy agent loses general internet. 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 authenticated user, whatever its flag: what the agent harness needs off the
swarm. The default is the hosts Claude Code requires swarm. The default is the hosts Claude Code requires
(<https://code.claude.com/docs/en/network-config>): `api.anthropic.com` for (<https://code.claude.com/docs/en/network-config>): `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 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 isn't fatal. The rules below still refuse an entry that resolves to a
private address. private address.
- **Allow-list.** `services.hyperhive.squid.allowedDomains` defaults to empty, - **Allow-list.** `services.hyperhive.deploy.hive-controller.squid.allowedDomains` defaults to empty,
plus the self-check canary (`services.hyperhive.squid.selfcheck.canary`), so 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 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, 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 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 ([allowed domains](../swarm/ui.md#allowed-domains)); the swarm controller
keeps them in one value of the `egress-allow` bucket, each for every agent 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 or for one agent. Every 30 seconds `hive-c0re` reads it and writes two files
beside the users file, `services.hyperhive.squid.allowedFile` beside the users file, `services.hyperhive.deploy.hive-controller.squid.allowedFile`
(`/var/lib/hive-squid/allowed`) and `services.hyperhive.squid.agentAllowFile` (`/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 (`/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 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 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. client's source address.
- **Self-check.** `squid-selfcheck.timer` runs `squid-selfcheck.service` - **Self-check.** `squid-selfcheck.timer` runs `squid-selfcheck.service`
every minute from the bridge IP, logging in as `selfcheck.squid`: `CONNECT` 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) a public name that's also on the allow-list)
must answer 200, to `deny-canary.invalid` 403, and to the canary without a 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 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 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 `example.com`, a name outside the allow-list) must answer 200. It writes one
`squid-selfcheck ok=… allow_status=… deny_status=… anon_status=… `squid-selfcheck ok=… allow_status=… deny_status=… anon_status=…
credential=present|absent unrestricted_canary=… unrestricted_status=…` line credential=present|absent unrestricted_canary=… unrestricted_status=…` line

View file

@ -149,16 +149,16 @@ pieces fit, and the reasoning behind them, is in [`credentials.md`](credentials.
### Hive-level — one of each per hive ### Hive-level — one of each per hive
| secret | generated by | lives at | | secret | generated by | lives at |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| hive leaf certs | `hive-tls.nix`, issued by the secret store's `pki` mount | `<deploy.hive-controller.tls.stateDir>/<name>.pem` | | hive leaf certs | `hive-tls.nix`, issued by the secret store's `pki` mount | `<deploy.hive-controller.tls.stateDir>/<name>.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 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/<hive>/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 cache's public key and pull token, for hive-priv | `swarm-controller` copies them to `swarm/hives/<hive>/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/<id>.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` | | 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/<id>.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/<id>.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel 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/<id>.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/<hive>/forge/token`, for the `hive-<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 | | this hive's forge read token | `swarm-controller`, into the secret store at `swarm/hives/<hive>/forge/token`, for the `hive-<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`): `<user>:<hash>` 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 | | 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`): `<user>:<hash>` 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/<hive>/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 | | the squid self-check's password | `swarm-controller` mints `swarm/hives/<hive>/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 ### Store identities, one per principal

View file

@ -15,7 +15,7 @@
//! Squid reads both when it parses its configuration, so the path unit in //! Squid reads both when it parses its configuration, so the path unit in
//! `nix/host-modules/hive-squid.nix` reloads it when either changes. //! `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, //! 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 //! 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`]; //! 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; const FAILURES_BEFORE_WARNING: u32 = 3;
/// The swarm-wide names file. Set by `nix/host-modules/hive-c0re` from /// 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"; pub const ENV_ALLOWED_FILE: &str = "HIVE_SQUID_ALLOWED_FILE";
/// The per-agent squid configuration file. Set from /// 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"; 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. /// line, which every rendering of the swarm-wide names includes.
pub const ENV_SEED_FILE: &str = "HIVE_SQUID_ALLOWED_SEED_FILE"; pub const ENV_SEED_FILE: &str = "HIVE_SQUID_ALLOWED_SEED_FILE";

View file

@ -42,11 +42,11 @@ pub const INTERVAL: Duration = Duration::from_secs(30);
const FAILURES_BEFORE_WARNING: u32 = 3; const FAILURES_BEFORE_WARNING: u32 = 3;
/// The environment variable naming the file. Set by `nix/host-modules/hive-c0re` /// 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"; pub const ENV_USERS_FILE: &str = "HIVE_SQUID_USERS_FILE";
/// The environment variable naming the unrestricted-users list. Set by /// 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"; pub const ENV_UNRESTRICTED_FILE: &str = "HIVE_SQUID_UNRESTRICTED_FILE";
/// Raw salt bytes: twelve of them encode to the sixteen characters `crypt()` /// Raw salt bytes: twelve of them encode to the sixteen characters `crypt()`

View file

@ -254,6 +254,45 @@ in
[ "services" "hyperhive" "c0re" "adminUsers" ] [ "services" "hyperhive" "c0re" "adminUsers" ]
[ "services" "hyperhive" "deploy" "hive-controller" "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 (lib.mkRenamedOptionModule
[ "services" "hyperhive" "otel" "extraResourceAttributes" ] [ "services" "hyperhive" "otel" "extraResourceAttributes" ]
[ "services" "hyperhive" "deploy" "hive-controller" "otel" "extraResourceAttributes" ] [ "services" "hyperhive" "deploy" "hive-controller" "otel" "extraResourceAttributes" ]

View file

@ -349,7 +349,7 @@ in
# `-` so a host without the squid users directory still starts the daemon. # `-` so a host without the squid users directory still starts the daemon.
ReadWritePaths = [ ReadWritePaths = [
"/var/lib/hive-gateway/conf" "/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 ProtectKernelTunables = true; # no sysctl writes
ProtectKernelLogs = true; # no /dev/kmsg access needed ProtectKernelLogs = true; # no /dev/kmsg access needed

View file

@ -290,21 +290,23 @@ in
# credentials for squid, and the list of the users whose agent holds the # credentials for squid, and the list of the users whose agent holds the
# swarm's `unrestricted_internet` flag; it reads the credentials from the # swarm's `unrestricted_internet` flag; it reads the credentials from the
# store under the identity above. # store under the identity above.
HIVE_SQUID_USERS_FILE = config.services.hyperhive.squid.usersFile; HIVE_SQUID_USERS_FILE = config.services.hyperhive.deploy.hive-controller.squid.usersFile;
HIVE_SQUID_UNRESTRICTED_FILE = config.services.hyperhive.squid.unrestrictedFile; 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 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, # 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. # and the always-allowed names are those it never repeats.
HIVE_SQUID_ALLOWED_FILE = config.services.hyperhive.squid.allowedFile; HIVE_SQUID_ALLOWED_FILE = config.services.hyperhive.deploy.hive-controller.squid.allowedFile;
HIVE_SQUID_AGENT_ALLOW_FILE = config.services.hyperhive.squid.agentAllowFile; HIVE_SQUID_AGENT_ALLOW_FILE = config.services.hyperhive.deploy.hive-controller.squid.agentAllowFile;
HIVE_SQUID_ALLOWED_SEED_FILE = config.services.hyperhive.squid.allowedSeedFile; HIVE_SQUID_ALLOWED_SEED_FILE =
HIVE_SQUID_ALWAYS_ALLOWED = lib.concatStringsSep " " config.services.hyperhive.squid.alwaysAllowed; 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 # The egress proxy facts every container is handed (`HiveFacts` in
# `hive-priv-sock`). They sit with the users file above because that is what # `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 # 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 # agent sends nothing through the proxy unless it reads both. Absent on a
# hive that predates them, so its agents go direct. # 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; HIVE_EGRESS_NO_PROXY = lib.concatStringsSep "," egressDirectHosts;
} }
// lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) { // lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) {

View file

@ -28,7 +28,7 @@ let
&& tls.baoHiveIdentityCertFile != null && tls.baoHiveIdentityCertFile != null
&& tls.baoHiveIdentityKeyFile != 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 { }; storeRetry = import ./lib/store-retry.nix { };
storeRefreshTimer = import ./lib/store-refresh-timer.nix { }; storeRefreshTimer = import ./lib/store-refresh-timer.nix { };

View file

@ -18,7 +18,7 @@
}: }:
let let
hyperhiveCfg = config.services.hyperhive; hyperhiveCfg = config.services.hyperhive;
cfg = hyperhiveCfg.squid; cfg = hyperhiveCfg.deploy.hive-controller.squid;
netCfg = hyperhiveCfg.network; netCfg = hyperhiveCfg.network;
port = 3128; port = 3128;
@ -232,7 +232,7 @@ let
}; };
in in
{ {
options.services.hyperhive.squid = { options.services.hyperhive.deploy.hive-controller.squid = {
listen = lib.mkOption { listen = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = listen; default = listen;
@ -492,7 +492,7 @@ in
{ {
assertion = !(builtins.elem cfg.selfcheck.unrestrictedCanary (allowed ++ base)); assertion = !(builtins.elem cfg.selfcheck.unrestrictedCanary (allowed ++ base));
message = '' 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 allowedDomains and baseAllowedDomains, or the self-check cannot tell an unrestricted user from a
restricted one. restricted one.
''; '';

View file

@ -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 # swarm-controller refuses as allow-list entries because squid already allows
# them to every authenticated user: the hosts Claude Code requires # them to every authenticated user: the hosts Claude Code requires
# (<https://code.claude.com/docs/en/network-config>). # (<https://code.claude.com/docs/en/network-config>).

View file

@ -867,8 +867,8 @@ in
# The names squid allows every authenticated agent by default. The # The names squid allows every authenticated agent by default. The
# allow-list route refuses them as entries # allow-list route refuses them as entries
# (swarm-controller/src/egress_allow.rs). A hive that overrides # (swarm-controller/src/egress_allow.rs). A hive that overrides
# `squid.baseAllowedDomains` is not reflected here; its hive-c0re # `deploy.hive-controller.squid.baseAllowedDomains` is not reflected
# skips the entries its own list covers. # here; its hive-c0re skips the entries its own list covers.
SWARM_CONTROLLER_SQUID_BASE_DOMAINS = lib.concatStringsSep " " ( SWARM_CONTROLLER_SQUID_BASE_DOMAINS = lib.concatStringsSep " " (
import ./lib/squid-base-domains.nix import ./lib/squid-base-domains.nix
); );

View file

@ -256,7 +256,7 @@ let
name = "the controller is handed the base list squid defaults to"; name = "the controller is handed the base list squid defaults to";
ok = ok =
let let
base = baoControllerHere.services.hyperhive.squid.baseAllowedDomains; base = baoControllerHere.services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains;
in in
base != [ ] base != [ ]
&& &&

View file

@ -1621,6 +1621,73 @@ let
&& (locs h.swarm.forge.domain)."/".proxyPass == "http://127.0.0.1:3000/" && (locs h.swarm.forge.domain)."/".proxyPass == "http://127.0.0.1:3000/"
&& !allLocal.containers.hive-matrix.config.services.matrix-tuwunel.settings.global.allow_encryption; && !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 # 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 # it: the coordinator's name and client, the dashboard name, the CN of the

View file

@ -507,7 +507,7 @@ let
unrestricted = at "http_access allow unrestricted"; unrestricted = at "http_access allow unrestricted";
authed = at "http_access deny !authed"; authed = at "http_access deny !authed";
# The generated per-agent `http_access allow` rules sit in this file. # 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)); firstAllow = lib.min agentRules (lib.min base (lib.min unrestricted allow));
in in
allow != null allow != null
@ -550,9 +550,10 @@ let
acl = lib.findFirst (l: lib.hasPrefix "acl allowed_dst dstdomain " l) "" (map lib.trim lines); 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); baseAcl = lib.findFirst (l: lib.hasPrefix "acl base_dst dstdomain " l) "" (map lib.trim lines);
in 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" && 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)); && !(builtins.elem hiveOtel.services.hyperhive.swarm.forge.domain (lib.splitString " " acl));
} }
{ {
@ -592,7 +593,7 @@ let
conf = hiveOtel.services.squid.configText; conf = hiveOtel.services.squid.configText;
lines = lib.splitString "\n" conf; lines = lib.splitString "\n" conf;
at = l: lib.lists.findFirstIndex (x: x == l) null lines; 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 in
at "http_access allow unrestricted" != null at "http_access allow unrestricted" != null
&& at "http_access deny !allowed_dst" != null && at "http_access deny !allowed_dst" != null
@ -609,15 +610,15 @@ let
name = "squid is reloaded when its unrestricted-users list changes"; name = "squid is reloaded when its unrestricted-users list changes";
ok = ok =
let let
list = hiveOtel.services.hyperhive.squid.unrestrictedFile; list = hiveOtel.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile;
rules = hiveOtel.systemd.tmpfiles.rules; rules = hiveOtel.systemd.tmpfiles.rules;
path = hiveOtel.systemd.paths.squid-unrestricted-reload; path = hiveOtel.systemd.paths.squid-unrestricted-reload;
svc = hiveOtel.systemd.services.squid-unrestricted-reload.serviceConfig; svc = hiveOtel.systemd.services.squid-unrestricted-reload.serviceConfig;
in in
path.pathConfig.PathChanged == [ path.pathConfig.PathChanged == [
list list
hiveOtel.services.hyperhive.squid.allowedFile hiveOtel.services.hyperhive.deploy.hive-controller.squid.allowedFile
hiveOtel.services.hyperhive.squid.agentAllowFile hiveOtel.services.hyperhive.deploy.hive-controller.squid.agentAllowFile
] ]
&& hiveOtel.systemd.services.squid-unrestricted-reload.startLimitIntervalSec == 0 && hiveOtel.systemd.services.squid-unrestricted-reload.startLimitIntervalSec == 0
&& builtins.elem "paths.target" path.wantedBy && 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"; name = "squid reads the swarm's allow-list from a file seeded with the hive's own names";
ok = ok =
let let
sq = hiveOtelAllLocal.services.hyperhive.squid; sq = hiveOtelAllLocal.services.hyperhive.deploy.hive-controller.squid;
lines = lib.splitString "\n" hiveOtelAllLocal.services.squid.configText; lines = lib.splitString "\n" hiveOtelAllLocal.services.squid.configText;
rules = hiveOtelAllLocal.systemd.tmpfiles.rules; rules = hiveOtelAllLocal.systemd.tmpfiles.rules;
env = hiveOtelAllLocal.systemd.services.hive-c0re.environment; env = hiveOtelAllLocal.systemd.services.hive-c0re.environment;
@ -675,7 +676,7 @@ let
let let
conf = hiveOtel.services.squid.configText; conf = hiveOtel.services.squid.configText;
lines = lib.splitString "\n" conf; 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; rules = hiveOtel.systemd.tmpfiles.rules;
sc = hiveOtel.systemd.services.squid-selfcheck.serviceConfig; sc = hiveOtel.systemd.services.squid-selfcheck.serviceConfig;
in in
@ -686,7 +687,9 @@ let
&& users == "/var/lib/hive-squid/users" && users == "/var/lib/hive-squid/users"
&& builtins.elem "d /var/lib/hive-squid 2750 hive-core squid - -" rules && builtins.elem "d /var/lib/hive-squid 2750 hive-core squid - -" rules
&& builtins.elem "f ${users} 0640 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; && hiveOtelAllLocal.systemd.services.hive-c0re.environment.HIVE_SQUID_USERS_FILE == users;
} }
{ {

View file

@ -175,13 +175,13 @@ pkgs.testers.runNixOSTest {
networking.firewall.interfaces.eth1.allowedTCPPorts = [ 443 ]; networking.firewall.interfaces.eth1.allowedTCPPorts = [ 443 ];
# Renders the users file the way hive-c0re does. # Renders the users file the way hive-c0re does.
environment.systemPackages = [ pkgs.mkpasswd ]; environment.systemPackages = [ pkgs.mkpasswd ];
services.hyperhive.squid.selfcheck.unrestrictedCanary = "other.t.local"; services.hyperhive.deploy.hive-controller.squid.selfcheck.unrestrictedCanary = "other.t.local";
services.hyperhive.squid.selfcheck.canary = "cache.t.local"; services.hyperhive.deploy.hive-controller.squid.selfcheck.canary = "cache.t.local";
services.hyperhive.squid.baseAllowedDomains = [ services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains = [
"api.t.local" "api.t.local"
"basebridge.t.local" "basebridge.t.local"
]; ];
services.hyperhive.squid.allowedDomains = [ services.hyperhive.deploy.hive-controller.squid.allowedDomains = [
"forge.t.local" "forge.t.local"
"loop.t.local" "loop.t.local"
"bridge.t.local" "bridge.t.local"

View file

@ -6,7 +6,7 @@
//! reads it and renders the entries for the agents it runs: a swarm-wide entry //! 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 //! 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 //! 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. //! only add to them.
//! //!
//! Each name goes into the proxy's configuration, so it is checked here with //! Each name goes into the proxy's configuration, so it is checked here with