Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7c5067f79 | ||
|
|
b6df0b4afc | ||
|
|
81b9ddd189 | ||
|
|
7003d14d2c | ||
|
|
1db2ac26a7 | ||
|
|
368f5d82aa |
33 changed files with 875 additions and 455 deletions
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
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.*`. Container
|
attachments). Configured via `services.hyperhive.swarm.forge.*` (what the
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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`) | `forge.behindGateway` |
|
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo.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 forge.behindGateway)
|
- `forge.<swarm>` → `127.0.0.1` (when deploy.forgejo.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 `forge.behindGateway`); `external`-kind links are
|
tracked by `deploy.forgejo.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,8 +335,10 @@ 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.
|
||||||
|
|
@ -357,7 +359,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 (`forge.behindGateway = true`) does
|
Forgejo served through the gateway (`deploy.forgejo.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.
|
||||||
|
|
||||||
|
|
@ -373,8 +375,8 @@ covers most cases:
|
||||||
|
|
||||||
| Shape | Auto-derived `ROOT_URL` |
|
| Shape | Auto-derived `ROOT_URL` |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
|
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
|
||||||
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
| `deploy.forgejo.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 =
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.wireguard = {
|
services.hyperhive.deploy.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 `swarm.wireguard.enable`, **not** on
|
Note that the mesh is gated on `deploy.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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 `forge.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 `deploy.forgejo.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.swarm.forge.mirrors`:
|
pull-mirror via `services.hyperhive.deploy.forgejo.mirrors`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.swarm.forge.mirrors = [
|
services.hyperhive.deploy.forgejo.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"; }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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) — `swarm.wireguard.enable` reads each
|
3. **WireGuard mesh** (optional) — `deploy.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 = {
|
||||||
swarm.wireguard = {
|
deploy.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 = {
|
||||||
swarm.wireguard = {
|
deploy.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";
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
`swarm.nats.calloutUserPublicKey`, is published to every client that connects,
|
`deploy.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 `swarm.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you |
|
| wireguard private key | **the operator** — `wg genkey` | whatever `deploy.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 `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` |
|
| 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` |
|
||||||
| 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
|
||||||
|
|
||||||
`nats.autoGenerateCallout` mints both keypairs on the host before the queue
|
`deploy.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 `calloutUserSeedFile` /
|
Keep both seeds at `0600` and name them in `deploy.nats.calloutUserSeedFile` /
|
||||||
`calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to
|
`deploy.nats.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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
||||||
| `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. |
|
| `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. |
|
||||||
|
|
||||||
**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
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.forge.behindGateway = true`; `matrix` needs
|
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
|
||||||
`services.hyperhive.swarm.matrix.gui.enable = true`. In those cases the command
|
`services.hyperhive.deploy.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.
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.matrix.gui.enable` is on (defaults to
|
`services.hyperhive.deploy.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.
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.matrix.gui.enable` is on). The gateway
|
/// when `services.hyperhive.deploy.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.forge.publicUrl` (defaults to
|
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
|
||||||
/// the gateway vhost URL when `forge.behindGateway = true`,
|
/// the gateway vhost URL when `deploy.forgejo.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
|
||||||
|
|
|
||||||
|
|
@ -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.forge.behindGateway = true`",
|
"the public forge URL needs `services.hyperhive.deploy.forgejo.behindGateway = true`",
|
||||||
),
|
),
|
||||||
OpenTarget::Matrix => (
|
OpenTarget::Matrix => (
|
||||||
urls.matrix,
|
urls.matrix,
|
||||||
"the matrix GUI URL needs `services.hyperhive.swarm.matrix.gui.enable = true`",
|
"the matrix GUI URL needs `services.hyperhive.deploy.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}"))?;
|
||||||
|
|
|
||||||
|
|
@ -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 swarm.wireguard nix options).
|
/// `privateKeyFile` example in the deploy.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.swarm.wireguard = {{");
|
println!(" services.hyperhive.deploy.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.swarm.wireguard.enable = true, then deploy)"
|
(services.hyperhive.deploy.wireguard.enable = true, then deploy)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,22 @@ 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" ]
|
||||||
|
|
@ -53,6 +69,22 @@ 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" ]
|
||||||
|
|
@ -98,6 +130,100 @@ 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
|
||||||
|
|
@ -148,6 +274,34 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -48,8 +47,9 @@ 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.
|
||||||
# ⚠️ `matrixCfg.machine` parses fine and fails at module-system resolution,
|
# ⚠️ A `swarm.matrix.machine` read parses fine and fails at module-system
|
||||||
# so this is the kind of mistake only reading the target module catches.
|
# resolution, so this is the kind of mistake only reading the target module
|
||||||
|
# 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 matrixCfg.registrationTokenFile)}
|
printf '%s\n' "$token" > ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)}
|
||||||
chmod 0600 ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)}
|
chmod 0600 ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.matrix.gui.enable {
|
// lib.optionalAttrs config.services.hyperhive.deploy.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 `behindGateway = true` and `null` otherwise
|
# gateway vhost URL when `deploy.forgejo.behindGateway = true` and `null`
|
||||||
# — see that option's doc for the "hide, don't guess" rationale.
|
# otherwise — 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.swarm.matrix.gui.enable
|
config.services.hyperhive.deploy.matrix.gui.enable
|
||||||
&& config.services.hyperhive.swarm.matrix.gatewayHost != null
|
&& config.services.hyperhive.swarm.matrix.gatewayHost != null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
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;
|
||||||
|
|
@ -79,8 +80,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.swarm.forge` because the runner is
|
Grouped under `services.hyperhive.deploy.forgejo` because the runner
|
||||||
tightly coupled to the forge instance it registers against.
|
is 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.
|
||||||
|
|
||||||
|
|
@ -153,17 +154,17 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# `forge.behindGateway = true` (the default) is required because the
|
# `deploy.forgejo.behindGateway = true` (the default) is required because
|
||||||
# CI container uses private networking and reaches the forge through
|
# the 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 = forgeCfg.behindGateway;
|
assertion = forgeDeployCfg.behindGateway;
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.deploy.forgejo.ci.enable requires
|
services.hyperhive.deploy.forgejo.ci.enable requires
|
||||||
services.hyperhive.swarm.forge.behindGateway = true.
|
services.hyperhive.deploy.forgejo.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
|
||||||
|
|
@ -216,8 +217,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 `forge.behindGateway = true` (asserted in the options
|
# Requires `deploy.forgejo.behindGateway = true` (asserted in the
|
||||||
# block above). See docs/networking/network.md.
|
# options block above). See docs/networking/network.md.
|
||||||
privateNetwork = true;
|
privateNetwork = true;
|
||||||
hostBridge = networkCfg.bridgeName;
|
hostBridge = networkCfg.bridgeName;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 cfg.behindGateway then
|
if deployCfg.forgejo.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 =
|
||||||
cfg.mirrors
|
deployCfg.forgejo.mirrors
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) deployCfg.forgejo.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.swarm.forge.sso.clientSecretFile and
|
false, set services.hyperhive.deploy.forgejo.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 `behindGateway = true`
|
gateway vhost server-name when `deploy.forgejo.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,7 +201,10 @@ 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 && cfg.behindGateway then "https://${cfg.domain}" else null;
|
if config.services.hyperhive.enable && deployCfg.forgejo.behindGateway then
|
||||||
|
"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
|
||||||
'';
|
'';
|
||||||
|
|
@ -212,7 +215,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 `behindGateway =
|
Defaults to `https://''${cfg.domain}` when `deploy.forgejo.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
|
||||||
|
|
@ -221,7 +224,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 `behindGateway = false`** and the
|
**Set this explicitly if `deploy.forgejo.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
|
||||||
|
|
@ -244,6 +247,60 @@ 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;
|
||||||
|
|
@ -274,29 +331,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:
|
|
||||||
|
|
||||||
- `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;
|
||||||
|
|
@ -312,8 +346,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.swarm.forge.openFirewall = true;` to your host
|
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your
|
||||||
config before rebuilding.
|
host config before rebuilding.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -360,27 +394,10 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# The swarm's authelia is always registered as an OpenID Connect
|
# The other half of the SSO pair: the client *id* is swarm-wide (it has
|
||||||
# login source here — there is no toggle, for the same reason the
|
# to match authelia's register), the secret is a path on this host. The
|
||||||
# forge itself has none.
|
# design note for the login source itself is with the id.
|
||||||
#
|
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";
|
||||||
|
|
@ -399,7 +416,6 @@ in
|
||||||
eval error.
|
eval error.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
hostSwarmControllerTokenFile = lib.mkOption {
|
hostSwarmControllerTokenFile = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
@ -443,13 +459,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 cfg.behindGateway cfg.domain;
|
services.hyperhive.gateway.localNames = lib.optional deployCfg.forgejo.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 cfg.behindGateway {
|
services.hyperhive.swarm.controller.links = lib.optional deployCfg.forgejo.behindGateway {
|
||||||
label = "Forge";
|
label = "Forge";
|
||||||
icon = "⚒";
|
icon = "⚒";
|
||||||
url = "https://${cfg.domain}/";
|
url = "https://${cfg.domain}/";
|
||||||
|
|
@ -466,7 +482,9 @@ 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 = lib.optionalAttrs cfg.behindGateway {
|
services.hyperhive.swarm.otel.publishedScrapeTargets =
|
||||||
|
lib.optionalAttrs deployCfg.forgejo.behindGateway
|
||||||
|
{
|
||||||
forgejo = "https://${cfg.domain}/metrics";
|
forgejo = "https://${cfg.domain}/metrics";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -474,7 +492,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 cfg.behindGateway {
|
services.nginx.virtualHosts = lib.optionalAttrs deployCfg.forgejo.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;
|
||||||
|
|
@ -543,10 +561,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 = cfg.sso.clientSecretFile != null;
|
assertion = deployCfg.forgejo.sso.clientSecretFile != null;
|
||||||
message = ''
|
message = ''
|
||||||
The forge's SSO login source requires
|
The forge's SSO login source requires
|
||||||
services.hyperhive.swarm.forge.sso.clientSecretFile — the path
|
services.hyperhive.deploy.forgejo.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.
|
||||||
|
|
||||||
|
|
@ -600,7 +618,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.swarm.forge.mirrors[].dest must be exactly
|
Every services.hyperhive.deploy.forgejo.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}
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
@ -618,7 +636,7 @@ in
|
||||||
])
|
])
|
||||||
) effectiveMirrors;
|
) effectiveMirrors;
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.swarm.forge.mirrors[].dest must not place a mirror
|
services.hyperhive.deploy.forgejo.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").
|
||||||
|
|
@ -771,7 +789,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 = cfg.behindGateway;
|
metrics.ENABLED = deployCfg.forgejo.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
|
||||||
|
|
@ -790,8 +808,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 = cfg.behindGateway;
|
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway;
|
||||||
metrics.ENABLED_ISSUE_BY_REPOSITORY = cfg.behindGateway;
|
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.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
|
||||||
|
|
@ -1025,9 +1043,9 @@ in
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
|
secret=$(cat ${lib.escapeShellArg deployCfg.forgejo.sso.clientSecretFile})
|
||||||
if [ -z "$secret" ]; then
|
if [ -z "$secret" ]; then
|
||||||
echo "empty OIDC client secret at ${cfg.sso.clientSecretFile}" >&2
|
echo "empty OIDC client secret at ${deployCfg.forgejo.sso.clientSecretFile}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1204,7 +1222,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.swarm.forge.sso.clientSecretFile = lib.mkIf ssoLocal (
|
services.hyperhive.deploy.forgejo.sso.clientSecretFile = lib.mkIf ssoLocal (
|
||||||
lib.mkDefault forgeSecretPath
|
lib.mkDefault forgeSecretPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1298,7 +1316,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 cfg.hostSwarmControllerTokenFile}
|
dst=${lib.escapeShellArg deployCfg.forgejo.hostSwarmControllerTokenFile}
|
||||||
|
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
[ -s "$src" ] && break
|
[ -s "$src" ] && break
|
||||||
|
|
@ -1313,7 +1331,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf deployCfg.forgejo.openFirewall {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
cfg.httpPort
|
cfg.httpPort
|
||||||
cfg.sshPort
|
cfg.sshPort
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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
|
||||||
|
|
@ -104,6 +105,7 @@ let
|
||||||
cfg
|
cfg
|
||||||
errorPages
|
errorPages
|
||||||
matrixCfg
|
matrixCfg
|
||||||
|
matrixDeployCfg
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
dashboardDist
|
dashboardDist
|
||||||
swaggerUiTheme
|
swaggerUiTheme
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
matrixCfg,
|
matrixCfg, # services.hyperhive.swarm.matrix — what the homeserver IS
|
||||||
|
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
|
||||||
|
|
@ -44,7 +45,8 @@ 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 (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null)
|
lib.optionalAttrs
|
||||||
|
(matrixDeployCfg.enable && matrixDeployCfg.gui.enable && matrixCfg.gatewayHost != null)
|
||||||
(
|
(
|
||||||
let
|
let
|
||||||
target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
|
target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
|
||||||
|
|
@ -62,7 +64,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 matrixCfg.enable (
|
wellKnownLocations = lib.optionalAttrs matrixDeployCfg.enable (
|
||||||
let
|
let
|
||||||
clientBaseUrl =
|
clientBaseUrl =
|
||||||
if matrixCfg.gatewayHost != null then
|
if matrixCfg.gatewayHost != null then
|
||||||
|
|
|
||||||
|
|
@ -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.swarm.matrix.sso.clientSecretFile and
|
set services.hyperhive.deploy.matrix.sso.clientSecretFile and
|
||||||
services.hyperhive.swarm.authelia.url as the assertions describe.
|
services.hyperhive.swarm.authelia.url as the assertions describe.
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
@ -312,6 +312,91 @@ 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;
|
||||||
|
|
@ -327,7 +412,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.swarm.matrix.openFirewall = true;` to your host
|
`services.hyperhive.deploy.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
|
||||||
|
|
@ -381,92 +466,23 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
allowEncryption = lib.mkOption {
|
gui.enable = 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 `matrix.gatewayHost != null` (default `matrix.<hive>`
|
Requires `swarm.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.
|
auto-find the sub-domain. The client build itself is
|
||||||
|
`swarm.matrix.gui.package` — which client, as opposed to whether
|
||||||
|
this host serves it.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
package = lib.mkOption {
|
sso.clientSecretFile = 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";
|
||||||
|
|
@ -479,16 +495,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.
|
||||||
|
|
||||||
Required when `enable` is set — deliberately no fallback. A
|
Deliberately no fallback — a homeserver that boots with SSO
|
||||||
homeserver that boots with SSO half-configured is worse than
|
half-configured is worse than one that fails to evaluate:
|
||||||
one that fails to evaluate: tuwunel reads OIDC from its
|
tuwunel reads OIDC from its config file rather than a database
|
||||||
config file rather than a database row, so a malformed block
|
row, so a malformed block can stop the server outright instead
|
||||||
can stop the server outright instead of merely hiding a
|
of merely hiding a button. The id this pairs with is
|
||||||
button.
|
`swarm.matrix.sso.clientId`, which is swarm-wide because it must
|
||||||
|
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
|
||||||
|
|
@ -520,7 +536,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 && cfg.gui.enable)
|
lib.optional (cfg.gatewayHost != null && deployCfg.matrix.gui.enable)
|
||||||
{
|
{
|
||||||
label = "Matrix";
|
label = "Matrix";
|
||||||
icon = "💬";
|
icon = "💬";
|
||||||
|
|
@ -534,7 +550,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 cfg.gui.enable ''
|
services.nginx.appendHttpConfig = lib.optionalString deployCfg.matrix.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";
|
||||||
|
|
@ -569,7 +585,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs cfg.gui.enable {
|
// lib.optionalAttrs deployCfg.matrix.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.
|
||||||
"/" = {
|
"/" = {
|
||||||
|
|
@ -588,7 +604,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (!cfg.gui.enable) {
|
// lib.optionalAttrs (!deployCfg.matrix.gui.enable) {
|
||||||
"/" = {
|
"/" = {
|
||||||
return = "404";
|
return = "404";
|
||||||
};
|
};
|
||||||
|
|
@ -618,10 +634,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 = cfg.sso.clientSecretFile != null;
|
assertion = deployCfg.matrix.sso.clientSecretFile != null;
|
||||||
message = ''
|
message = ''
|
||||||
This homeserver's SSO login flow requires
|
This homeserver's SSO login flow requires
|
||||||
services.hyperhive.swarm.matrix.sso.clientSecretFile — the path
|
services.hyperhive.deploy.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.
|
||||||
|
|
||||||
|
|
@ -683,7 +699,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.swarm.matrix.sso.clientSecretFile = lib.mkIf ssoLocal (
|
services.hyperhive.deploy.matrix.sso.clientSecretFile = lib.mkIf ssoLocal (
|
||||||
lib.mkDefault matrixSecretPath
|
lib.mkDefault matrixSecretPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -729,7 +745,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 cfg.sso.clientSecretFile}"}
|
dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString deployCfg.matrix.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
|
||||||
|
|
@ -811,7 +827,7 @@ in
|
||||||
);
|
);
|
||||||
|
|
||||||
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] ''
|
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] ''
|
||||||
tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)}
|
tokenFile=${lib.escapeShellArg (toString deployCfg.matrix.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"
|
||||||
|
|
@ -833,8 +849,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 = {
|
||||||
${cfg.registrationTokenFile} = {
|
${deployCfg.matrix.registrationTokenFile} = {
|
||||||
hostPath = cfg.registrationTokenFile;
|
hostPath = deployCfg.matrix.registrationTokenFile;
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -923,11 +939,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 = cfg.maxRequestSize;
|
max_request_size = deployCfg.matrix.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 = cfg.trustedServers;
|
trusted_servers = deployCfg.matrix.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.
|
||||||
|
|
@ -995,18 +1011,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 cfg.registrationTokenFile}"
|
"registration_token:${toString deployCfg.matrix.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 cfg.sso.clientSecretFile}"
|
"oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}"
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = [ cfg.package ];
|
environment.systemPackages = [ cfg.package ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf deployCfg.matrix.openFirewall {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
cfg.httpPort
|
cfg.httpPort
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue