swarm options: no swarm.* value depends on a deploy option or this host's hive
- swarm.serviceDomains lists every swarm service name on every host. The
store's issuing role is built from it, so the store now admits a leaf
for grafana, metrics, otel, logs, the bao UI and the apex whichever host
runs the store. Each gateway's own leaf stays narrowed by
localServiceDomains.
- swarm.matrix.apiUrl defaults to http://<swarm.matrix.gatewayHost>
(null when gatewayHost is null) instead of loopback on the matrix host.
- The authelia vhost proxies /bridge/ to swarm-authelia-bridge with the
prefix stripped, and deploy.swarm-controller.authBridgeUrl defaults to
${swarm.authelia.url}/bridge on every host. swarm.authelia.bridgeUrl is
removed.
- swarm-authelia-bridge admits only swarm-controller's OIDC client on
every request (403 for any other active token), configured by
SWARM_AUTHELIA_BRIDGE_CONTROLLER_CLIENT_ID (renamed from
..._OPERATOR_READER_CLIENT_ID, which gated ListOperators alone).
- swarm.peerHives is removed; swarm-wireguard.nix filters this host's own
hive out of swarm.hives itself.
Closes #4933
This commit is contained in:
parent
207b089498
commit
bed4358e19
21 changed files with 226 additions and 283 deletions
|
|
@ -127,9 +127,9 @@ in
|
|||
./options.nix
|
||||
./theme.nix
|
||||
# Hive-CA trust for this daemon's outbound TLS. Nothing it is given by
|
||||
# default is https — the forge and matrix URLs resolve to plain http or
|
||||
# loopback, and the queue's TLS is verified against
|
||||
# `HIVE_C0RE_OIDC_CA_FILE` (../hive-tls.nix), not this bundle — so this
|
||||
# default is https — the forge and matrix URLs are plain http, and the
|
||||
# queue's TLS is verified against `HIVE_C0RE_OIDC_CA_FILE`
|
||||
# (../hive-tls.nix), not this bundle — so this
|
||||
# changes nothing on an all-local hive. It matters for the split-host
|
||||
# shape the options invite: `swarm.matrix.apiUrl`'s own example is
|
||||
# `https://matrix.example.com`, and pointing it at another hive's
|
||||
|
|
|
|||
|
|
@ -180,15 +180,10 @@ in
|
|||
// lib.optionalAttrs (config.services.hyperhive.swarm.matrix.apiUrl != null) {
|
||||
# Client-server API base hive-c0re uses to provision matrix (register
|
||||
# agent users, create the hive space + chat room, invite members).
|
||||
# Supplied by `services.hyperhive.swarm.matrix.apiUrl`, which the matrix
|
||||
# module fills in with its own loopback listener when it is the thing
|
||||
# running tuwunel — and which the operator sets by hand when the
|
||||
# homeserver lives on another machine.
|
||||
#
|
||||
# NOT the agent-facing HIVE_MATRIX_URL above: that one is the gateway
|
||||
# vhost, and it is absent whenever there is no vhost. Reusing it here
|
||||
# would silently stop provisioning on a hive that runs matrix without
|
||||
# one.
|
||||
# Supplied by `services.hyperhive.swarm.matrix.apiUrl`, which defaults to
|
||||
# the same gateway vhost as HIVE_MATRIX_URL above but is its own option,
|
||||
# so an operator can point provisioning elsewhere without changing what
|
||||
# agents are handed.
|
||||
HIVE_MATRIX_API_URL = config.services.hyperhive.swarm.matrix.apiUrl;
|
||||
}
|
||||
// {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.swarm.matrix;
|
||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.swarm.matrix = {
|
||||
|
|
@ -65,11 +64,10 @@ in
|
|||
|
||||
apiUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if deployCfg.matrix.enable then "http://127.0.0.1:${toString cfg.httpPort}" else null;
|
||||
default = if cfg.gatewayHost == null then null else "http://${cfg.gatewayHost}";
|
||||
defaultText = lib.literalExpression ''
|
||||
if services.hyperhive.deploy.matrix.enable
|
||||
then "http://127.0.0.1:''${toString services.hyperhive.swarm.matrix.httpPort}"
|
||||
else null
|
||||
if services.hyperhive.swarm.matrix.gatewayHost == null then null
|
||||
else "http://''${services.hyperhive.swarm.matrix.gatewayHost}"
|
||||
'';
|
||||
example = "https://matrix.example.com";
|
||||
description = ''
|
||||
|
|
@ -79,12 +77,10 @@ in
|
|||
`hyperhive.matrix.url`, which is the gateway vhost handed to
|
||||
each agent's `hive-matrix-daemon`.
|
||||
|
||||
Defaults to the loopback listener **only when this module is the
|
||||
thing running tuwunel** — in that case the address is not a
|
||||
guess, it is where this module just put the container. Set it
|
||||
explicitly (with `enable = false`) when the homeserver runs on
|
||||
another machine; "everything on one host" is a special case of
|
||||
the full deployment, not the assumption.
|
||||
Defaults to the homeserver's gateway vhost (`gatewayHost`), the
|
||||
same name on every hive whichever host runs tuwunel, and `null`
|
||||
when there is no vhost. Set it explicitly when the homeserver is
|
||||
reachable only some other way.
|
||||
|
||||
`null` means hive-c0re has no homeserver to provision against
|
||||
and matrix provisioning no-ops. There is deliberately no
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.swarm.authelia;
|
||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
|
||||
# The unit name upstream's `services.authelia.instances.<name>` derives,
|
||||
# which `unit` publishes. ./swarm-authelia.nix binds the same two names for
|
||||
|
|
@ -432,32 +431,10 @@ in
|
|||
(`127.0.0.1:''${bridgePort}`) — one above authelia's own default
|
||||
`port` (9091), outside hyperhive's other claimed ranges.
|
||||
|
||||
Reachable directly from this host's other processes (this
|
||||
container shares the host netns, same as authelia's own `port`)
|
||||
without going through the gateway — this is an internal
|
||||
service-to-service endpoint, not something meant to be exposed
|
||||
publicly.
|
||||
'';
|
||||
};
|
||||
|
||||
bridgeUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
readOnly = true;
|
||||
default = if deployCfg.authelia.enable then "http://127.0.0.1:${toString cfg.bridgePort}" else null;
|
||||
defaultText = lib.literalExpression ''if enable then "http://127.0.0.1:''${bridgePort}" else null'';
|
||||
description = ''
|
||||
Where `swarm-authelia-bridge` answers, **as seen from this
|
||||
host** — correct only when a caller (`swarm-controller`) also
|
||||
runs on this host, the same co-location assumption
|
||||
`swarm.nix`'s `clientSecretFile` documents for its own
|
||||
cross-host case. `null` when this host doesn't run
|
||||
`swarm-authelia` at all.
|
||||
|
||||
A split-host swarm has no automated delivery for this address:
|
||||
the operator points `swarm-controller`'s own option at wherever
|
||||
this host has made the bridge reachable (a firewall rule, a
|
||||
different bind address), the same manual-copy shape used
|
||||
throughout this codebase's other cross-host cases.
|
||||
Not published on its own: the authelia vhost proxies
|
||||
`/bridge/` to it, and that is the address `swarm-controller`
|
||||
uses from any host. The bridge answers only
|
||||
`swarm-controller`'s OIDC client.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -759,6 +759,13 @@ in
|
|||
locations."/.well-known/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
# `swarm-authelia-bridge`, a separate process from authelia, also a
|
||||
# machine surface. The trailing slash on `proxyPass` strips the
|
||||
# prefix, so `/bridge/requests` reaches the bridge as `/requests`.
|
||||
# The bridge itself refuses every client but swarm-controller's.
|
||||
locations."/bridge/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.bridgePort}/";
|
||||
};
|
||||
};
|
||||
|
||||
# Order the container after the host CA generator, so the bind source
|
||||
|
|
@ -924,10 +931,10 @@ in
|
|||
# includes `bridgeClient`) — same file this container's own
|
||||
# `renderClient` reads the digest half of.
|
||||
SWARM_AUTHELIA_BRIDGE_CLIENT_SECRET_FILE = "${clientsDir}/${bridgeClientId}.secret";
|
||||
# The operator list names who holds operator rights, so the
|
||||
# bridge answers it to the controller's client and refuses every
|
||||
# other active token (each hive and its agents hold one).
|
||||
SWARM_AUTHELIA_BRIDGE_OPERATOR_READER_CLIENT_ID = hyperhiveCfg.swarm.controller.queueClientId;
|
||||
# The one client the bridge answers. Every other active token
|
||||
# (each hive and its agents hold one) is refused, on every
|
||||
# request, since `/bridge/` is reachable from any host.
|
||||
SWARM_AUTHELIA_BRIDGE_CONTROLLER_CLIENT_ID = hyperhiveCfg.swarm.controller.queueClientId;
|
||||
SWARM_AUTHELIA_BRIDGE_OPERATOR_GROUP = operatorGroup;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -587,17 +587,14 @@ in
|
|||
|
||||
authBridgeUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if deployCfg.authelia.enable then autheliaCfg.bridgeUrl else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
authelia's own `bridgeUrl` when this host also runs
|
||||
`swarm-authelia`, else null
|
||||
'';
|
||||
example = "http://127.0.0.1:9092";
|
||||
default = "${autheliaCfg.url}/bridge";
|
||||
defaultText = lib.literalExpression ''"''${services.hyperhive.swarm.authelia.url}/bridge"'';
|
||||
example = "https://auth.example.com/bridge";
|
||||
description = ''
|
||||
Where `swarm-authelia-bridge` (the only writer of authelia's
|
||||
users database) answers — see that option's own doc comment for
|
||||
the cross-host caveat, since this default is only correct when
|
||||
this host also runs `swarm-authelia`.
|
||||
users database) answers. The default is the `/bridge/` location
|
||||
on the authelia vhost, the same address from every host whether
|
||||
or not it runs `swarm-authelia`; the daemon appends `/requests`.
|
||||
|
||||
No new credential to configure: the bearer token presented to
|
||||
the bridge is minted from THIS daemon's own existing queue OIDC
|
||||
|
|
@ -879,8 +876,7 @@ in
|
|||
environment = {
|
||||
SWARM_CONTROLLER_SOCKET = deployCfg.swarm-controller.socketPath;
|
||||
# The swarm's hive directory, JSON-encoded — the full directory
|
||||
# (this daemon has no "self" hive to exclude, unlike
|
||||
# `swarm.peerHives`, `swarm.hives` minus this hive) rather than
|
||||
# (this daemon has no "self" hive to exclude) rather than
|
||||
# peers-minus-self. Consumed by `GET /api/hives`
|
||||
# (swarm-controller/src/main.rs::load_hives).
|
||||
# The one blacklist of names an agent may not take, shared verbatim
|
||||
|
|
|
|||
|
|
@ -114,12 +114,14 @@
|
|||
networking.wireguard.interfaces =
|
||||
let
|
||||
wgCfg = config.services.hyperhive.deploy.wireguard;
|
||||
# `peerHives` is `swarm.hives` minus this hive (../swarm.nix) —
|
||||
# a mesh that included our own entry would configure a tunnel to
|
||||
# ourselves.
|
||||
# `swarm.hives` minus this host's own hive: a mesh that included
|
||||
# our own entry would configure a tunnel to ourselves.
|
||||
meshPeers = lib.filterAttrs (
|
||||
_: p: p.wireguardPublicKey != null && p.wireguardAddress != null
|
||||
) config.services.hyperhive.swarm.peerHives;
|
||||
name: p:
|
||||
name != config.services.hyperhive.hiveName
|
||||
&& p.wireguardPublicKey != null
|
||||
&& p.wireguardAddress != null
|
||||
) config.services.hyperhive.swarm.hives;
|
||||
in
|
||||
{
|
||||
wg-hive = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# The swarm's directory: one entry per hive, **including this one**,
|
||||
# identical on every host in the swarm. `services.hyperhive.hiveName`
|
||||
# says which entry is us, and `peerHives` below derives the rest.
|
||||
# says which entry is us.
|
||||
#
|
||||
# Why a directory rather than a per-host peer list: every field here is
|
||||
# intrinsic to the hive it describes — none of them says anything about
|
||||
|
|
@ -78,41 +78,31 @@ let
|
|||
# assertion encoded an intended shape, not a property of the code, and
|
||||
# it rejected the supported migration path: a hive pinning its old
|
||||
# `forge.<hive domain>` while joining a swarm at a different apex.
|
||||
#
|
||||
# Every name is listed whether or not this host runs the service: the
|
||||
# store's issuing role is built from this list on the store's host and
|
||||
# has to admit a leaf for any gateway in the swarm. What one gateway's
|
||||
# leaf carries is `localServiceDomains`.
|
||||
#
|
||||
# Membership is also what `gateway.lib.tlsFor` consults to pick the
|
||||
# services leaf over the hive one, so a gateway-published swarm name
|
||||
# missing here is served the hive certificate, which cannot cover a name
|
||||
# under a different apex.
|
||||
serviceDomains' = [
|
||||
swarmCfg.forge.domain
|
||||
swarmCfg.matrix.gatewayHost
|
||||
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 (deployCfg.swarm-ui.enable && swarmCfg.domain != null) swarmCfg.domain
|
||||
# Every swarm service that claims a gateway name belongs here, and
|
||||
# these three were missing it. Membership is what `gateway.lib.tlsFor`
|
||||
# consults to pick the services leaf over the hive one, so a name
|
||||
# absent from this list is served the HIVE certificate — which cannot
|
||||
# cover a name under a different apex however the sub-CA is set up.
|
||||
#
|
||||
# ⚠️ How that stayed invisible: a mismatch a browser shows as a
|
||||
# click-through warning is one a machine client rejects outright. The
|
||||
# metrics UI and store looked fine for as long as only people opened
|
||||
# them; the collector's exporter — same defect, no human in the loop —
|
||||
# failed every POST and dropped the samples.
|
||||
++ lib.optional deployCfg.grafana.enable swarmCfg.grafana.domain
|
||||
++ lib.optional deployCfg.victoriametrics.enable swarmCfg.victoriametrics.domain
|
||||
++ lib.optional deployCfg.swarm-otel.enable swarmCfg.otel.domain
|
||||
# VictoriaLogs' vhost is new (was previously unpublished entirely — see
|
||||
# swarm-victorialogs.nix's file-top comment) and needs the same
|
||||
# membership every gateway-published swarm service needs: absent from
|
||||
# this list, `gateway.lib.tlsFor` falls back to the hive leaf, which
|
||||
# cannot cover a name under a different apex — see the ⚠️ above this
|
||||
# list for what that looked like the last time a name was missed here.
|
||||
++ lib.optional deployCfg.victorialogs.enable swarmCfg.victorialogs.domain
|
||||
# The store's browser UI, not the store: `swarm.bao.domain` is mTLS and
|
||||
# never gets a gateway certificate.
|
||||
++ lib.optional deployCfg.bao.enable swarmCfg.bao.ui.domain;
|
||||
# The swarm UI's apex is a sibling of the other names, not a parent:
|
||||
# no CA in the hierarchy issues for it implicitly.
|
||||
swarmCfg.domain
|
||||
swarmCfg.grafana.domain
|
||||
swarmCfg.victoriametrics.domain
|
||||
swarmCfg.otel.domain
|
||||
swarmCfg.victorialogs.domain
|
||||
# The store's browser UI, not the store: `swarm.bao.domain` is mTLS and
|
||||
# never gets a gateway certificate.
|
||||
swarmCfg.bao.ui.domain
|
||||
];
|
||||
|
||||
# Hives whose entry still carries the removed `certFingerprint`. Scanned
|
||||
# here, at top level, because that is the only place an assertion about a
|
||||
|
|
@ -274,28 +264,17 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.swarm.peerHives = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified);
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = ''
|
||||
Read-only: `hives` minus this host's own entry. Derived once here
|
||||
rather than in each consumer, because "everything that isn't me"
|
||||
is a filter four different modules were re-implementing and only
|
||||
one of them has to be wrong for a hive to peer with itself.
|
||||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.swarm.serviceDomains = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = ''
|
||||
Read-only: the public hostnames of the swarm's own services, in a
|
||||
stable sorted order. Second derived set alongside `peerHives`, and
|
||||
here for the same reason — the CA that name-constrains these and
|
||||
the leaf that carries them as SANs must agree exactly, and two
|
||||
modules each assembling the list is how they stop agreeing.
|
||||
Read-only: the public hostnames of every swarm service, in a
|
||||
stable sorted order, identical on every host whichever services it
|
||||
runs. Derived once here because the CA that name-constrains these,
|
||||
the store role that issues for them and the leaf that carries them
|
||||
as SANs must agree exactly, and two modules each assembling the
|
||||
list is how they stop agreeing.
|
||||
|
||||
Sorted and deduplicated deliberately: consumers compare this list
|
||||
against what they issued last time to decide whether to re-issue,
|
||||
|
|
@ -326,8 +305,6 @@ in
|
|||
};
|
||||
|
||||
config = {
|
||||
services.hyperhive.swarm.peerHives = lib.filterAttrs (name: _: name != cfg.hiveName) swarmCfg.hives;
|
||||
|
||||
services.hyperhive.swarm.serviceDomains = lib.sort (a: b: a < b) (
|
||||
lib.unique (lib.filter (d: d != null && d != "") serviceDomains')
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue