Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

...
Author SHA1 Message Date
atlas
b7c5067f79 deploy: move the controller's socket and credentials out of swarm.controller
`swarm.*` is what a hive needs to be a *client* of the swarm. For the
controller that is the quick-links it publishes, the client id it is
registered under, how stale a snapshot may read, and its packages. Where
its socket lives and where the three credentials it reads sit are
decisions of the machine running it, so socketPath, forgeTokenFile,
authBridgeUrl and queue.clientSecretFile move to
`deploy.swarm-controller.*`.

⚠️ `deploy.swarm-controller`, not `deploy.hive-controller`. Both exist on
main — the latter is hive-c0re's namespace, where `tls.*` lives — and a
definition on the wrong one lands on a live unrelated option instead of
failing. The shim table is anchored on both sides for that reason.

`queue.clientSecretFile` is a nested field, not a top-level option: it
moves out of `queue` and leaves natsUrl and tokenEndpoint behind, so the
far side needs `deploy.swarm-controller.queue.clientSecretFile`. That is
the `forge.sso` split shape, applied again rather than reinvented — the
endpoints a client dials are swarm-wide, the secret beside them is a path
on one host.

local-defaults.nix set that secret from INSIDE
`config.services.hyperhive.swarm = { ... }`, where a bare `controller.`
prefix means `swarm.controller`. Left there it would still resolve —
through the rename — and warn on every evaluation of a single-host swarm,
which is the same defect an earlier slice fixed for the matrix module. It
moves out into its own `deploy` statement beside the controller's
`enable`. `queue.natsUrl` stays bare: it is a stayer.

swarm-ui.nix read `socketPath` through its own `controllerCfg` alias.
With that repointed the binding had no reader left, so it goes. Unlike
the dead `natsCfg` an earlier slice removed, this one was live until this
commit — the move orphaned it. `deployCfg` was already bound there.

Prose that named a moved option by its full path is requalified: the
assertion message for the queue secret (operator-facing, and inside
`config` where an options-block sweep would miss it) and
`swarm-controller/README.md`'s socket-path line. Left bare on purpose:
`docs/swarm/README.md` and the README's own "never point `socketPath` at
a directory that carries anything else" both name the FEATURE, not a
path.

module-eval configures a hive through all four OLD paths and asserts a
rendered effect for EACH of them — the unit's socket env, its auth-bridge
env, and both credential paths in `LoadCredential`. Asserting all four
rather than one means a rename that resolves but stops reaching the
module is caught per-option, not only where a single assertion happens to
look.
2026-09-07 14:24:52 +02:00
atlas
b6df0b4afc deploy: move grafana's datasources, plugins and socket out of swarm.grafana
`swarm.*` is what a hive needs to be a *client* of the swarm. For Grafana
that is the package, the name it is served under, the port its `/metrics`
is re-served on, and the OIDC client it is registered as. Where its
datasources point, which plugins sit in its store path and the directory
it shares a socket with nginx through are decisions of the machine
running it, so datasourceUrl, logsDatasourceUrl, plugins and socketDir
move to `deploy.grafana.*`.

The two URLs are the interesting half. `swarm-grafana.nix`'s own summary
sentence said what stays is "its package, domain, and wiring" — and both
datasource URLs ARE wiring, so that sentence is rewritten with the move
rather than left asserting the opposite of what the module does. They
move because a URL's scope is the scope of what it ADDRESSES, not the
fact that it is a URL: docs/swarm/services.md already said datasourceUrl
"defaults to the store on this host, which is the only thing it can
reach", because that store binds loopback. The doc argues the move.

`socketDir` was already ruled host-side: the directory is shared between
the host's nginx and the container, and it is the same shape as the
options that moved in earlier slices.

Declared in swarm-grafana.nix under the `deploy.*` path, following
swarm-nats.nix; deploy.nix carries only the renames. One reader outside
the options block is prose: `metricsPort` STAYS and cross-referenced
`{option}services.hyperhive.swarm.grafana.socketDir`, which the split
makes a pointer to another rendered page — nixosOptionsDoc emits
swarm.md and deploy.md separately. Requalified, along with the one line
in docs/swarm/services.md that named a mover.

module-eval configures a hive through all four OLD paths and asserts a
rendered effect: the host tmpfiles rule that creates the socket
directory carries the fixture's custom path. The new paths evaluate fine
without the shims, so dropping them has to read as a clean tree; all
four are defined in the fixture, so removing any single shim fails the
eval rather than only the one the assertion reads.
2026-09-07 14:24:52 +02:00
atlas
81b9ddd189 deploy: move the queue's callout identity out of swarm.nats
`swarm.*` is what a hive needs to be a *client* of the swarm. For the
queue that is the ports it answers on, the client id it is registered
under, and the responder package. Whether this host mints its own
auth-callout keypairs and where the seeds sit are decisions of the
machine running it, so autoGenerateCallout, both seed files and both
public keys move to `deploy.nats.*`.

The two PUBLIC keys are the non-obvious half, so the reasoning is here
rather than in a comment nobody re-reads. A public key looks swarm-wide,
and docs/swarm/secrets.md says the user key "is published to every
client that connects" — which is true and does not make it swarm
config: peers RECEIVE it over the wire at connect time, they never
configure it. What decides the placement is that its seed is
unambiguously host-side, and splitting a keypair across two namespaces
is worse than either placement.

local-defaults.nix set `nats.autoGenerateCallout` from INSIDE
`config.services.hyperhive.swarm = { ... }`, so the bare prefix meant
`swarm.nats` and no full-path grep could see it. It moves out of that
block into a `deploy` statement rather than being rewritten in place.

swarm-controller.nix bound `natsCfg` and never used it — one mention,
no bare pass, while its sibling bindings have 5 and 10. Pre-existing
dead code, found by this slice's own alias sweep, removed with it.

Four assertion messages and five doc lines named moved options by a
short form (`nats.calloutUserSeedFile`) that is ambiguous now that both
`swarm.nats` and `deploy.nats` exist; one opened with
"services.hyperhive.swarm.nats has callout public keys", which the split
makes false. Prose that names a VALUE rather than a path — the `nk`
pipeline's `# -> calloutUserPublicKey` — is left bare on purpose.

module-eval configures a hive through all five OLD paths and asserts the
responder's delivery unit exists and interpolates the seed path.
`autoGenerateCallout` is deliberately FALSE there: that is what makes
the seed options decide `responderConfigured`, so the assertion is about
the seeds rather than about the auto-mint branch.
2026-09-07 14:24:52 +02:00
atlas
7003d14d2c deploy: split the homeserver's host decisions out of swarm.matrix
`swarm.*` is what a hive needs to be a *client* of the swarm. For the
homeserver that is what it IS from anywhere: its package, the name it
answers to, the ports and URLs it is reached on, and the client id it is
registered under. Whether it is exposed, which peers it trusts, how large
a request it accepts and where its host-local secrets sit are decisions
of the machine running it, so openFirewall, trustedServers,
maxRequestSize, registrationTokenFile, gui.enable and
sso.clientSecretFile move to `deploy.matrix.*`.

Two sub-blocks split rather than moving whole, on their own evidence.
`gui.enable` is whether THIS host serves the web client; `gui.package` is
which client, an artifact identity, and stays. `sso.clientSecretFile` is a
path on one host; `clientId` must match the id in authelia's register, so
it is swarm-wide. Each half now points at the other, because the rendered
docs put them on separate pages.

hive-gateway passed the whole `swarm.matrix` attrset into vhosts.nix, so
that file read a moving option through an argument with no option path
anywhere in it. It now takes `matrixDeployCfg` beside `matrixCfg` — the
only shape that carries a split namespace across that boundary.

While there: vhosts.nix read `matrixCfg.enable`, which has been a rename
alias for `deploy.matrix.enable` since the enable moved. Reading it made
the module system print `Obsolete option services.hyperhive.swarm.matrix.
enable is used` on EVERY evaluation of every host — a deprecation warning
no operator could silence, because the config tripping it was ours. That
shim lives in hive-matrix.nix rather than in this file's table, which is
why deploy.nix's header claim to be their single home is now qualified
in the new block's comment.

glue-matrix-bao-token.nix read the registration token through its own
`matrixCfg` alias; with that read repointed, the binding had no reader
left, so it goes, and the comment naming it is reworded.

module-eval gains a case configuring a hive through all six OLD paths and
asserting two rendered effects — the host firewall's port list and the
container's bind-mount table — because the new paths evaluate fine
without the shims. `gui.enable` is set to the opposite of its default so
the definition has to land rather than agreeing with it by accident.
2026-09-07 14:24:52 +02:00
atlas
1db2ac26a7 deploy: split the forge's host decisions out of swarm.forge
`swarm.*` is what a hive needs to be a *client* of the swarm. For the
forge that is what it 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. How it is served, what it mirrors and where its
host-local secrets sit are decisions of the machine running it, so
behindGateway, openFirewall, mirrors, sso.clientSecretFile and
hostSwarmControllerTokenFile move to `deploy.forgejo.*`.

Unlike the wireguard mesh this SPLITS a module rather than relocating a
whole namespace. `sso` splits with it: `clientId` stays because it must
match the id in authelia's register, while the secret beside it is a
path on one host. Moving the whole `sso` block for symmetry with `ci`
was considered and rejected on exactly that asymmetry.

Declared in hive-forge/default.nix under the `deploy.*` path, following
swarm-victorialogs.nix; deploy.nix carries only the renames. `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,
where `ci` needed five because it is a plain attrset of options.

Readers outside the module: hive-ci.nix binds `deploy.forgejo` for its
behindGateway assertion; swarm-authelia.nix and swarm-controller.nix
read theirs off the `deployCfg` they already bind. hivectl's `open`
printed `services.hyperhive.forge.behindGateway` in an operator-facing
hint — a path that never existed, missing `swarm.` — and hive-c0re's
state_snapshot doc comment carried the same defect; both now name the
new path. The rendered docs put the two halves on separate pages, so
the five descriptions of staying options that explain themselves in
terms of `behindGateway` now qualify it in full.

module-eval gains a forge case configured entirely through the old
paths, asserting the rendered firewall ports and the mirror env var
c0re seeds from: the new paths evaluate fine without the shims, so
dropping them reads as a clean tree. All five old paths are defined in
the fixture, so removing any single shim entry fails the eval rather
than only the two the assertion reads.
2026-09-07 14:24:52 +02:00
atlas
368f5d82aa deploy: move the wireguard mesh out of the namespace hives read
`swarm.*` is what a hive needs to be a *client* of the swarm; the mesh is
none of it. A peer needs this host's `wireguardEndpoint` -- the roster entry
in swarm.nix, which stays -- and nothing about the interface this host
brings up. The module already said so: "plain host networking that a machine
which runs no hive at all still needs."

All five options move, so the namespace relocates rather than splitting.
`listenPort` is the one that reads the other way: it is what this host
*binds*, while the port a peer *dials* lives inside `wireguardEndpoint`.

Declared in swarm-wireguard.nix under the `deploy.*` path, following
swarm-victorialogs.nix; deploy.nix carries only the renames, per its own
"a single file to delete when the deprecation window closes". Deliberately
NOT added to deploy.nix's own options block: every entry there is a swarm
service this host deploys, and the mesh is host networking.

hivectl/src/wg.rs generates the config snippet an operator pastes, so it
moves too -- otherwise the tool's own output trips the deprecation warning.

module-eval gains a case that configures a host through the OLD path and
asserts the rendered wg-hive interface, because the new path evaluates
fine without the shim: dropping it reads as a clean tree.
2026-09-07 14:24:52 +02:00
33 changed files with 875 additions and 455 deletions

View file

@ -2,7 +2,11 @@
Private Forgejo instance running in a nixos-container, used as the
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
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the
per-agent integration story and the notification pump that wakes

View file

@ -10,7 +10,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
| `<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>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
| `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`:
- `<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)
- `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
`DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still
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
frontend-side derivation.
@ -335,8 +335,10 @@ container.
services.hyperhive.swarm.forge = {
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
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.
@ -357,7 +359,7 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
(not behind the gateway).
- 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
that path.
@ -373,8 +375,8 @@ covers most cases:
| Shape | Auto-derived `ROOT_URL` |
|---|---|
| `behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
The gateway always terminates TLS, so the `behindGateway = true` case is
always advertised over `https://`; only the direct (`behindGateway =

View file

@ -26,7 +26,7 @@ services.hyperhive.snapshotStore = {
};
# The mesh is a hard requirement, and is asserted:
services.hyperhive.swarm.wireguard = {
services.hyperhive.deploy.wireguard = {
enable = true;
address = "10.100.0.9/24";
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
[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
`wg-hive` interface at all.

View file

@ -100,7 +100,7 @@ build can't hold the runner's single slot indefinitely).
## 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).
- **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.
@ -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.
**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
services.hyperhive.swarm.forge.mirrors = [
services.hyperhive.deploy.forgejo.mirrors = [
{ upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }
{ upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; }
];

View file

@ -168,7 +168,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
issue. Until then, federation needs CA-issued certs (ACME). See
`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`
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
# hive A (pr1ma.example.com, mesh IP 10.100.0.1)
services.hyperhive = {
swarm.wireguard = {
deploy.wireguard = {
enable = true;
privateKeyFile = "/etc/wireguard/hive.key";
address = "10.100.0.1/24";
@ -237,7 +237,7 @@ services.hyperhive = {
# hive B (edge.corp, mesh IP 10.100.0.2)
services.hyperhive = {
swarm.wireguard = {
deploy.wireguard = {
enable = true;
privateKeyFile = "/etc/wireguard/hive.key";
address = "10.100.0.2/24";

View file

@ -14,7 +14,7 @@ than a convenience.
The rule is about what must stay secret, not about credentials generally.
**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.
**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` |
| 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 |
| wireguard private key | **the operator** — `wg genkey` | whatever `swarm.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you |
| queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `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` |
| 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 `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 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
`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
topology where the queue, its responder and the operator are the same person. On
every other topology, mint them yourself:
@ -102,8 +102,8 @@ nk -inkey callout-user.seed -pubout # → calloutUserPublicKey
nk -inkey issuer.seed -pubout # → calloutIssuerPublicKey
```
Keep both seeds at `0600` and name them in `calloutUserSeedFile` /
`calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to
Keep both seeds at `0600` and name them in `deploy.nats.calloutUserSeedFile` /
`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
else.

View file

@ -116,7 +116,7 @@ See `retentionPeriod` below before leaving it at its default.
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victoriametrics.retentionPeriod` | Default `5y`. Lower it once you have measured how fast this swarm actually fills a disk — the default is deliberately generous because too-short silently discards history you cannot get back. |
| `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
authelia ones — there is no separate Grafana password, and the local

View file

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

View file

@ -627,7 +627,7 @@ A static matrix web client (default `pkgs.fluffychat-web` rebuilt
with `--base-href /matrix/`, swappable via
`services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway
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
`HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in
`/api/state`; the gateway does the actual static serving.

View file

@ -75,7 +75,7 @@ pub(super) struct StateSnapshot {
forge_present: bool,
/// Whether the matrix GUI is reachable at `/matrix/`. Sourced from
/// `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;
/// this flag is just an availability signal for iris's dashboard
/// 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.
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
/// sets from `services.hyperhive.forge.publicUrl` (defaults to
/// the gateway vhost URL when `forge.behindGateway = true`,
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
/// the gateway vhost URL when `deploy.forgejo.behindGateway = true`,
/// `null` otherwise). `None` when absent — the frontend **hides**
/// forge links rather than guessing `http://<hostname>:3000`,
/// which is only right by accident on deployments that aren't

View file

@ -26,11 +26,11 @@ pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
),
OpenTarget::Forge => (
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 => (
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}"))?;

View file

@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, bail};
use crate::util::query_hive_urls;
/// 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";
/// The mesh interface name hive-c0re's nix module brings up.
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!(" {pubkey}");
println!("\nAdd to this hive's NixOS config:");
println!(" services.hyperhive.swarm.wireguard = {{");
println!(" services.hyperhive.deploy.wireguard = {{");
println!(" enable = true;");
println!(" privateKeyFile = \"{WG_KEY_PATH}\";");
println!(" address = \"{addr}\";");
@ -238,7 +238,7 @@ pub(crate) fn wg_status() -> Result<()> {
if !status.success() {
bail!(
"`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(())

View file

@ -41,6 +41,22 @@ in
[ "services" "hyperhive" "swarm" "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
[ "services" "hyperhive" "swarm" "victoriametrics" "enable" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "enable" ]
@ -53,6 +69,22 @@ in
[ "services" "hyperhive" "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
[ "services" "hyperhive" "swarm" "ui" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-ui" "enable" ]
@ -98,6 +130,100 @@ in
[ "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
# decision of the host running the store rather than something the swarm
# agrees on. The two stores keep everything else — package, domain, port
@ -148,6 +274,34 @@ in
[ "services" "hyperhive" "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

View file

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

View file

@ -200,7 +200,7 @@ in
# one.
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`.
# Matrix GUI lives entirely on the gateway nginx (matrix tab
# 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
# host, e.g. through the gateway or a reverse proxy). Sourced from
# `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the
# gateway vhost URL when `behindGateway = true` and `null` otherwise
# — see that option's doc for the "hide, don't guess" rationale.
# gateway vhost URL when `deploy.forgejo.behindGateway = true` and `null`
# otherwise — see that option's doc for the "hide, don't guess" rationale.
# Absent here whenever `publicUrl` is `null`; the dashboard hides
# forge links rather than emitting one it can't justify.
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
@ -229,7 +229,7 @@ in
//
lib.optionalAttrs
(
config.services.hyperhive.swarm.matrix.gui.enable
config.services.hyperhive.deploy.matrix.gui.enable
&& config.services.hyperhive.swarm.matrix.gatewayHost != null
)
{

View file

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

View file

@ -75,7 +75,7 @@ let
# links. Operators can still override via `cfg.rootUrl` for bespoke
# shapes.
defaultRootUrl =
if cfg.behindGateway then
if deployCfg.forgejo.behindGateway then
let
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
in
@ -98,9 +98,9 @@ let
# operator hasn't already declared that dest themselves (else CI-on +
# an explicit `actions/checkout` entry would duplicate it).
effectiveMirrors =
cfg.mirrors
deployCfg.forgejo.mirrors
++ lib.optional (
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) deployCfg.forgejo.mirrors)
) actionCheckoutMirror;
in
{
@ -133,7 +133,7 @@ in
Removed rather than defaulted to true so a config that turned it
OFF fails here, where the line is, instead of silently gaining a
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.
'')
];
@ -178,7 +178,7 @@ in
description = ''
Public hostname for the forge. Doubles as both the forgejo
`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`).
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
@ -201,7 +201,10 @@ in
publicUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
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 ''
if behindGateway then "https://''${domain}" else null
'';
@ -212,7 +215,7 @@ in
the approval-queue's "review PR on forge" link) — sourced into
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)
and `null` otherwise. When `null`, the dashboard **hides**
forge links rather than guessing one — see
@ -221,7 +224,7 @@ in
container port is only an accident away from wrong on any
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
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
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 {
type = lib.types.bool;
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 {
type = lib.types.bool;
default = false;
@ -312,8 +346,8 @@ in
**Breaking change**: this used to default to `true`. If you
relied on the old default for external reach, add
`services.hyperhive.swarm.forge.openFirewall = true;` to your host
config before rebuilding.
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your
host config before rebuilding.
'';
};
@ -360,27 +394,10 @@ in
'';
};
# 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`.
'';
};
clientSecretFile = lib.mkOption {
# The other half of the SSO pair: the client *id* is swarm-wide (it has
# to match authelia's register), the secret is a path on this host. The
# design note for the login source itself is with the id.
sso.clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/hyperhive/forge-oidc-secret";
@ -399,7 +416,6 @@ in
eval error.
'';
};
};
hostSwarmControllerTokenFile = lib.mkOption {
type = lib.types.str;
@ -443,13 +459,13 @@ in
# Both halves are gated on `behindGateway`: with it off the operator
# fronts forgejo themselves, so this hive must neither claim the
# 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
# vhost/DNS name above — with it off, this host doesn't actually
# serve `cfg.domain`, so linking to it would be dead. See
# `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";
icon = "⚒";
url = "https://${cfg.domain}/";
@ -466,7 +482,9 @@ in
# what authelia compares against — this string agreeing with the
# `location` block above it is the whole mechanism. A near miss is a
# 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";
};
@ -474,7 +492,7 @@ in
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
# clones). SSH stays direct on `forge.sshPort`. See
# `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) // {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
@ -543,10 +561,10 @@ in
{
# Fail at EVAL, not at boot. The alternative failure is a login
# button that always 401s, three layers from the missing file.
assertion = cfg.sso.clientSecretFile != null;
assertion = deployCfg.forgejo.sso.clientSecretFile != null;
message = ''
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
plaintext.
@ -600,7 +618,7 @@ in
# splits on the single slash to create the org + repo.
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
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}
'';
}
@ -618,7 +636,7 @@ in
])
) effectiveMirrors;
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) —
those are provisioned by hive-c0re and a mirror there would
collide. Use a dedicated org (e.g. "actions/checkout").
@ -771,7 +789,7 @@ in
# scraper at the gateway, so a second credential system per
# service would buy nothing and would be the one that stops
# getting rotated.
metrics.ENABLED = cfg.behindGateway;
metrics.ENABLED = deployCfg.forgejo.behindGateway;
# The two per-dimension breakdowns, on the same condition as
# the endpoint itself: `gitea_issues_by_label{label=…}` and
# `gitea_issues_by_repository{repository=…}`. Off by default
@ -790,8 +808,8 @@ in
# forge that grew to thousands of repos would want this
# revisited — that is a real trigger, unlike a time-based one:
# `count(gitea_issues_by_repository)` answers it directly.
metrics.ENABLED_ISSUE_BY_LABEL = cfg.behindGateway;
metrics.ENABLED_ISSUE_BY_REPOSITORY = cfg.behindGateway;
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway;
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.behindGateway;
# Repo migrations / pull-mirrors fetch from the source
# URL *inside* Forgejo. hyperhive code is synced from
# `localhost` (and the host LAN), which Forgejo's
@ -1025,9 +1043,9 @@ in
script = ''
set -euo pipefail
secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
secret=$(cat ${lib.escapeShellArg deployCfg.forgejo.sso.clientSecretFile})
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
fi
@ -1204,7 +1222,7 @@ in
# Same case, same reasoning: this host minted the secret, so it can
# 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
);
@ -1298,7 +1316,7 @@ in
set -euo pipefail
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
[ -s "$src" ] && break
@ -1313,7 +1331,7 @@ in
'';
};
networking.firewall = lib.mkIf cfg.openFirewall {
networking.firewall = lib.mkIf deployCfg.forgejo.openFirewall {
allowedTCPPorts = [
cfg.httpPort
cfg.sshPort

View file

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

View file

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

View file

@ -178,7 +178,7 @@ in
Removed rather than defaulted to true so a config that turned it
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,
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.
'')
];
@ -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 {
type = lib.types.bool;
default = false;
@ -327,7 +412,7 @@ in
**Breaking change**: this used to default to `true`. If you
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.
Note: federation (the matrix-spec well-known port 8448) is
@ -381,92 +466,23 @@ 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 = {
enable = lib.mkOption {
gui.enable = lib.mkOption {
type = lib.types.bool;
default = deployCfg.matrix.enable;
defaultText = lib.literalExpression "config.services.hyperhive.deploy.matrix.enable";
description = ''
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
off, the dashboard's `M4TR1X →` tab is hidden. See
`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 {
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 {
sso.clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/tuwunel-oidc/tuwunel.secret";
@ -479,16 +495,16 @@ in
needs the plaintext), and a literal written here would be
rendered into the world-readable nix store.
Required when `enable` is set — deliberately no fallback. A
homeserver that boots with SSO half-configured is worse than
one that fails to evaluate: tuwunel reads OIDC from its
config file rather than a database row, so a malformed block
can stop the server outright instead of merely hiding a
button.
Deliberately no fallback — a homeserver that boots with SSO
half-configured is worse than one that fails to evaluate:
tuwunel reads OIDC from its config file rather than a database
row, so a malformed block can stop the server outright instead
of merely hiding a button. The id this pairs with is
`swarm.matrix.sso.clientId`, which is swarm-wide because it must
match authelia's register.
'';
};
};
};
config = lib.mkIf deployCfg.matrix.enable {
# 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
# description.
services.hyperhive.swarm.controller.links =
lib.optional (cfg.gatewayHost != null && cfg.gui.enable)
lib.optional (cfg.gatewayHost != null && deployCfg.matrix.gui.enable)
{
label = "Matrix";
icon = "💬";
@ -534,7 +550,7 @@ in
# contributes instead of replacing it.
#
# 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 {
default "/__matrix_spa_no_html_fallback";
"~*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
# 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";
};
@ -618,10 +634,10 @@ in
# Fail at EVAL, not at boot. tuwunel reads its identity providers
# from the config file, so a half-configured one does not hide a
# login button — it can stop the homeserver from starting at all.
assertion = cfg.sso.clientSecretFile != null;
assertion = deployCfg.matrix.sso.clientSecretFile != null;
message = ''
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
plaintext.
@ -683,7 +699,7 @@ in
# Same case, same reasoning: this host minted the secret, so it can say
# 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
);
@ -729,7 +745,7 @@ in
set -euo pipefail
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
# 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" ] ''
tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)}
tokenFile=${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)}
if [ ! -s "$tokenFile" ]; then
mkdir -p "$(dirname "$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
# reads it via systemd LoadCredential below (not directly).
bindMounts = {
${cfg.registrationTokenFile} = {
hostPath = cfg.registrationTokenFile;
${deployCfg.matrix.registrationTokenFile} = {
hostPath = deployCfg.matrix.registrationTokenFile;
isReadOnly = true;
};
}
@ -923,11 +939,11 @@ in
# `address` + `port` are upstream `listOf` — wrap singles.
address = [ "0.0.0.0" ];
port = [ cfg.httpPort ];
max_request_size = cfg.maxRequestSize;
max_request_size = deployCfg.matrix.maxRequestSize;
# Federation enabled at the protocol level; empty
# trustedServers keeps it effectively closed.
allow_federation = true;
trusted_servers = cfg.trustedServers;
trusted_servers = deployCfg.matrix.trustedServers;
# Token-gated registration. The absent
# `yes_i_am_very_very_sure_…_open_registration_…` flag
# keeps the server closed to anyone without the token.
@ -995,18 +1011,18 @@ in
# host-side chown :tuwunel / GID-pin gymnastics needed.
# See `man systemd.exec` → 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
# every OAuth exchange, not just at startup, so it has to
# 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 ];
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
networking.firewall = lib.mkIf deployCfg.matrix.openFirewall {
allowedTCPPorts = [
cfg.httpPort
];

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