Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
33 changed files with 455 additions and 875 deletions

View file

@ -2,11 +2,7 @@
Private Forgejo instance running in a nixos-container, used as the Private Forgejo instance running in a nixos-container, used as the
swarm's persistent code-collaboration surface (issues, PRs, reviews, swarm's persistent code-collaboration surface (issues, PRs, reviews,
attachments). Configured via `services.hyperhive.swarm.forge.*` (what the attachments). Configured via `services.hyperhive.swarm.forge.*`. Container
forge IS to every hive — its package, names, ports, URLs) plus
`services.hyperhive.deploy.forgejo.*` (what the host running it decides —
how it is served, what it mirrors, where its host-local secrets sit).
Container
shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the
per-agent integration story and the notification pump that wakes per-agent integration story and the notification pump that wakes

View file

@ -10,7 +10,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) | | `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` | | `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
| `<hive>/matrix/` (deprecated) | `_` | 301 → `chat.<swarm>/` | `matrix.gui.enable` | | `<hive>/matrix/` (deprecated) | `_` | 301 → `chat.<swarm>/` | `matrix.gui.enable` |
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo.behindGateway` | | `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `forge.behindGateway` |
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` | | `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` | | `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | | `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
@ -62,7 +62,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't
`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`: `services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`:
- `<hive-domain>` → `127.0.0.1` - `<hive-domain>` → `127.0.0.1`
- `forge.<swarm>` → `127.0.0.1` (when deploy.forgejo.behindGateway) - `forge.<swarm>` → `127.0.0.1` (when forge.behindGateway)
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set) - `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
- `auth.<swarm>` → `127.0.0.1` (when deploy.authelia) - `auth.<swarm>` → `127.0.0.1` (when deploy.authelia)
@ -159,7 +159,7 @@ flip together: the primary agent-name link, the favicon fetch
(`<url>/icon`), and the nav-strip `container`-kind links from (`<url>/icon`), and the nav-strip `container`-kind links from
`DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still `DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still
resolve against `http://<host>:3000` (separate sub-domain transition resolve against `http://<host>:3000` (separate sub-domain transition
tracked by `deploy.forgejo.behindGateway`); `external`-kind links are tracked by `forge.behindGateway`); `external`-kind links are
already absolute. See `docs/web-ui/dashboard.md::Container row` for the already absolute. See `docs/web-ui/dashboard.md::Container row` for the
frontend-side derivation. frontend-side derivation.
@ -335,10 +335,8 @@ container.
services.hyperhive.swarm.forge = { services.hyperhive.swarm.forge = {
httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range
sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh
openFirewall = false; # default — expose httpPort + sshPort to the host firewall
}; };
# Which ports the forge answers on is swarm-wide; whether THIS host opens
# them in its firewall is a deployment decision, so it lives under deploy.*.
services.hyperhive.deploy.forgejo.openFirewall = false; # default
``` ```
`httpPort` (default **3000**) is the port Forgejo's HTTP server binds to. `httpPort` (default **3000**) is the port Forgejo's HTTP server binds to.
@ -359,7 +357,7 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
(not behind the gateway). (not behind the gateway).
- External git clients that push/pull via SSH directly to the host. - External git clients that push/pull via SSH directly to the host.
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does Forgejo served through the gateway (`forge.behindGateway = true`) does
not need `openFirewall` — the gateway's own `openFirewall` option covers not need `openFirewall` — the gateway's own `openFirewall` option covers
that path. that path.
@ -375,8 +373,8 @@ covers most cases:
| Shape | Auto-derived `ROOT_URL` | | Shape | Auto-derived `ROOT_URL` |
|---|---| |---|---|
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) | | `behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` | | `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
The gateway always terminates TLS, so the `behindGateway = true` case is The gateway always terminates TLS, so the `behindGateway = true` case is
always advertised over `https://`; only the direct (`behindGateway = always advertised over `https://`; only the direct (`behindGateway =

View file

@ -26,7 +26,7 @@ services.hyperhive.snapshotStore = {
}; };
# The mesh is a hard requirement, and is asserted: # The mesh is a hard requirement, and is asserted:
services.hyperhive.deploy.wireguard = { services.hyperhive.swarm.wireguard = {
enable = true; enable = true;
address = "10.100.0.9/24"; address = "10.100.0.9/24";
privateKeyFile = "/etc/wireguard/hive.key"; privateKeyFile = "/etc/wireguard/hive.key";
@ -37,7 +37,7 @@ The store host is a swarm member like any other: it gets an entry in
`services.hyperhive.swarm.hives`, the same directory every host holds. See `services.hyperhive.swarm.hives`, the same directory every host holds. See
[swarm/](../swarm/README.md) for the mesh itself. [swarm/](../swarm/README.md) for the mesh itself.
Note that the mesh is gated on `deploy.wireguard.enable`, **not** on Note that the mesh is gated on `swarm.wireguard.enable`, **not** on
`c0re.enable` --- a store host runs no hive and would otherwise get no `c0re.enable` --- a store host runs no hive and would otherwise get no
`wg-hive` interface at all. `wg-hive` interface at all.

View file

@ -100,7 +100,7 @@ build can't hold the runner's single slot indefinitely).
## Container design ## Container design
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `deploy.forgejo.behindGateway = true`. - **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `forge.behindGateway = true`.
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter). - **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`. - **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only. - **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.
@ -150,10 +150,10 @@ forge provisioning sweep (orgs, agent accounts, etc.), so mirror
seeding lives in the same place rather than a separate host-side unit. seeding lives in the same place rather than a separate host-side unit.
**General-purpose mirrors**: you can pre-seed any external repo as a **General-purpose mirrors**: you can pre-seed any external repo as a
pull-mirror via `services.hyperhive.deploy.forgejo.mirrors`: pull-mirror via `services.hyperhive.swarm.forge.mirrors`:
```nix ```nix
services.hyperhive.deploy.forgejo.mirrors = [ services.hyperhive.swarm.forge.mirrors = [
{ upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; } { upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }
{ upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; } { upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; }
]; ];

View file

@ -168,7 +168,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
issue. Until then, federation needs CA-issued certs (ACME). See issue. Until then, federation needs CA-issued certs (ACME). See
`docs/integrations/matrix.md` for federation firewall + TLS requirements. `docs/integrations/matrix.md` for federation firewall + TLS requirements.
3. **WireGuard mesh** (optional) — `deploy.wireguard.enable` reads each 3. **WireGuard mesh** (optional) — `swarm.wireguard.enable` reads each
entry's `wireguardPublicKey`/`wireguardEndpoint`/`wireguardAddress` entry's `wireguardPublicKey`/`wireguardEndpoint`/`wireguardAddress`
to configure `wg-hive`. See "WireGuard inter-hive mesh" below. to configure `wg-hive`. See "WireGuard inter-hive mesh" below.
@ -210,7 +210,7 @@ wg pubkey < /etc/wireguard/hive.key # → share this with peer operators
```nix ```nix
# hive A (pr1ma.example.com, mesh IP 10.100.0.1) # hive A (pr1ma.example.com, mesh IP 10.100.0.1)
services.hyperhive = { services.hyperhive = {
deploy.wireguard = { swarm.wireguard = {
enable = true; enable = true;
privateKeyFile = "/etc/wireguard/hive.key"; privateKeyFile = "/etc/wireguard/hive.key";
address = "10.100.0.1/24"; address = "10.100.0.1/24";
@ -237,7 +237,7 @@ services.hyperhive = {
# hive B (edge.corp, mesh IP 10.100.0.2) # hive B (edge.corp, mesh IP 10.100.0.2)
services.hyperhive = { services.hyperhive = {
deploy.wireguard = { swarm.wireguard = {
enable = true; enable = true;
privateKeyFile = "/etc/wireguard/hive.key"; privateKeyFile = "/etc/wireguard/hive.key";
address = "10.100.0.2/24"; address = "10.100.0.2/24";

View file

@ -14,7 +14,7 @@ than a convenience.
The rule is about what must stay secret, not about credentials generally. The rule is about what must stay secret, not about credentials generally.
**Public material is a value**: a certificate, or a public nkey like **Public material is a value**: a certificate, or a public nkey like
`deploy.nats.calloutUserPublicKey`, is published to every client that connects, `swarm.nats.calloutUserPublicKey`, is published to every client that connects,
so the store is a perfectly good place for it. so the store is a perfectly good place for it.
**The generator and the reader are usually in different containers.** They share **The generator and the reader are usually in different containers.** They share
@ -53,8 +53,8 @@ neither is a renaming of the other.
| OIDC client secret, digest half | the same mint | `oidc-clients/<id>.digest` | authelia's own half; merged at runtime via `settingsFiles` | | OIDC client secret, digest half | the same mint | `oidc-clients/<id>.digest` | authelia's own half; merged at runtime via `settingsFiles` |
| the swarm collector's copy of its OIDC secret | `swarm-otel-oidc-secret.service` copies it from authelia's tree, **when authelia runs on this host** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | operator provides the file and names it in `swarm.otel.clientSecretFile` — the collector need not share a host with authelia | | the swarm collector's copy of its OIDC secret | `swarm-otel-oidc-secret.service` copies it from authelia's tree, **when authelia runs on this host** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | operator provides the file and names it in `swarm.otel.clientSecretFile` — the collector need not share a host with authelia |
| authelia subject store | `swarmctl` and `swarm-authelia-bridge` | `users.yml` — one file, read and written by both | `swarmctl`, on the host that runs authelia | | authelia subject store | `swarmctl` and `swarm-authelia-bridge` | `users.yml` — one file, read and written by both | `swarmctl`, on the host that runs authelia |
| wireguard private key | **the operator** — `wg genkey` | whatever `deploy.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you | | wireguard private key | **the operator** — `wg genkey` | whatever `swarm.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you |
| queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `deploy.nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `deploy.nats.calloutUserSeedFile` / `deploy.nats.calloutIssuerSeedFile` | | queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `nats.calloutUserSeedFile` / `nats.calloutIssuerSeedFile` |
| the secret store's own contents | openbao, on first `bao operator init` — **an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this | | the secret store's own contents | openbao, on first `bao operator init` — **an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this |
| the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it is stated rather than inferred | n/a — only the host running the store seals anything | | the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it is stated rather than inferred | n/a — only the host running the store seals anything |
@ -90,7 +90,7 @@ not the definition of the case.
### Minting the queue's callout nkeys ### Minting the queue's callout nkeys
`deploy.nats.autoGenerateCallout` mints both keypairs on the host before the queue `nats.autoGenerateCallout` mints both keypairs on the host before the queue
starts. It is on by default only under `singleHostSwarm` — the one starts. It is on by default only under `singleHostSwarm` — the one
topology where the queue, its responder and the operator are the same person. On topology where the queue, its responder and the operator are the same person. On
every other topology, mint them yourself: every other topology, mint them yourself:
@ -102,8 +102,8 @@ nk -inkey callout-user.seed -pubout # → calloutUserPublicKey
nk -inkey issuer.seed -pubout # → calloutIssuerPublicKey nk -inkey issuer.seed -pubout # → calloutIssuerPublicKey
``` ```
Keep both seeds at `0600` and name them in `deploy.nats.calloutUserSeedFile` / Keep both seeds at `0600` and name them in `calloutUserSeedFile` /
`deploy.nats.calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to `calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to
admit anyone to the queue, so it belongs wherever the responder runs and nowhere admit anyone to the queue, so it belongs wherever the responder runs and nowhere
else. else.

View file

@ -116,7 +116,7 @@ See `retentionPeriod` below before leaving it at its default.
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victoriametrics.retentionPeriod` | Default `5y`. Lower it once you have measured how fast this swarm actually fills a disk — the default is deliberately generous because too-short silently discards history you cannot get back. | | `deploy.victoriametrics.retentionPeriod` | Default `5y`. Lower it once you have measured how fast this swarm actually fills a disk — the default is deliberately generous because too-short silently discards history you cannot get back. |
| `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who should not be able to reconfigure Grafana. | | `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who should not be able to reconfigure Grafana. |
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. | | `swarm.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
**Logging in.** Grafana is behind swarm SSO, so the accounts are the **Logging in.** Grafana is behind swarm SSO, so the accounts are the
authelia ones — there is no separate Grafana password, and the local authelia ones — there is no separate Grafana password, and the local

View file

@ -314,6 +314,6 @@ an error.
A surface has no URL when it isn't browser-reachable: `home` needs A surface has no URL when it isn't browser-reachable: `home` needs
`services.hyperhive.domain`; `forge` needs `services.hyperhive.domain`; `forge` needs
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs `services.hyperhive.swarm.forge.behindGateway = true`; `matrix` needs
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command `services.hyperhive.swarm.matrix.gui.enable = true`. In those cases the command
exits with a hint naming the option to set. exits with a hint naming the option to set.

View file

@ -627,7 +627,7 @@ A static matrix web client (default `pkgs.fluffychat-web` rebuilt
with `--base-href /matrix/`, swappable via with `--base-href /matrix/`, swappable via
`services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway `services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway
nginx container at `/matrix/` when nginx container at `/matrix/` when
`services.hyperhive.deploy.matrix.gui.enable` is on (defaults to `services.hyperhive.swarm.matrix.gui.enable` is on (defaults to
`matrix.enable`). c0re signals availability via the `matrix.enable`). c0re signals availability via the
`HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in `HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in
`/api/state`; the gateway does the actual static serving. `/api/state`; the gateway does the actual static serving.

View file

@ -75,7 +75,7 @@ pub(super) struct StateSnapshot {
forge_present: bool, forge_present: bool,
/// Whether the matrix GUI is reachable at `/matrix/`. Sourced from /// Whether the matrix GUI is reachable at `/matrix/`. Sourced from
/// `HIVE_MATRIX_GUI_ENABLED` env var (set by the c0re NixOS module /// `HIVE_MATRIX_GUI_ENABLED` env var (set by the c0re NixOS module
/// when `services.hyperhive.deploy.matrix.gui.enable` is on). The gateway /// when `services.hyperhive.swarm.matrix.gui.enable` is on). The gateway
/// (`nix/host-modules/hive-gateway/vhosts.nix`) does the actual `/matrix/` static serving; /// (`nix/host-modules/hive-gateway/vhosts.nix`) does the actual `/matrix/` static serving;
/// this flag is just an availability signal for iris's dashboard /// this flag is just an availability signal for iris's dashboard
/// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off. /// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off.
@ -94,8 +94,8 @@ pub(super) struct StateSnapshot {
/// Public URL of the forge (e.g. /// Public URL of the forge (e.g.
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the /// `"https://forge.pr1ma.darkest.space"`). Sourced from the
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module /// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to /// sets from `services.hyperhive.forge.publicUrl` (defaults to
/// the gateway vhost URL when `deploy.forgejo.behindGateway = true`, /// the gateway vhost URL when `forge.behindGateway = true`,
/// `null` otherwise). `None` when absent — the frontend **hides** /// `null` otherwise). `None` when absent — the frontend **hides**
/// forge links rather than guessing `http://<hostname>:3000`, /// forge links rather than guessing `http://<hostname>:3000`,
/// which is only right by accident on deployments that aren't /// which is only right by accident on deployments that aren't

View file

@ -26,11 +26,11 @@ pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
), ),
OpenTarget::Forge => ( OpenTarget::Forge => (
urls.forge, urls.forge,
"the public forge URL needs `services.hyperhive.deploy.forgejo.behindGateway = true`", "the public forge URL needs `services.hyperhive.forge.behindGateway = true`",
), ),
OpenTarget::Matrix => ( OpenTarget::Matrix => (
urls.matrix, urls.matrix,
"the matrix GUI URL needs `services.hyperhive.deploy.matrix.gui.enable = true`", "the matrix GUI URL needs `services.hyperhive.swarm.matrix.gui.enable = true`",
), ),
}; };
let url = url.with_context(|| format!("no URL available for this surface — {hint}"))?; let url = url.with_context(|| format!("no URL available for this surface — {hint}"))?;

View file

@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, bail};
use crate::util::query_hive_urls; use crate::util::query_hive_urls;
/// Host path of this hive's WireGuard private key (matches the /// Host path of this hive's WireGuard private key (matches the
/// `privateKeyFile` example in the deploy.wireguard nix options). /// `privateKeyFile` example in the swarm.wireguard nix options).
const WG_KEY_PATH: &str = "/etc/wireguard/hive.key"; const WG_KEY_PATH: &str = "/etc/wireguard/hive.key";
/// The mesh interface name hive-c0re's nix module brings up. /// The mesh interface name hive-c0re's nix module brings up.
const WG_INTERFACE: &str = "wg-hive"; const WG_INTERFACE: &str = "wg-hive";
@ -116,7 +116,7 @@ pub(crate) async fn wg_init(socket: &Path, address: Option<&str>) -> Result<()>
println!("\nPublic key (share this with peer hives — they pass it to `hivectl wg peer`):"); println!("\nPublic key (share this with peer hives — they pass it to `hivectl wg peer`):");
println!(" {pubkey}"); println!(" {pubkey}");
println!("\nAdd to this hive's NixOS config:"); println!("\nAdd to this hive's NixOS config:");
println!(" services.hyperhive.deploy.wireguard = {{"); println!(" services.hyperhive.swarm.wireguard = {{");
println!(" enable = true;"); println!(" enable = true;");
println!(" privateKeyFile = \"{WG_KEY_PATH}\";"); println!(" privateKeyFile = \"{WG_KEY_PATH}\";");
println!(" address = \"{addr}\";"); println!(" address = \"{addr}\";");
@ -238,7 +238,7 @@ pub(crate) fn wg_status() -> Result<()> {
if !status.success() { if !status.success() {
bail!( bail!(
"`wg show {WG_INTERFACE}` failed — is the mesh enabled + up? \ "`wg show {WG_INTERFACE}` failed — is the mesh enabled + up? \
(services.hyperhive.deploy.wireguard.enable = true, then deploy)" (services.hyperhive.swarm.wireguard.enable = true, then deploy)"
); );
} }
Ok(()) Ok(())

View file

@ -41,22 +41,6 @@ in
[ "services" "hyperhive" "swarm" "grafana" "enable" ] [ "services" "hyperhive" "swarm" "grafana" "enable" ]
[ "services" "hyperhive" "deploy" "grafana" "enable" ] [ "services" "hyperhive" "deploy" "grafana" "enable" ]
) )
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "socketDir" ]
[ "services" "hyperhive" "deploy" "grafana" "socketDir" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "datasourceUrl" ]
[ "services" "hyperhive" "deploy" "grafana" "datasourceUrl" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "logsDatasourceUrl" ]
[ "services" "hyperhive" "deploy" "grafana" "logsDatasourceUrl" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "plugins" ]
[ "services" "hyperhive" "deploy" "grafana" "plugins" ]
)
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victoriametrics" "enable" ] [ "services" "hyperhive" "swarm" "victoriametrics" "enable" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "enable" ] [ "services" "hyperhive" "deploy" "victoriametrics" "enable" ]
@ -69,22 +53,6 @@ in
[ "services" "hyperhive" "swarm" "controller" "enable" ] [ "services" "hyperhive" "swarm" "controller" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "enable" ] [ "services" "hyperhive" "deploy" "swarm-controller" "enable" ]
) )
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "socketPath" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "socketPath" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "forgeTokenFile" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "forgeTokenFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "authBridgeUrl" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "authBridgeUrl" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "queue" "clientSecretFile" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "queue" "clientSecretFile" ]
)
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "ui" "enable" ] [ "services" "hyperhive" "swarm" "ui" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-ui" "enable" ] [ "services" "hyperhive" "deploy" "swarm-ui" "enable" ]
@ -130,100 +98,6 @@ in
[ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ] [ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ]
) )
# The queue's callout identity. What stays under `swarm.nats` is what the
# queue IS to every hive — the ports it answers on, the client id it is
# registered under, the responder package; these five are what the host
# running it decides.
#
# ⚠️ The two PUBLIC keys move with their seeds. A public key looks
# swarm-wide, and the user key genuinely is published to every client that
# connects — but peers RECEIVE it over the wire, they never configure it,
# and splitting a keypair across two namespaces is worse than either
# placement. `enable` moved earlier; its shim is the one above.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "autoGenerateCallout" ]
[ "services" "hyperhive" "deploy" "nats" "autoGenerateCallout" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutUserPublicKey" ]
[ "services" "hyperhive" "deploy" "nats" "calloutUserPublicKey" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutIssuerPublicKey" ]
[ "services" "hyperhive" "deploy" "nats" "calloutIssuerPublicKey" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutUserSeedFile" ]
[ "services" "hyperhive" "deploy" "nats" "calloutUserSeedFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutIssuerSeedFile" ]
[ "services" "hyperhive" "deploy" "nats" "calloutIssuerSeedFile" ]
)
# The rest of the forge split. What stays under `swarm.forge` is what the
# forge IS from any hive's point of view — its package, the names and ports
# it answers on, the URLs it advertises, the client id it is registered
# under; these five are what the host running it decides. `sso` splits
# because its two halves are different facts: the client id must match the
# entry in authelia's register, the secret is a path on this machine.
#
# `mirrors` renames in one entry rather than one per field: it is a single
# option of a list-of-submodule type, so the rename carries its whole
# value. The `ci` block above needs five because it is a plain attrset of
# separate options, which is the case with no parent path to rename.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "behindGateway" ]
[ "services" "hyperhive" "deploy" "forgejo" "behindGateway" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "openFirewall" ]
[ "services" "hyperhive" "deploy" "forgejo" "openFirewall" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "mirrors" ]
[ "services" "hyperhive" "deploy" "forgejo" "mirrors" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "sso" "clientSecretFile" ]
[ "services" "hyperhive" "deploy" "forgejo" "sso" "clientSecretFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "hostSwarmControllerTokenFile" ]
[ "services" "hyperhive" "deploy" "forgejo" "hostSwarmControllerTokenFile" ]
)
# The homeserver, split the same way. `swarm.matrix` keeps what it IS to
# every hive — its package, the name it answers to, the ports and URLs it
# is reached on, the client id it is registered under. These six are what
# the host running it decides. ⚠️ `enable` is NOT here: it was renamed
# earlier and its shim lives in ./hive-matrix.nix, so that file is the one
# to check before assuming this table is the whole matrix story.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "openFirewall" ]
[ "services" "hyperhive" "deploy" "matrix" "openFirewall" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "trustedServers" ]
[ "services" "hyperhive" "deploy" "matrix" "trustedServers" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "maxRequestSize" ]
[ "services" "hyperhive" "deploy" "matrix" "maxRequestSize" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "registrationTokenFile" ]
[ "services" "hyperhive" "deploy" "matrix" "registrationTokenFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "gui" "enable" ]
[ "services" "hyperhive" "deploy" "matrix" "gui" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "sso" "clientSecretFile" ]
[ "services" "hyperhive" "deploy" "matrix" "sso" "clientSecretFile" ]
)
# Retention is read only where the container is defined, so it is a # Retention is read only where the container is defined, so it is a
# decision of the host running the store rather than something the swarm # decision of the host running the store rather than something the swarm
# agrees on. The two stores keep everything else — package, domain, port # agrees on. The two stores keep everything else — package, domain, port
@ -274,34 +148,6 @@ in
[ "services" "hyperhive" "tls" "leafValidityDays" ] [ "services" "hyperhive" "tls" "leafValidityDays" ]
[ "services" "hyperhive" "deploy" "hive-controller" "tls" "leafValidityDays" ] [ "services" "hyperhive" "deploy" "hive-controller" "tls" "leafValidityDays" ]
) )
# The WireGuard mesh, whole. Unlike every rename above this one moves a
# namespace rather than a toggle: nothing under it is a fact another hive
# reads. A peer needs this host's `wireguardEndpoint` — the roster entry
# in ./swarm.nix, which stays — and nothing about the interface this host
# brings up. `listenPort` moves for the same reason and is easy to read
# the other way: it is what this host *binds*, while the port a peer
# *dials* is the one inside `wireguardEndpoint`.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "enable" ]
[ "services" "hyperhive" "deploy" "wireguard" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "privateKeyFile" ]
[ "services" "hyperhive" "deploy" "wireguard" "privateKeyFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "address" ]
[ "services" "hyperhive" "deploy" "wireguard" "address" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "listenPort" ]
[ "services" "hyperhive" "deploy" "wireguard" "listenPort" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "persistentKeepalive" ]
[ "services" "hyperhive" "deploy" "wireguard" "persistentKeepalive" ]
)
]; ];
# ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the # ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the

View file

@ -31,6 +31,7 @@ let
hyperhiveCfg = config.services.hyperhive; hyperhiveCfg = config.services.hyperhive;
deployCfg = hyperhiveCfg.deploy; deployCfg = hyperhiveCfg.deploy;
baoCfg = hyperhiveCfg.swarm.bao; baoCfg = hyperhiveCfg.swarm.bao;
matrixCfg = hyperhiveCfg.swarm.matrix;
baoDeploy = deployCfg.bao; baoDeploy = deployCfg.bao;
@ -47,9 +48,8 @@ let
# A literal, not an option — ./hive-matrix.nix names its container # A literal, not an option — ./hive-matrix.nix names its container
# `containers.hive-matrix` directly and declares no `machine` to derive it # `containers.hive-matrix` directly and declares no `machine` to derive it
# from, which the trust-bundle call in that file already says out loud. # from, which the trust-bundle call in that file already says out loud.
# ⚠️ A `swarm.matrix.machine` read parses fine and fails at module-system # ⚠️ `matrixCfg.machine` parses fine and fails at module-system resolution,
# resolution, so this is the kind of mistake only reading the target module # so this is the kind of mistake only reading the target module catches.
# catches.
matrixMachine = "hive-matrix"; matrixMachine = "hive-matrix";
in in
{ {
@ -115,8 +115,8 @@ in
fi fi
umask 077 umask 077
printf '%s\n' "$token" > ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} printf '%s\n' "$token" > ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)}
chmod 0600 ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} chmod 0600 ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)}
''; '';
}; };
}; };

View file

@ -200,7 +200,7 @@ in
# one. # one.
HIVE_MATRIX_API_URL = config.services.hyperhive.swarm.matrix.apiUrl; HIVE_MATRIX_API_URL = config.services.hyperhive.swarm.matrix.apiUrl;
} }
// lib.optionalAttrs config.services.hyperhive.deploy.matrix.gui.enable { // lib.optionalAttrs config.services.hyperhive.swarm.matrix.gui.enable {
# Availability flags read by the dashboard's `/api/state`. # Availability flags read by the dashboard's `/api/state`.
# Matrix GUI lives entirely on the gateway nginx (matrix tab # Matrix GUI lives entirely on the gateway nginx (matrix tab
# only shows when both flags are on). Gateway routing detail: # only shows when both flags are on). Gateway routing detail:
@ -220,8 +220,8 @@ in
# breaks the moment the operator's browser hostname isn't the forge # breaks the moment the operator's browser hostname isn't the forge
# host, e.g. through the gateway or a reverse proxy). Sourced from # host, e.g. through the gateway or a reverse proxy). Sourced from
# `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the # `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the
# gateway vhost URL when `deploy.forgejo.behindGateway = true` and `null` # gateway vhost URL when `behindGateway = true` and `null` otherwise
# otherwise — see that option's doc for the "hide, don't guess" rationale. # — see that option's doc for the "hide, don't guess" rationale.
# Absent here whenever `publicUrl` is `null`; the dashboard hides # Absent here whenever `publicUrl` is `null`; the dashboard hides
# forge links rather than emitting one it can't justify. # forge links rather than emitting one it can't justify.
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl; HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
@ -229,7 +229,7 @@ in
// //
lib.optionalAttrs lib.optionalAttrs
( (
config.services.hyperhive.deploy.matrix.gui.enable config.services.hyperhive.swarm.matrix.gui.enable
&& config.services.hyperhive.swarm.matrix.gatewayHost != null && config.services.hyperhive.swarm.matrix.gatewayHost != null
) )
{ {

View file

@ -7,7 +7,6 @@
let let
cfg = config.services.hyperhive.deploy.forgejo.ci; cfg = config.services.hyperhive.deploy.forgejo.ci;
forgeCfg = config.services.hyperhive.swarm.forge; forgeCfg = config.services.hyperhive.swarm.forge;
forgeDeployCfg = config.services.hyperhive.deploy.forgejo;
gatewayCfg = config.services.hyperhive.gateway; gatewayCfg = config.services.hyperhive.gateway;
networkCfg = config.services.hyperhive.network; networkCfg = config.services.hyperhive.network;
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
@ -80,8 +79,8 @@ in
example = true; example = true;
description = '' description = ''
Run a Forgejo Actions runner in a `hive-ci` nixos-container. Run a Forgejo Actions runner in a `hive-ci` nixos-container.
Grouped under `services.hyperhive.deploy.forgejo` because the runner Grouped under `services.hyperhive.swarm.forge` because the runner is
is tightly coupled to the forge instance it registers against. tightly coupled to the forge instance it registers against.
Disabled by default; the internal forge it registers against is Disabled by default; the internal forge it registers against is
always present (mandatory), so enabling this is all that's needed. always present (mandatory), so enabling this is all that's needed.
@ -154,17 +153,17 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# `deploy.forgejo.behindGateway = true` (the default) is required because # `forge.behindGateway = true` (the default) is required because the
# the CI container uses private networking and reaches the forge through # CI container uses private networking and reaches the forge through
# the gateway vhost. Without the gateway vhost there is no HTTP # the gateway vhost. Without the gateway vhost there is no HTTP
# listener for `forgeCfg.domain` on the bridge that the runner can # listener for `forgeCfg.domain` on the bridge that the runner can
# connect to. # connect to.
assertions = [ assertions = [
{ {
assertion = forgeDeployCfg.behindGateway; assertion = forgeCfg.behindGateway;
message = '' message = ''
services.hyperhive.deploy.forgejo.ci.enable requires services.hyperhive.deploy.forgejo.ci.enable requires
services.hyperhive.deploy.forgejo.behindGateway = true. services.hyperhive.swarm.forge.behindGateway = true.
The CI container runs with a private network namespace and The CI container runs with a private network namespace and
reaches the forge through the gateway vhost on the bridge IP. reaches the forge through the gateway vhost on the bridge IP.
Set behindGateway = true (it defaults to true alongside Set behindGateway = true (it defaults to true alongside
@ -217,8 +216,8 @@ in
# Private network namespace, attached to the hive bridge so the # Private network namespace, attached to the hive bridge so the
# runner reaches the forge via the gateway — and cannot reach # runner reaches the forge via the gateway — and cannot reach
# host-loopback (127.0.0.1:7000 dashboard, raw forge port, etc.). # host-loopback (127.0.0.1:7000 dashboard, raw forge port, etc.).
# Requires `deploy.forgejo.behindGateway = true` (asserted in the # Requires `forge.behindGateway = true` (asserted in the options
# options block above). See docs/networking/network.md. # block above). See docs/networking/network.md.
privateNetwork = true; privateNetwork = true;
hostBridge = networkCfg.bridgeName; hostBridge = networkCfg.bridgeName;

View file

@ -75,7 +75,7 @@ let
# links. Operators can still override via `cfg.rootUrl` for bespoke # links. Operators can still override via `cfg.rootUrl` for bespoke
# shapes. # shapes.
defaultRootUrl = defaultRootUrl =
if deployCfg.forgejo.behindGateway then if cfg.behindGateway then
let let
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}"; portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
in in
@ -98,9 +98,9 @@ let
# operator hasn't already declared that dest themselves (else CI-on + # operator hasn't already declared that dest themselves (else CI-on +
# an explicit `actions/checkout` entry would duplicate it). # an explicit `actions/checkout` entry would duplicate it).
effectiveMirrors = effectiveMirrors =
deployCfg.forgejo.mirrors cfg.mirrors
++ lib.optional ( ++ lib.optional (
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) deployCfg.forgejo.mirrors) ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
) actionCheckoutMirror; ) actionCheckoutMirror;
in in
{ {
@ -133,7 +133,7 @@ in
Removed rather than defaulted to true so a config that turned it Removed rather than defaulted to true so a config that turned it
OFF fails here, where the line is, instead of silently gaining a OFF fails here, where the line is, instead of silently gaining a
login provider on the next rebuild. Drop the line; if it was login provider on the next rebuild. Drop the line; if it was
false, set services.hyperhive.deploy.forgejo.sso.clientSecretFile and false, set services.hyperhive.swarm.forge.sso.clientSecretFile and
services.hyperhive.swarm.authelia.url as the assertions describe. services.hyperhive.swarm.authelia.url as the assertions describe.
'') '')
]; ];
@ -178,7 +178,7 @@ in
description = '' description = ''
Public hostname for the forge. Doubles as both the forgejo Public hostname for the forge. Doubles as both the forgejo
`DOMAIN` setting (clone URLs forgejo advertises) AND the `DOMAIN` setting (clone URLs forgejo advertises) AND the
gateway vhost server-name when `deploy.forgejo.behindGateway = true` gateway vhost server-name when `behindGateway = true`
(sub-domain routing — see `docs/networking/gateway.md`). (sub-domain routing — see `docs/networking/gateway.md`).
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
@ -201,10 +201,7 @@ in
publicUrl = lib.mkOption { publicUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = default =
if config.services.hyperhive.enable && deployCfg.forgejo.behindGateway then if config.services.hyperhive.enable && cfg.behindGateway then "https://${cfg.domain}" else null;
"https://${cfg.domain}"
else
null;
defaultText = lib.literalExpression '' defaultText = lib.literalExpression ''
if behindGateway then "https://''${domain}" else null if behindGateway then "https://''${domain}" else null
''; '';
@ -215,7 +212,7 @@ in
the approval-queue's "review PR on forge" link) — sourced into the approval-queue's "review PR on forge" link) — sourced into
every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`. every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`.
Defaults to `https://''${cfg.domain}` when `deploy.forgejo.behindGateway = Defaults to `https://''${cfg.domain}` when `behindGateway =
true` (the gateway vhost is genuinely reachable at that URL) true` (the gateway vhost is genuinely reachable at that URL)
and `null` otherwise. When `null`, the dashboard **hides** and `null` otherwise. When `null`, the dashboard **hides**
forge links rather than guessing one — see forge links rather than guessing one — see
@ -224,7 +221,7 @@ in
container port is only an accident away from wrong on any container port is only an accident away from wrong on any
deployment that isn't plain localhost). deployment that isn't plain localhost).
**Set this explicitly if `deploy.forgejo.behindGateway = false`** and the **Set this explicitly if `behindGateway = false`** and the
forge is still reachable at a stable URL you want linked from forge is still reachable at a stable URL you want linked from
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}` the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
for an all-LAN deployment) — leaving it unset there means the for an all-LAN deployment) — leaving it unset there means the
@ -247,60 +244,6 @@ in
''; '';
}; };
rootUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://forge.example.com/";
description = ''
Override the auto-derived forgejo `ROOT_URL`. When `null`
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
state, including the scheme:
- `deploy.forgejo.behindGateway = true` → `https://''${cfg.domain}/`. The gateway
always terminates TLS (self-signed is the implicit floor when no
`gateway.tls.certDir` / ACME is set), so the forge is always
advertised over https. A non-canonical `gateway.httpsPort` is
appended as `:<port>`.
- `deploy.forgejo.behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
The TLS scheme is derived automatically now, so you only need to
set this for a genuinely bespoke shape (e.g. an external reverse
proxy on a different host/path). Must end with `/` per forgejo's
`ROOT_URL` contract.
'';
};
# The swarm's authelia is always registered as an OpenID Connect
# login source here — there is no toggle, for the same reason the
# forge itself has none.
#
# **Additive, never exclusive.** Forgejo keeps its local password
# database and gains an extra "sign in with" button; this does not
# disable local login. Deliberate: an identity provider that can take
# the forge offline when it hiccups is a worse forge than one with
# two ways in — which is also what makes always-on safe.
sso = {
clientId = lib.mkOption {
type = lib.types.str;
default = "forgejo";
description = ''
OAuth2 client id this forge identifies itself with. Must match
the `id` of the corresponding entry in
`services.hyperhive.swarm.authelia.oidc.clients`.
'';
};
# The secret half is a path on the host that runs the forge, so it
# lives under `deploy.forgejo.sso` — see the block below.
};
};
# What stays above is what the forge IS from any hive's point of view: its
# package, the names and ports it answers on, the URLs it advertises, and
# the client id it is registered under. What lives here is what the host
# running it decides — how it is served, what it mirrors, and where its
# host-local secrets sit. Same rule as ./swarm-victorialogs.nix, and the
# renames are in ./deploy.nix with the rest.
options.services.hyperhive.deploy.forgejo = {
behindGateway = lib.mkOption { behindGateway = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = config.services.hyperhive.enable; default = config.services.hyperhive.enable;
@ -331,6 +274,29 @@ in
''; '';
}; };
rootUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://forge.example.com/";
description = ''
Override the auto-derived forgejo `ROOT_URL`. When `null`
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
state, including the scheme:
- `behindGateway = true` → `https://''${cfg.domain}/`. The gateway
always terminates TLS (self-signed is the implicit floor when no
`gateway.tls.certDir` / ACME is set), so the forge is always
advertised over https. A non-canonical `gateway.httpsPort` is
appended as `:<port>`.
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
The TLS scheme is derived automatically now, so you only need to
set this for a genuinely bespoke shape (e.g. an external reverse
proxy on a different host/path). Must end with `/` per forgejo's
`ROOT_URL` contract.
'';
};
openFirewall = lib.mkOption { openFirewall = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@ -346,8 +312,8 @@ in
**Breaking change**: this used to default to `true`. If you **Breaking change**: this used to default to `true`. If you
relied on the old default for external reach, add relied on the old default for external reach, add
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your `services.hyperhive.swarm.forge.openFirewall = true;` to your host
host config before rebuilding. config before rebuilding.
''; '';
}; };
@ -394,10 +360,27 @@ in
''; '';
}; };
# The other half of the SSO pair: the client *id* is swarm-wide (it has # The swarm's authelia is always registered as an OpenID Connect
# to match authelia's register), the secret is a path on this host. The # login source here — there is no toggle, for the same reason the
# design note for the login source itself is with the id. # forge itself has none.
sso.clientSecretFile = lib.mkOption { #
# **Additive, never exclusive.** Forgejo keeps its local password
# database and gains an extra "sign in with" button; this does not
# disable local login. Deliberate: an identity provider that can take
# the forge offline when it hiccups is a worse forge than one with
# two ways in — which is also what makes always-on safe.
sso = {
clientId = lib.mkOption {
type = lib.types.str;
default = "forgejo";
description = ''
OAuth2 client id this forge identifies itself with. Must match
the `id` of the corresponding entry in
`services.hyperhive.swarm.authelia.oidc.clients`.
'';
};
clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "/var/lib/hyperhive/forge-oidc-secret"; example = "/var/lib/hyperhive/forge-oidc-secret";
@ -416,6 +399,7 @@ in
eval error. eval error.
''; '';
}; };
};
hostSwarmControllerTokenFile = lib.mkOption { hostSwarmControllerTokenFile = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -459,13 +443,13 @@ in
# Both halves are gated on `behindGateway`: with it off the operator # Both halves are gated on `behindGateway`: with it off the operator
# fronts forgejo themselves, so this hive must neither claim the # fronts forgejo themselves, so this hive must neither claim the
# vhost nor answer DNS for it. # vhost nor answer DNS for it.
services.hyperhive.gateway.localNames = lib.optional deployCfg.forgejo.behindGateway cfg.domain; services.hyperhive.gateway.localNames = lib.optional cfg.behindGateway cfg.domain;
# This swarm-ui quick-links entry, same `behindGateway` guard as the # This swarm-ui quick-links entry, same `behindGateway` guard as the
# vhost/DNS name above — with it off, this host doesn't actually # vhost/DNS name above — with it off, this host doesn't actually
# serve `cfg.domain`, so linking to it would be dead. See # serve `cfg.domain`, so linking to it would be dead. See
# `services.hyperhive.swarm.controller.links`'s description. # `services.hyperhive.swarm.controller.links`'s description.
services.hyperhive.swarm.controller.links = lib.optional deployCfg.forgejo.behindGateway { services.hyperhive.swarm.controller.links = lib.optional cfg.behindGateway {
label = "Forge"; label = "Forge";
icon = "⚒"; icon = "⚒";
url = "https://${cfg.domain}/"; url = "https://${cfg.domain}/";
@ -482,9 +466,7 @@ in
# what authelia compares against — this string agreeing with the # what authelia compares against — this string agreeing with the
# `location` block above it is the whole mechanism. A near miss is a # `location` block above it is the whole mechanism. A near miss is a
# correctly minted token refused at the target. # correctly minted token refused at the target.
services.hyperhive.swarm.otel.publishedScrapeTargets = services.hyperhive.swarm.otel.publishedScrapeTargets = lib.optionalAttrs cfg.behindGateway {
lib.optionalAttrs deployCfg.forgejo.behindGateway
{
forgejo = "https://${cfg.domain}/metrics"; forgejo = "https://${cfg.domain}/metrics";
}; };
@ -492,7 +474,7 @@ in
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB # git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
# clones). SSH stays direct on `forge.sshPort`. See # clones). SSH stays direct on `forge.sshPort`. See
# `docs/networking/gateway.md`. # `docs/networking/gateway.md`.
services.nginx.virtualHosts = lib.optionalAttrs deployCfg.forgejo.behindGateway { services.nginx.virtualHosts = lib.optionalAttrs cfg.behindGateway {
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { "${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
listen = gatewayCfg.lib.listen; listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders; extraConfig = gatewayCfg.lib.securityHeaders;
@ -561,10 +543,10 @@ in
{ {
# Fail at EVAL, not at boot. The alternative failure is a login # Fail at EVAL, not at boot. The alternative failure is a login
# button that always 401s, three layers from the missing file. # button that always 401s, three layers from the missing file.
assertion = deployCfg.forgejo.sso.clientSecretFile != null; assertion = cfg.sso.clientSecretFile != null;
message = '' message = ''
The forge's SSO login source requires The forge's SSO login source requires
services.hyperhive.deploy.forgejo.sso.clientSecretFile — the path services.hyperhive.swarm.forge.sso.clientSecretFile — the path
(inside the forge container) holding the OIDC client secret's (inside the forge container) holding the OIDC client secret's
plaintext. plaintext.
@ -618,7 +600,7 @@ in
# splits on the single slash to create the org + repo. # splits on the single slash to create the org + repo.
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors; assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
message = '' message = ''
Every services.hyperhive.deploy.forgejo.mirrors[].dest must be exactly Every services.hyperhive.swarm.forge.mirrors[].dest must be exactly
"<owner>/<repo>" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors} "<owner>/<repo>" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors}
''; '';
} }
@ -636,7 +618,7 @@ in
]) ])
) effectiveMirrors; ) effectiveMirrors;
message = '' message = ''
services.hyperhive.deploy.forgejo.mirrors[].dest must not place a mirror services.hyperhive.swarm.forge.mirrors[].dest must not place a mirror
in a hive-c0re-managed org (config / shared / agents / core) — in a hive-c0re-managed org (config / shared / agents / core) —
those are provisioned by hive-c0re and a mirror there would those are provisioned by hive-c0re and a mirror there would
collide. Use a dedicated org (e.g. "actions/checkout"). collide. Use a dedicated org (e.g. "actions/checkout").
@ -789,7 +771,7 @@ in
# scraper at the gateway, so a second credential system per # scraper at the gateway, so a second credential system per
# service would buy nothing and would be the one that stops # service would buy nothing and would be the one that stops
# getting rotated. # getting rotated.
metrics.ENABLED = deployCfg.forgejo.behindGateway; metrics.ENABLED = cfg.behindGateway;
# The two per-dimension breakdowns, on the same condition as # The two per-dimension breakdowns, on the same condition as
# the endpoint itself: `gitea_issues_by_label{label=…}` and # the endpoint itself: `gitea_issues_by_label{label=…}` and
# `gitea_issues_by_repository{repository=…}`. Off by default # `gitea_issues_by_repository{repository=…}`. Off by default
@ -808,8 +790,8 @@ in
# forge that grew to thousands of repos would want this # forge that grew to thousands of repos would want this
# revisited — that is a real trigger, unlike a time-based one: # revisited — that is a real trigger, unlike a time-based one:
# `count(gitea_issues_by_repository)` answers it directly. # `count(gitea_issues_by_repository)` answers it directly.
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway; metrics.ENABLED_ISSUE_BY_LABEL = cfg.behindGateway;
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.behindGateway; metrics.ENABLED_ISSUE_BY_REPOSITORY = cfg.behindGateway;
# Repo migrations / pull-mirrors fetch from the source # Repo migrations / pull-mirrors fetch from the source
# URL *inside* Forgejo. hyperhive code is synced from # URL *inside* Forgejo. hyperhive code is synced from
# `localhost` (and the host LAN), which Forgejo's # `localhost` (and the host LAN), which Forgejo's
@ -1043,9 +1025,9 @@ in
script = '' script = ''
set -euo pipefail set -euo pipefail
secret=$(cat ${lib.escapeShellArg deployCfg.forgejo.sso.clientSecretFile}) secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
if [ -z "$secret" ]; then if [ -z "$secret" ]; then
echo "empty OIDC client secret at ${deployCfg.forgejo.sso.clientSecretFile}" >&2 echo "empty OIDC client secret at ${cfg.sso.clientSecretFile}" >&2
exit 1 exit 1
fi fi
@ -1222,7 +1204,7 @@ in
# Same case, same reasoning: this host minted the secret, so it can # Same case, same reasoning: this host minted the secret, so it can
# say where the forge will find it. # say where the forge will find it.
services.hyperhive.deploy.forgejo.sso.clientSecretFile = lib.mkIf ssoLocal ( services.hyperhive.swarm.forge.sso.clientSecretFile = lib.mkIf ssoLocal (
lib.mkDefault forgeSecretPath lib.mkDefault forgeSecretPath
); );
@ -1316,7 +1298,7 @@ in
set -euo pipefail set -euo pipefail
src=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${swarmControllerTokenPath}"} src=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${swarmControllerTokenPath}"}
dst=${lib.escapeShellArg deployCfg.forgejo.hostSwarmControllerTokenFile} dst=${lib.escapeShellArg cfg.hostSwarmControllerTokenFile}
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
[ -s "$src" ] && break [ -s "$src" ] && break
@ -1331,7 +1313,7 @@ in
''; '';
}; };
networking.firewall = lib.mkIf deployCfg.forgejo.openFirewall { networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ allowedTCPPorts = [
cfg.httpPort cfg.httpPort
cfg.sshPort cfg.sshPort

View file

@ -22,7 +22,6 @@ let
# same list rather than each deciding what "a swarm service" means. # same list rather than each deciding what "a swarm service" means.
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains; swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
matrixCfg = config.services.hyperhive.swarm.matrix; matrixCfg = config.services.hyperhive.swarm.matrix;
matrixDeployCfg = config.services.hyperhive.deploy.matrix;
networkCfg = config.services.hyperhive.network; networkCfg = config.services.hyperhive.network;
# Every vhost claiming `default_server`, ours and the operator's # Every vhost claiming `default_server`, ours and the operator's
@ -105,7 +104,6 @@ let
cfg cfg
errorPages errorPages
matrixCfg matrixCfg
matrixDeployCfg
hyperhiveDomain hyperhiveDomain
dashboardDist dashboardDist
swaggerUiTheme swaggerUiTheme

View file

@ -15,8 +15,7 @@
{ {
lib, lib,
cfg, # services.hyperhive.gateway cfg, # services.hyperhive.gateway
matrixCfg, # services.hyperhive.swarm.matrix — what the homeserver IS matrixCfg,
matrixDeployCfg, # services.hyperhive.deploy.matrix — what THIS host runs
hyperhiveDomain, hyperhiveDomain,
dashboardDist, dashboardDist,
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
@ -45,8 +44,7 @@ let
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link # `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link
# shim during the fluffychat sub-domain move). See `docs/networking/gateway.md`. # shim during the fluffychat sub-domain move). See `docs/networking/gateway.md`.
matrixRedirectLocations = matrixRedirectLocations =
lib.optionalAttrs lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null)
(matrixDeployCfg.enable && matrixDeployCfg.gui.enable && matrixCfg.gatewayHost != null)
( (
let let
target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"; target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
@ -64,7 +62,7 @@ let
# clients at `matrixCfg.gatewayHost` when set; falls back to direct # clients at `matrixCfg.gatewayHost` when set; falls back to direct
# `<hive>:<httpPort>`. CORS `*` per matrix spec. The `m.server` # `<hive>:<httpPort>`. CORS `*` per matrix spec. The `m.server`
# port-8448 carve-out is documented inline. See `docs/networking/gateway.md`. # port-8448 carve-out is documented inline. See `docs/networking/gateway.md`.
wellKnownLocations = lib.optionalAttrs matrixDeployCfg.enable ( wellKnownLocations = lib.optionalAttrs matrixCfg.enable (
let let
clientBaseUrl = clientBaseUrl =
if matrixCfg.gatewayHost != null then if matrixCfg.gatewayHost != null then

View file

@ -178,7 +178,7 @@ in
Removed rather than defaulted to true so a config that turned it Removed rather than defaulted to true so a config that turned it
OFF fails here, where the line is, instead of silently gaining a OFF fails here, where the line is, instead of silently gaining a
login flow on the next rebuild. Drop the line; if it was false, login flow on the next rebuild. Drop the line; if it was false,
set services.hyperhive.deploy.matrix.sso.clientSecretFile and set services.hyperhive.swarm.matrix.sso.clientSecretFile and
services.hyperhive.swarm.authelia.url as the assertions describe. services.hyperhive.swarm.authelia.url as the assertions describe.
'') '')
]; ];
@ -312,91 +312,6 @@ in
''; '';
}; };
allowEncryption = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Server-side switch for matrix end-to-end encryption — sets
tuwunel's `allow_encryption`. Off by default: on the hive-internal
homeserver the operator already controls the transport, so server
E2EE adds key-management overhead (cross-signing, device
verification, undecryptable-message recovery) without a clear
threat-model win for the common single-hive case. Turn on when
agents join encrypted rooms on external / federated homeservers,
or when the operator wants message contents opaque to the
homeserver admin. Independent of the agent matrix client, which
always supports decryption so it can read encrypted rooms it is
invited to regardless of this flag; this option only governs
whether THIS homeserver permits room encryption.
'';
};
gui = {
# Whether this host serves it is `deploy.matrix.gui.enable`.
package = lib.mkOption {
type = lib.types.package;
default = fluffychat-web-fixed;
defaultText = lib.literalMD ''
`pkgs.fluffychat-web` with a `postInstall` patch that adds
the three files `flutter341.buildFlutterApplication` skips.
'';
description = ''
Static web client dist served at `matrix.<hive>/`. Override
to swap fluffychat for hydrogen-web, cinny, element-web, or
an out-of-tree dist — any replacement is mounted at the
sub-domain root with the upstream-default `<base href "/">`,
no sub-path gymnastics needed.
'';
};
};
# This homeserver always delegates login to the swarm's authelia, as
# an OIDC relying party — matrix SSO (`m.login.sso`), offered
# alongside password login. No toggle: a homeserver in a swarm is a
# client of that swarm's identity provider.
#
# ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point
# the other way: those make this homeserver an *authorization server*
# for matrix clients. This family makes it a *client* of an external
# identity provider. The two share the protocol's name and answer
# opposite questions.
#
# This **adds** a way in. Password login keeps working: an identity
# provider that can take the homeserver offline when it hiccups is a
# worse homeserver than one with two ways in — which is also what
# makes always-on safe. Making authelia the *only* path is a
# separate, reversible switch (tuwunel's `login_with_password`),
# deliberately not folded in here.
#
# ⚠️ Matrix SSO lives **inside** the homeserver, never behind a
# forward-auth proxy: the client-server API is spoken by non-browser
# clients holding matrix access tokens — every agent's own
# `hive-matrix-daemon` — plus federation, and a proxy in front of
# `/_matrix/` breaks all of it.
sso = {
clientId = lib.mkOption {
type = lib.types.str;
default = "tuwunel";
description = ''
OAuth2 client id this homeserver identifies itself with. Must
match the `id` of the corresponding entry in
`services.hyperhive.swarm.authelia.oidc.clients`.
The secret it pairs with is a host path, so it lives at
`services.hyperhive.deploy.matrix.sso.clientSecretFile`.
'';
};
};
};
# What stays above is what the homeserver IS from any hive's point of view:
# its package, the name it answers to, the ports and URLs it is reached on,
# and the client id it is registered under. What lives here is what the host
# running it decides — whether it is exposed, which peers it trusts, how large
# a request it accepts, and where its host-local secrets sit. Same rule as
# ./swarm-victorialogs.nix; `enable` already lives in ./deploy.nix, which also
# carries the renames.
options.services.hyperhive.deploy.matrix = {
openFirewall = lib.mkOption { openFirewall = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@ -412,7 +327,7 @@ in
**Breaking change**: this used to default to `true`. If you **Breaking change**: this used to default to `true`. If you
relied on the old default for external reach, add relied on the old default for external reach, add
`services.hyperhive.deploy.matrix.openFirewall = true;` to your host `services.hyperhive.swarm.matrix.openFirewall = true;` to your host
config before rebuilding. config before rebuilding.
Note: federation (the matrix-spec well-known port 8448) is Note: federation (the matrix-spec well-known port 8448) is
@ -466,23 +381,92 @@ in
''; '';
}; };
gui.enable = lib.mkOption { allowEncryption = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Server-side switch for matrix end-to-end encryption — sets
tuwunel's `allow_encryption`. Off by default: on the hive-internal
homeserver the operator already controls the transport, so server
E2EE adds key-management overhead (cross-signing, device
verification, undecryptable-message recovery) without a clear
threat-model win for the common single-hive case. Turn on when
agents join encrypted rooms on external / federated homeservers,
or when the operator wants message contents opaque to the
homeserver admin. Independent of the agent matrix client, which
always supports decryption so it can read encrypted rooms it is
invited to regardless of this flag; this option only governs
whether THIS homeserver permits room encryption.
'';
};
gui = {
enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = deployCfg.matrix.enable; default = deployCfg.matrix.enable;
defaultText = lib.literalExpression "config.services.hyperhive.deploy.matrix.enable"; defaultText = lib.literalExpression "config.services.hyperhive.deploy.matrix.enable";
description = '' description = ''
Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`. Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`.
Requires `swarm.matrix.gatewayHost != null` (default `matrix.<hive>` Requires `matrix.gatewayHost != null` (default `matrix.<hive>`
when hive-domain set); the gateway itself always runs. When when hive-domain set); the gateway itself always runs. When
off, the dashboard's `M4TR1X →` tab is hidden. See off, the dashboard's `M4TR1X →` tab is hidden. See
`docs/networking/gateway.md` for the discovery flow that lets clients `docs/networking/gateway.md` for the discovery flow that lets clients
auto-find the sub-domain. The client build itself is auto-find the sub-domain.
`swarm.matrix.gui.package` — which client, as opposed to whether
this host serves it.
''; '';
}; };
sso.clientSecretFile = lib.mkOption { package = lib.mkOption {
type = lib.types.package;
default = fluffychat-web-fixed;
defaultText = lib.literalMD ''
`pkgs.fluffychat-web` with a `postInstall` patch that adds
the three files `flutter341.buildFlutterApplication` skips.
'';
description = ''
Static web client dist served at `matrix.<hive>/`. Override
to swap fluffychat for hydrogen-web, cinny, element-web, or
an out-of-tree dist — any replacement is mounted at the
sub-domain root with the upstream-default `<base href "/">`,
no sub-path gymnastics needed.
'';
};
};
# This homeserver always delegates login to the swarm's authelia, as
# an OIDC relying party — matrix SSO (`m.login.sso`), offered
# alongside password login. No toggle: a homeserver in a swarm is a
# client of that swarm's identity provider.
#
# ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point
# the other way: those make this homeserver an *authorization server*
# for matrix clients. This family makes it a *client* of an external
# identity provider. The two share the protocol's name and answer
# opposite questions.
#
# This **adds** a way in. Password login keeps working: an identity
# provider that can take the homeserver offline when it hiccups is a
# worse homeserver than one with two ways in — which is also what
# makes always-on safe. Making authelia the *only* path is a
# separate, reversible switch (tuwunel's `login_with_password`),
# deliberately not folded in here.
#
# ⚠️ Matrix SSO lives **inside** the homeserver, never behind a
# forward-auth proxy: the client-server API is spoken by non-browser
# clients holding matrix access tokens — every agent's own
# `hive-matrix-daemon` — plus federation, and a proxy in front of
# `/_matrix/` breaks all of it.
sso = {
clientId = lib.mkOption {
type = lib.types.str;
default = "tuwunel";
description = ''
OAuth2 client id this homeserver identifies itself with. Must
match the `id` of the corresponding entry in
`services.hyperhive.swarm.authelia.oidc.clients`.
'';
};
clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "/var/lib/tuwunel-oidc/tuwunel.secret"; example = "/var/lib/tuwunel-oidc/tuwunel.secret";
@ -495,16 +479,16 @@ in
needs the plaintext), and a literal written here would be needs the plaintext), and a literal written here would be
rendered into the world-readable nix store. rendered into the world-readable nix store.
Deliberately no fallback — a homeserver that boots with SSO Required when `enable` is set — deliberately no fallback. A
half-configured is worse than one that fails to evaluate: homeserver that boots with SSO half-configured is worse than
tuwunel reads OIDC from its config file rather than a database one that fails to evaluate: tuwunel reads OIDC from its
row, so a malformed block can stop the server outright instead config file rather than a database row, so a malformed block
of merely hiding a button. The id this pairs with is can stop the server outright instead of merely hiding a
`swarm.matrix.sso.clientId`, which is swarm-wide because it must button.
match authelia's register.
''; '';
}; };
}; };
};
config = lib.mkIf deployCfg.matrix.enable { config = lib.mkIf deployCfg.matrix.enable {
# Matrix's own gateway surface: the sub-domain vhost, the name the # Matrix's own gateway surface: the sub-domain vhost, the name the
@ -536,7 +520,7 @@ in
# page). See `services.hyperhive.swarm.controller.links`'s # page). See `services.hyperhive.swarm.controller.links`'s
# description. # description.
services.hyperhive.swarm.controller.links = services.hyperhive.swarm.controller.links =
lib.optional (cfg.gatewayHost != null && deployCfg.matrix.gui.enable) lib.optional (cfg.gatewayHost != null && cfg.gui.enable)
{ {
label = "Matrix"; label = "Matrix";
icon = "💬"; icon = "💬";
@ -550,7 +534,7 @@ in
# contributes instead of replacing it. # contributes instead of replacing it.
# #
# The dashboard needs no equivalent — it routes by path. # The dashboard needs no equivalent — it routes by path.
services.nginx.appendHttpConfig = lib.optionalString deployCfg.matrix.gui.enable '' services.nginx.appendHttpConfig = lib.optionalString cfg.gui.enable ''
map $http_accept $matrix_spa_target { map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback"; default "/__matrix_spa_no_html_fallback";
"~*text/html" "/index.html"; "~*text/html" "/index.html";
@ -585,7 +569,7 @@ in
''; '';
}; };
} }
// lib.optionalAttrs deployCfg.matrix.gui.enable { // lib.optionalAttrs cfg.gui.enable {
# fluffychat at sub-domain root, SPA-fallback via the # fluffychat at sub-domain root, SPA-fallback via the
# Accept-header `$matrix_spa_target` map above. # Accept-header `$matrix_spa_target` map above.
"/" = { "/" = {
@ -604,7 +588,7 @@ in
''; '';
}; };
} }
// lib.optionalAttrs (!deployCfg.matrix.gui.enable) { // lib.optionalAttrs (!cfg.gui.enable) {
"/" = { "/" = {
return = "404"; return = "404";
}; };
@ -634,10 +618,10 @@ in
# Fail at EVAL, not at boot. tuwunel reads its identity providers # Fail at EVAL, not at boot. tuwunel reads its identity providers
# from the config file, so a half-configured one does not hide a # from the config file, so a half-configured one does not hide a
# login button — it can stop the homeserver from starting at all. # login button — it can stop the homeserver from starting at all.
assertion = deployCfg.matrix.sso.clientSecretFile != null; assertion = cfg.sso.clientSecretFile != null;
message = '' message = ''
This homeserver's SSO login flow requires This homeserver's SSO login flow requires
services.hyperhive.deploy.matrix.sso.clientSecretFile — the path services.hyperhive.swarm.matrix.sso.clientSecretFile — the path
(inside the matrix container) holding the OIDC client secret's (inside the matrix container) holding the OIDC client secret's
plaintext. plaintext.
@ -699,7 +683,7 @@ in
# Same case, same reasoning: this host minted the secret, so it can say # Same case, same reasoning: this host minted the secret, so it can say
# where the homeserver will find it. # where the homeserver will find it.
services.hyperhive.deploy.matrix.sso.clientSecretFile = lib.mkIf ssoLocal ( services.hyperhive.swarm.matrix.sso.clientSecretFile = lib.mkIf ssoLocal (
lib.mkDefault matrixSecretPath lib.mkDefault matrixSecretPath
); );
@ -745,7 +729,7 @@ in
set -euo pipefail set -euo pipefail
src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"} src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"}
dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString deployCfg.matrix.sso.clientSecretFile}"} dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString cfg.sso.clientSecretFile}"}
# authelia's container is up, but its first-boot generator may # authelia's container is up, but its first-boot generator may
# still be minting. Bounded wait, then fail: a silent skip here # still be minting. Bounded wait, then fail: a silent skip here
@ -827,7 +811,7 @@ in
); );
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] '' system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] ''
tokenFile=${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)}
if [ ! -s "$tokenFile" ]; then if [ ! -s "$tokenFile" ]; then
mkdir -p "$(dirname "$tokenFile")" mkdir -p "$(dirname "$tokenFile")"
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$tokenFile" head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$tokenFile"
@ -849,8 +833,8 @@ in
# Read-only bind of the host-managed registration token; tuwunel # Read-only bind of the host-managed registration token; tuwunel
# reads it via systemd LoadCredential below (not directly). # reads it via systemd LoadCredential below (not directly).
bindMounts = { bindMounts = {
${deployCfg.matrix.registrationTokenFile} = { ${cfg.registrationTokenFile} = {
hostPath = deployCfg.matrix.registrationTokenFile; hostPath = cfg.registrationTokenFile;
isReadOnly = true; isReadOnly = true;
}; };
} }
@ -939,11 +923,11 @@ in
# `address` + `port` are upstream `listOf` — wrap singles. # `address` + `port` are upstream `listOf` — wrap singles.
address = [ "0.0.0.0" ]; address = [ "0.0.0.0" ];
port = [ cfg.httpPort ]; port = [ cfg.httpPort ];
max_request_size = deployCfg.matrix.maxRequestSize; max_request_size = cfg.maxRequestSize;
# Federation enabled at the protocol level; empty # Federation enabled at the protocol level; empty
# trustedServers keeps it effectively closed. # trustedServers keeps it effectively closed.
allow_federation = true; allow_federation = true;
trusted_servers = deployCfg.matrix.trustedServers; trusted_servers = cfg.trustedServers;
# Token-gated registration. The absent # Token-gated registration. The absent
# `yes_i_am_very_very_sure_…_open_registration_…` flag # `yes_i_am_very_very_sure_…_open_registration_…` flag
# keeps the server closed to anyone without the token. # keeps the server closed to anyone without the token.
@ -1011,18 +995,18 @@ in
# host-side chown :tuwunel / GID-pin gymnastics needed. # host-side chown :tuwunel / GID-pin gymnastics needed.
# See `man systemd.exec` → LoadCredential. # See `man systemd.exec` → LoadCredential.
systemd.services.tuwunel.serviceConfig.LoadCredential = [ systemd.services.tuwunel.serviceConfig.LoadCredential = [
"registration_token:${toString deployCfg.matrix.registrationTokenFile}" "registration_token:${toString cfg.registrationTokenFile}"
# Same mechanism, second secret. tuwunel re-reads this file on # Same mechanism, second secret. tuwunel re-reads this file on
# every OAuth exchange, not just at startup, so it has to # every OAuth exchange, not just at startup, so it has to
# outlive the unit's start — a credentials path does. # outlive the unit's start — a credentials path does.
"oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}" "oidc_client_secret:${toString cfg.sso.clientSecretFile}"
]; ];
environment.systemPackages = [ cfg.package ]; environment.systemPackages = [ cfg.package ];
}; };
}; };
networking.firewall = lib.mkIf deployCfg.matrix.openFirewall { networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ allowedTCPPorts = [
cfg.httpPort cfg.httpPort
]; ];

Some files were not shown because too many files have changed in this diff Show more