Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dd5093c6f | ||
|
|
40e1ed2967 | ||
|
|
470d2ad845 | ||
|
|
600fa15afe | ||
|
|
0dc2e6b64f | ||
|
|
d337fec565 | ||
|
|
f2840612c0 |
12 changed files with 345 additions and 12 deletions
|
|
@ -15,6 +15,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
|
||||||
| `matrix.<hive>/` | `matrix.<hive>` | fluffychat-web static | `matrix.gui.enable` |
|
| `matrix.<hive>/` | `matrix.<hive>` | fluffychat-web static | `matrix.gui.enable` |
|
||||||
| `matrix.<hive>/config.json` | `matrix.<hive>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
| `matrix.<hive>/config.json` | `matrix.<hive>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
||||||
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
|
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
|
||||||
|
| `<swarm>/` | `<swarm>` | swarm-ui dist (static), behind an authelia subrequest | `swarm.ui.enable` |
|
||||||
|
|
||||||
The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
|
The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,30 +49,51 @@ start. Skipping this step looks like a broken proxy.
|
||||||
swarmctl user add mara --display-name Mara --email mara@example.com --group admins
|
swarmctl user add mara --display-name Mara --email mara@example.com --group admins
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠️ **Keep `--group admins`.** It is not decoration: operator-only
|
||||||
|
surfaces (the swarm UI below) are gated on that group, and an account
|
||||||
|
without it authenticates successfully and is then refused — which reads
|
||||||
|
like a broken login rather than a missing group.
|
||||||
|
|
||||||
Detail, including what the password is and why this stays manual:
|
Detail, including what the password is and why this stays manual:
|
||||||
[`swarm/sso.md`](swarm/sso.md).
|
[`swarm/sso.md`](swarm/sso.md).
|
||||||
|
|
||||||
### 4 · Matrix
|
### 4 · Swarm UI (only when `swarm.ui.enable`, on by default with the controller)
|
||||||
|
|
||||||
|
Nothing to run — it is served on the swarm apex
|
||||||
|
(`https://<swarm.domain>/`) as soon as the host rebuilds. Two things
|
||||||
|
decide whether you can actually open it:
|
||||||
|
|
||||||
|
- **You are in `admins`** (step 3). The gateway asks authelia whether
|
||||||
|
you have a session; the rule that makes it mean *operator* wants the
|
||||||
|
group. Without it you log in and still get bounced.
|
||||||
|
- **The name resolves to this host.** It is published to the hive's own
|
||||||
|
resolver and to `/etc/hosts` when `gateway.localHostsEntry` is on; from
|
||||||
|
anywhere else it needs a real DNS record like any other public name.
|
||||||
|
|
||||||
|
Detail, including why reachability is deliberately not the access
|
||||||
|
control: [`swarm/ui.md`](swarm/ui.md).
|
||||||
|
|
||||||
|
### 5 · Matrix
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 4a. Ensure the hive-internal admin account exists first
|
# 5a. Ensure the hive-internal admin account exists first
|
||||||
hivectl matrix sync-admin
|
hivectl matrix sync-admin
|
||||||
|
|
||||||
# 4b. Provision ruth's own matrix account
|
# 5b. Provision ruth's own matrix account
|
||||||
hivectl matrix create-user ruth
|
hivectl matrix create-user ruth
|
||||||
|
|
||||||
# 4c. Create a human matrix account
|
# 5c. Create a human matrix account
|
||||||
hivectl matrix create-user mara --password hunter2
|
hivectl matrix create-user mara --password hunter2
|
||||||
|
|
||||||
# 4d. Invite the operator to the hive Space (and optionally to rooms)
|
# 5d. Invite the operator to the hive Space (and optionally to rooms)
|
||||||
hivectl matrix invite mara
|
hivectl matrix invite mara
|
||||||
hivectl matrix invite @mara:yourserver --room '#hive-chat:yourserver'
|
hivectl matrix invite @mara:yourserver --room '#hive-chat:yourserver'
|
||||||
|
|
||||||
# 4e. Promote the operator to homeserver admin if needed
|
# 5e. Promote the operator to homeserver admin if needed
|
||||||
hivectl matrix promote-user mara
|
hivectl matrix promote-user mara
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5 · Spawn sub-agents
|
### 6 · Spawn sub-agents
|
||||||
|
|
||||||
Sub-agent creation goes through the approval queue — ruth proposes, the
|
Sub-agent creation goes through the approval queue — ruth proposes, the
|
||||||
operator approves, the container builds. From ruth's own turn (inside
|
operator approves, the container builds. From ruth's own turn (inside
|
||||||
|
|
@ -93,7 +114,7 @@ request_init_config(name: "iris")
|
||||||
|
|
||||||
See [`approvals.md`](approvals.md) for the full flow.
|
See [`approvals.md`](approvals.md) for the full flow.
|
||||||
|
|
||||||
### 6 · Useful host commands
|
### 7 · Useful host commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Roster: all agents, status, rev, parent, pending reminders
|
# Roster: all agents, status, rev, parent, pending reminders
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,12 @@ Which secrets the SSO provider generates, which one has a reader in
|
||||||
another container, and the three ways that one gets delivered:
|
another container, and the three ways that one gets delivered:
|
||||||
[`sso.md`](sso.md).
|
[`sso.md`](sso.md).
|
||||||
|
|
||||||
|
## Swarm UI
|
||||||
|
|
||||||
|
The operator-only web surface on the swarm apex, why reaching it needs
|
||||||
|
the `admins` group rather than just a session, and the four sites a
|
||||||
|
swarm service name has to be wired into: [`ui.md`](ui.md).
|
||||||
|
|
||||||
## The swarm's hive directory
|
## The swarm's hive directory
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
|
|
|
||||||
87
docs/swarm/ui.md
Normal file
87
docs/swarm/ui.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Swarm UI
|
||||||
|
|
||||||
|
The swarm's own web surface, served by the gateway on the **swarm apex**
|
||||||
|
(`services.hyperhive.swarm.domain`) and readable only by operators.
|
||||||
|
|
||||||
|
Distinct from the per-hive dashboard, which lives on the hive domain and
|
||||||
|
answers for one host. This one is the view *across* hives.
|
||||||
|
|
||||||
|
## Enabling
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.hyperhive.swarm.ui.enable = true; # defaults to swarm.controller.enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Derived from the controller rather than from `enableRequiredServices`:
|
||||||
|
the UI is a view onto the controller's state and reaches it over that
|
||||||
|
daemon's socket, so the host that runs the controller is the host that
|
||||||
|
can serve the UI. A hive that merely *uses* a swarm has nothing to serve.
|
||||||
|
|
||||||
|
`swarm.ui.domain` defaults to the swarm apex and can be pinned, the same
|
||||||
|
way `swarm.forge.domain` and `swarm.matrix.gatewayHost` can.
|
||||||
|
|
||||||
|
> The apex must differ from `services.hyperhive.domain`. The gateway's
|
||||||
|
> default server already answers for the hive domain, and two vhosts
|
||||||
|
> claiming one `server_name` do not error — nginx picks one — so this is
|
||||||
|
> an assertion rather than a runtime surprise.
|
||||||
|
|
||||||
|
## 🔑 You must be in the `admins` group
|
||||||
|
|
||||||
|
**This is the step that separates "protected" from "locked out".** The
|
||||||
|
vhost's `auth_request` asks authelia *"is there a session"*; the rule
|
||||||
|
that makes it mean *"is this an operator"* is an `access_control` entry
|
||||||
|
requiring `group:admins`. An account without that group authenticates
|
||||||
|
fine and still gets bounced.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
swarmctl user add <you> --group admins
|
||||||
|
```
|
||||||
|
|
||||||
|
`admins` deliberately, not a new word: [`../setup.md`](../setup.md) has
|
||||||
|
told every operator to create exactly that group since the bootstrap step
|
||||||
|
existed, so an account made by following the guide already passes. This
|
||||||
|
is the first rule that *consumes* a group name — inventing a second one
|
||||||
|
would have meant those accounts silently failing a check they were
|
||||||
|
supposed to pass.
|
||||||
|
|
||||||
|
An account created without any group needs re-adding with the flag —
|
||||||
|
`swarmctl` treats the existing entry as the canonical store, so the group
|
||||||
|
is what changes.
|
||||||
|
|
||||||
|
Why a group and not a list of usernames: agents are getting authelia
|
||||||
|
accounts of their own (matrix SSO), and *authenticated* would then
|
||||||
|
include every agent in the hive. The group is the only thing standing
|
||||||
|
between "an operator's page" and "anyone with a session".
|
||||||
|
|
||||||
|
## What it costs to be reachable
|
||||||
|
|
||||||
|
The apex is published to the hive's resolver like every other swarm
|
||||||
|
service, so agent containers can resolve it. That is deliberate and it is
|
||||||
|
not a hole: **reachability is not the access control here.** An agent
|
||||||
|
that resolves the name and connects still has no operator session, and
|
||||||
|
the subrequest denies it.
|
||||||
|
|
||||||
|
## Four wiring sites
|
||||||
|
|
||||||
|
Adding a swarm service name means touching all four. Missing one ships as
|
||||||
|
a different flavour of "works from the host, broken from a container":
|
||||||
|
|
||||||
|
| site | file |
|
||||||
|
| --- | --- |
|
||||||
|
| vhost | `nix/host-modules/hive-gateway/vhosts.nix` |
|
||||||
|
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
|
||||||
|
| DNS record | `nix/host-modules/hive-gateway/dnsmasq.nix` |
|
||||||
|
| local-dev hosts | `nix/host-modules/hive-gateway/default.nix` |
|
||||||
|
|
||||||
|
⚠️ The certificate one is the least obvious and the most visible when
|
||||||
|
missed. `serviceDomains` is *both* the services sub-CA's
|
||||||
|
`nameConstraints` set and the leaf's SAN list, and the apex is a
|
||||||
|
**sibling** of `forge.<swarm>` / `chat.<swarm>` / `auth.<swarm>`, not a
|
||||||
|
parent — no CA in the hierarchy issues for it implicitly. Left out, the
|
||||||
|
vhost falls back to the hive leaf and the swarm's front page opens with a
|
||||||
|
name mismatch.
|
||||||
|
|
||||||
|
## Cross-references
|
||||||
|
|
||||||
|
- [`sso.md`](sso.md) — the authelia instance itself, and the user store.
|
||||||
|
- [`../gateway.md`](../gateway.md) — the full vhost map and TLS modes.
|
||||||
|
|
@ -148,6 +148,9 @@
|
||||||
services.hyperhive.swarm.controller.swarmctlPackage =
|
services.hyperhive.swarm.controller.swarmctlPackage =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarmctl;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarmctl;
|
||||||
|
services.hyperhive.swarm.ui.package =
|
||||||
|
lib.mkDefault
|
||||||
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui;
|
||||||
services.hyperhive.gateway.swaggerUiTheme =
|
services.hyperhive.gateway.swaggerUiTheme =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swagger-ui-theme;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swagger-ui-theme;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
./swarm-ca.nix
|
./swarm-ca.nix
|
||||||
./swarm-controller.nix
|
./swarm-controller.nix
|
||||||
./swarm-snapshot-store.nix
|
./swarm-snapshot-store.nix
|
||||||
|
./swarm-ui.nix
|
||||||
./swarm-wireguard.nix
|
./swarm-wireguard.nix
|
||||||
./swarm.nix
|
./swarm.nix
|
||||||
./swarm-peers-removed.nix
|
./swarm-peers-removed.nix
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ let
|
||||||
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
||||||
matrixCfg = config.services.hyperhive.swarm.matrix;
|
matrixCfg = config.services.hyperhive.swarm.matrix;
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
|
uiCfg = config.services.hyperhive.swarm.ui;
|
||||||
forgeCfg = config.services.hyperhive.swarm.forge;
|
forgeCfg = config.services.hyperhive.swarm.forge;
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
|
|
||||||
|
|
@ -70,6 +71,7 @@ let
|
||||||
forgeCfg
|
forgeCfg
|
||||||
matrixCfg
|
matrixCfg
|
||||||
autheliaCfg
|
autheliaCfg
|
||||||
|
uiCfg
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
dashboardDist
|
dashboardDist
|
||||||
swaggerUiTheme
|
swaggerUiTheme
|
||||||
|
|
@ -308,6 +310,7 @@ in
|
||||||
forgeCfg
|
forgeCfg
|
||||||
matrixCfg
|
matrixCfg
|
||||||
autheliaCfg
|
autheliaCfg
|
||||||
|
uiCfg
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
@ -332,6 +335,7 @@ in
|
||||||
) config.services.hyperhive.swarm.forge.domain
|
) config.services.hyperhive.swarm.forge.domain
|
||||||
++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost
|
++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost
|
||||||
++ lib.optional autheliaCfg.enable autheliaCfg.domain
|
++ lib.optional autheliaCfg.enable autheliaCfg.domain
|
||||||
|
++ lib.optional uiCfg.enable uiCfg.domain
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
forgeCfg,
|
forgeCfg,
|
||||||
matrixCfg,
|
matrixCfg,
|
||||||
autheliaCfg,
|
autheliaCfg,
|
||||||
|
uiCfg,
|
||||||
hyperhiveDomain,
|
hyperhiveDomain,
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
|
|
@ -60,7 +61,16 @@
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
matrixCfg.enable && matrixCfg.gatewayHost != null
|
matrixCfg.enable && matrixCfg.gatewayHost != null
|
||||||
) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}"
|
) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}"
|
||||||
++ lib.optional autheliaCfg.enable "/${autheliaCfg.domain}/${networkCfg.bridgeIp}";
|
++ lib.optional autheliaCfg.enable "/${autheliaCfg.domain}/${networkCfg.bridgeIp}"
|
||||||
|
# The swarm UI's name is the swarm APEX by default — a sibling of
|
||||||
|
# the three above, not a child of anything this resolver already
|
||||||
|
# answers for, so the `/<hive domain>/` rule does not cover it.
|
||||||
|
#
|
||||||
|
# Published to agents deliberately (mara: publishing it is fine).
|
||||||
|
# Reachability is not the access control here: the vhost's
|
||||||
|
# `auth_request` + authelia's `group:operators` rule are, and an
|
||||||
|
# agent that resolves the name still cannot open the page.
|
||||||
|
++ lib.optional uiCfg.enable "/${uiCfg.domain}/${networkCfg.bridgeIp}";
|
||||||
# DHCP pool covering all usable host addresses on the bridge
|
# DHCP pool covering all usable host addresses on the bridge
|
||||||
# subnet — bounds computed by hive-network.nix from
|
# subnet — bounds computed by hive-network.nix from
|
||||||
# bridgeIp/bridgePrefixLength. All containers (agents and service
|
# bridgeIp/bridgePrefixLength. All containers (agents and service
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
forgeCfg,
|
forgeCfg,
|
||||||
matrixCfg,
|
matrixCfg,
|
||||||
autheliaCfg, # services.hyperhive.swarm.authelia
|
autheliaCfg, # services.hyperhive.swarm.authelia
|
||||||
|
uiCfg, # services.hyperhive.swarm.ui
|
||||||
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
|
||||||
|
|
@ -171,6 +172,68 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Swarm UI vhost — the swarm's front page, on the swarm apex, and the
|
||||||
|
# FIRST `auth_request` anywhere in this gateway (everything else is
|
||||||
|
# `auth_basic` + htpasswd).
|
||||||
|
#
|
||||||
|
# ⚠️ `auth_request` answers "is there a session", not "is this an
|
||||||
|
# operator". The operator-only part is authelia's `access_control`
|
||||||
|
# rule (../swarm-authelia.nix) requiring `group:operators` — agents
|
||||||
|
# are getting authelia accounts of their own, and without that rule a
|
||||||
|
# session alone would open this page.
|
||||||
|
#
|
||||||
|
# ⚠️ Failure mode here is LOCKED OUT, not unprotected: a subrequest
|
||||||
|
# that wrongly denies takes the whole UI away. That is the reason the
|
||||||
|
# redirect target and the header set below are copied from a measured
|
||||||
|
# source rather than from an example.
|
||||||
|
swarmUiVhost = lib.optionalAttrs uiCfg.enable {
|
||||||
|
"${uiCfg.domain}" = (vhostTlsFor uiCfg.domain) // {
|
||||||
|
listen = vhostListen;
|
||||||
|
extraConfig = securityHeaders;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
root = "${uiCfg.package}";
|
||||||
|
extraConfig = ''
|
||||||
|
auth_request /__hive_authelia;
|
||||||
|
# Captured BEFORE the error_page jump: inside the 401 handler
|
||||||
|
# `$request_uri` is the internal one, so building the return
|
||||||
|
# link there sends the operator back to the auth subrequest
|
||||||
|
# instead of the page they asked for.
|
||||||
|
auth_request_set $target_url $scheme://$http_host$request_uri;
|
||||||
|
error_page 401 =302 https://${autheliaCfg.domain}/?rd=$target_url;
|
||||||
|
# SPA: any path the bundle routes client-side is served the
|
||||||
|
# entry document rather than a 404 from the filesystem.
|
||||||
|
try_files $uri /index.html;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# The subrequest itself. `auth-request` is the implementation
|
||||||
|
# name authelia exposes under `/api/authz/`; `/api/verify` is the
|
||||||
|
# LEGACY path every older example shows.
|
||||||
|
#
|
||||||
|
# Header set measured against the pinned binary (4.39.20), not
|
||||||
|
# copied: `X-Original-URL` and `X-Original-Method` are present as
|
||||||
|
# literals and are what this implementation reads —
|
||||||
|
# `X-Forwarded-Uri` does not appear in it at all, so sending it
|
||||||
|
# would look like configuration and be dead weight.
|
||||||
|
"= /__hive_authelia" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString autheliaCfg.port}/api/authz/auth-request";
|
||||||
|
extraConfig = ''
|
||||||
|
internal;
|
||||||
|
# A subrequest carries no body, and forwarding one here makes
|
||||||
|
# authelia read a payload it will never use.
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_set_header X-Original-Method $request_method;
|
||||||
|
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`.
|
# Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`.
|
||||||
# `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll
|
# `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll
|
||||||
# timeout). `/` serves fluffychat or 404 if GUI off. nginx
|
# timeout). `/` serves fluffychat or 404 if GUI off. nginx
|
||||||
|
|
@ -458,5 +521,6 @@ in
|
||||||
}
|
}
|
||||||
// forgeVhost
|
// forgeVhost
|
||||||
// autheliaVhost
|
// autheliaVhost
|
||||||
// matrixVhost;
|
// matrixVhost
|
||||||
|
// swarmUiVhost;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,20 @@ let
|
||||||
hyperhiveCfg = config.services.hyperhive;
|
hyperhiveCfg = config.services.hyperhive;
|
||||||
hyperhiveDomain = hyperhiveCfg.domain;
|
hyperhiveDomain = hyperhiveCfg.domain;
|
||||||
swarmDomain = hyperhiveCfg.swarm.domain;
|
swarmDomain = hyperhiveCfg.swarm.domain;
|
||||||
|
uiCfg = hyperhiveCfg.swarm.ui;
|
||||||
|
|
||||||
|
# Group an account must hold to reach operator-only surfaces. Named
|
||||||
|
# here because this module writes the rule that enforces it and
|
||||||
|
# `swarmctl user add --group <this>` is what grants it — the two must
|
||||||
|
# agree, and one constant is how they stay agreeing.
|
||||||
|
#
|
||||||
|
# ⚠️ `admins` and not a new word, because `docs/setup.md` and
|
||||||
|
# `docs/swarm/sso.md` have been telling every operator to create
|
||||||
|
# exactly that group since the bootstrap step existed. This is the
|
||||||
|
# first rule that CONSUMES a group name; picking a different one would
|
||||||
|
# have meant every account created by following the guide silently
|
||||||
|
# failing the check it was supposed to pass.
|
||||||
|
operatorGroup = "admins";
|
||||||
|
|
||||||
# Upstream's `services.authelia.instances.<name>` derives the unit,
|
# Upstream's `services.authelia.instances.<name>` derives the unit,
|
||||||
# user, group and StateDirectory from the instance name
|
# user, group and StateDirectory from the instance name
|
||||||
|
|
@ -536,7 +550,26 @@ in
|
||||||
|
|
||||||
authentication_backend.file.path = cfg.usersFile;
|
authentication_backend.file.path = cfg.usersFile;
|
||||||
|
|
||||||
access_control.default_policy = "one_factor";
|
# ⚠️ `one_factor` as the DEFAULT means "any authenticated
|
||||||
|
# user", which is authentication, not authorisation. The
|
||||||
|
# swarm UI is operator-only, and agents are getting
|
||||||
|
# authelia accounts of their own — so the day that lands,
|
||||||
|
# a session alone would be enough to open it. The rule
|
||||||
|
# below is what makes the distinction real; without it
|
||||||
|
# the vhost's `auth_request` is a check nobody fails.
|
||||||
|
#
|
||||||
|
# The group is a constant rather than an option: it is the
|
||||||
|
# value an operator types into `swarmctl user add --group`,
|
||||||
|
# and a configurable name is one more way for the rule and
|
||||||
|
# the account to disagree silently.
|
||||||
|
access_control = {
|
||||||
|
default_policy = "one_factor";
|
||||||
|
rules = lib.optional uiCfg.enable {
|
||||||
|
domain = uiCfg.domain;
|
||||||
|
subject = [ "group:${operatorGroup}" ];
|
||||||
|
policy = "one_factor";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# The cookie domain is the SWARM's domain, NOT authelia's
|
# The cookie domain is the SWARM's domain, NOT authelia's
|
||||||
# own host: the session cookie has to be sent to the apps
|
# own host: the session cookie has to be sent to the apps
|
||||||
|
|
|
||||||
97
nix/host-modules/swarm-ui.nix
Normal file
97
nix/host-modules/swarm-ui.nix
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# The swarm-level web UI: a static bundle served by the gateway's nginx,
|
||||||
|
# behind authelia. Distinct from the per-hive dashboard (hive-c0re's, on
|
||||||
|
# the hive domain) — this one answers for the swarm apex and is the
|
||||||
|
# operator's view across hives.
|
||||||
|
#
|
||||||
|
# Options only. The vhost itself is declared in hive-gateway/vhosts.nix
|
||||||
|
# alongside forge/matrix/authelia: a service module says *how* it is
|
||||||
|
# reached, the gateway says *whether this host serves it*.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.services.hyperhive.swarm.ui;
|
||||||
|
swarmCfg = config.services.hyperhive.swarm;
|
||||||
|
hiveDomain = config.services.hyperhive.domain;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.hyperhive.swarm.ui = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = swarmCfg.controller.enable;
|
||||||
|
defaultText = lib.literalExpression "services.hyperhive.swarm.controller.enable";
|
||||||
|
example = true;
|
||||||
|
description = ''
|
||||||
|
Serve the swarm UI from this host.
|
||||||
|
|
||||||
|
Derived from `swarm.controller.enable` rather than from
|
||||||
|
`enableRequiredServices`: the UI is a view onto the controller's
|
||||||
|
state and reaches it over that daemon's unix socket, so the host
|
||||||
|
that runs the controller is the host that can serve the UI. A
|
||||||
|
hive that merely *uses* a swarm has nothing to serve here.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain;
|
||||||
|
defaultText = lib.literalExpression "services.hyperhive.swarm.domain";
|
||||||
|
example = "swarm.example.com";
|
||||||
|
description = ''
|
||||||
|
Host name the swarm UI answers on. Defaults to the swarm apex
|
||||||
|
itself — the swarm's front page is the swarm's name.
|
||||||
|
|
||||||
|
An option rather than a hardcoded derivation so a hive can pin a
|
||||||
|
different name, the same way `swarm.forge.domain` and
|
||||||
|
`swarm.matrix.gatewayHost` can.
|
||||||
|
|
||||||
|
Total on a null swarm domain (`.invalid`, RFC 2606) so the
|
||||||
|
required-domain assertion is what fires rather than a coercion
|
||||||
|
error naming this option — same reasoning as
|
||||||
|
`hive-network.nix`'s.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
|
||||||
|
description = ''
|
||||||
|
Static build of the swarm UI. nginx serves this store path
|
||||||
|
directly — there is no server-side component beyond the
|
||||||
|
controller's own API.
|
||||||
|
|
||||||
|
Wired by default from this flake's own package set (see
|
||||||
|
`flake.nix`), the same way `swarm.controller.package` is. There
|
||||||
|
is deliberately **no overlay** in this project, so a
|
||||||
|
`pkgs.swarm-ui` default here would name an attribute that does
|
||||||
|
not exist on any real deployment.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
# The `_` default server already answers for the hive domain
|
||||||
|
# (dashboard, per-agent routes). A second vhost claiming the same
|
||||||
|
# server_name is not an error to nginx — it picks one and logs a
|
||||||
|
# conflict — so the failure would surface as "the dashboard is
|
||||||
|
# sometimes the swarm UI", which is far harder to read than an
|
||||||
|
# eval failure naming both options.
|
||||||
|
assertion = cfg.domain != hiveDomain;
|
||||||
|
message = ''
|
||||||
|
services.hyperhive.swarm.ui.domain (${cfg.domain}) must differ
|
||||||
|
from services.hyperhive.domain (${hiveDomain}) — the hive
|
||||||
|
domain is already served by the gateway's default vhost
|
||||||
|
(dashboard + agent routes), and two vhosts claiming one
|
||||||
|
server_name silently resolve to whichever nginx picks.
|
||||||
|
|
||||||
|
Set services.hyperhive.swarm.domain to a name distinct from
|
||||||
|
this hive's, or pin swarm.ui.domain explicitly.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,13 @@ let
|
||||||
swarmCfg.forge.domain
|
swarmCfg.forge.domain
|
||||||
swarmCfg.matrix.gatewayHost
|
swarmCfg.matrix.gatewayHost
|
||||||
swarmCfg.authelia.domain
|
swarmCfg.authelia.domain
|
||||||
];
|
]
|
||||||
|
# The swarm UI's name is a SIBLING of the other three, not a parent of
|
||||||
|
# them — the apex is as much a name needing a certificate as
|
||||||
|
# `forge.<apex>` is, and no CA in the hierarchy issues for it
|
||||||
|
# implicitly. Left out, its vhost falls back to the hive leaf and the
|
||||||
|
# swarm's front page opens with a name mismatch.
|
||||||
|
++ lib.optional swarmCfg.ui.enable swarmCfg.ui.domain;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.hyperhive.swarm.hives = lib.mkOption {
|
options.services.hyperhive.swarm.hives = lib.mkOption {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue