Watch
0
0
Fork
You've already forked hyperhive
0

nix: split swarm-bao into service and deploy-mode files

`swarm.bao` (what the secret store is to every hive: container name,
domain, UI domain and OIDC client, port, collector client id and
telemetry port) moves to nix/host-modules/swarm-bao-service.nix, together
with `domainBase`, the only helper it reads besides `cfg`. Everything
else -- the `deploy.bao` options, the removed-option import, the whole
`config` block including `containers.swarm-bao`, and the helpers only
they read -- stays in nix/host-modules/swarm-bao.nix, which default.nix
now imports alongside the new file.

Both halves read `cfg` (`swarm.bao.ui.oidc.redirectUri` defaults from
`cfg.ui.domain`; the config block reads `cfg` throughout). It is an
option read, so each file binds it from `config.services.hyperhive.swarm.bao`.
The service file has no `hyperhiveCfg`, so its `swarmDomain` reads
`config.services.hyperhive.swarm.domain` directly, as
swarm-nats-service.nix does.

A pure move: option paths, option definitions and config are unchanged
apart from the comment above `deploy.bao`, which now names the file
`swarm.bao` lives in, and the pointer in swarm-nats-service.nix to the
`domainBase` rationale, which moved with it.

Refs #3742
This commit is contained in:
atlas 2026-10-01 09:37:36 +02:00
commit 3bfba1925c
4 changed files with 159 additions and 144 deletions

View file

@ -41,6 +41,7 @@
./glue-swarm-bao-otel-oidc-client.nix ./glue-swarm-bao-otel-oidc-client.nix
./glue-swarm-otel-oidc-client.nix ./glue-swarm-otel-oidc-client.nix
./swarm-authelia.nix ./swarm-authelia.nix
./swarm-bao-service.nix
./swarm-bao.nix ./swarm-bao.nix
./swarm-ca.nix ./swarm-ca.nix
./swarm-secret-publisher.nix ./swarm-secret-publisher.nix

View file

@ -0,0 +1,153 @@
# The swarm's secret store as every hive sees it: the names it is reached on,
# its port, its container, and the client ids it is registered under,
# identical on every host. What the host running it decides, and the
# container itself, are in ./swarm-bao.nix.
{
lib,
config,
...
}:
let
cfg = config.services.hyperhive.swarm.bao;
swarmDomain = config.services.hyperhive.swarm.domain;
# Total on a null swarm domain for the same reason every sibling module is:
# the required-domain assertion in hive-network.nix should be what an operator
# sees, not a coercion error from here.
domainBase = if swarmDomain == null then "invalid" else swarmDomain;
in
{
options.services.hyperhive.swarm.bao = {
machine = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao";
description = ''
Container name. Read-only: the name appears in host paths and in
`machinectl`, so it is a fact other modules may read rather than a knob.
'';
};
domain = lib.mkOption {
type = lib.types.str;
default = "bao.${domainBase}";
defaultText = lib.literalExpression ''"bao.''${services.hyperhive.swarm.domain}"'';
description = ''
Name the store is reached on. A **sibling** of the swarm's other
service names, not a child of any hive domain: an authority whose
`nameConstraints` permit one hive's domain cannot issue for a sibling
of it, so the shape of this name decides which authorities could ever
sign for the store. That is a property of the name, not a choice of
issuer — this module makes no such choice.
'';
};
ui.domain = lib.mkOption {
type = lib.types.str;
default = "bao-ui.${domainBase}";
defaultText = lib.literalExpression ''"bao-ui.''${services.hyperhive.swarm.domain}"'';
description = ''
Name the gateway serves the store's browser UI on, to members of
authelia's `admins` group only. Swarm-wide because authelia's host
writes the access rule for it and the store's host serves it.
Must differ from {option}`services.hyperhive.swarm.bao.domain`: that
name is the mutual-TLS endpoint every reader dials, and it has no
vhost.
'';
};
ui.oidc.clientId = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao-ui";
description = ''
OAuth2 client id the store's `oidc` auth method logs browser users
in as, at authelia.
Swarm-wide and read-only because two hosts have to agree on it:
authelia registers the client (`glue-bao-ui-oidc-client.nix`) and
mints its secret, and the store's host reads that secret back out of
the store under a path composed from this id.
'';
};
ui.oidc.redirectUri = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "https://${cfg.ui.domain}/ui/vault/auth/oidc/oidc/callback";
defaultText = lib.literalExpression ''"https://''${services.hyperhive.swarm.bao.ui.domain}/ui/vault/auth/oidc/oidc/callback"'';
description = ''
Where authelia sends the browser back to after an OIDC login, and the
URI both authelia and the store's `oidc` role match **exactly**.
The format is the OpenBao UI's own route,
`/ui/vault/auth/<mount>/oidc/callback`, with the mount `oidc`. The
UI composes it from the page's origin, so it only matches when the
gateway serves the UI on port 443.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8200;
description = ''
TCP port the store listens on. Upstream's own default, kept so an
operator reading OpenBao documentation finds what they expect.
Swarm-wide because a client has to know it to reach the store, and
the same port on every listener: which *addresses* the store answers
on is the running host's business
({option}`services.hyperhive.deploy.bao.extraListenAddresses`), but
which port it answers on is something the whole swarm agrees.
'';
};
otel.clientId = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao-collector";
description = ''
OAuth2 client id the collector inside the store's container
authenticates as, and — self-referentially, the shape every hive's
client already uses — the audience it asks its token for.
**Its own, not the swarm collector's and not
`swarm-controller`'s.** One identity per principal: this forwarder
runs wherever the store runs, which is not where either of those
two runs, and the receiver it pushes to
(`swarm.otel.storeProducerName`) admits this id alone.
Swarm-wide and read-only because three hosts have to agree on it:
authelia registers the client
(`glue-swarm-bao-otel-oidc-client.nix`), the swarm collector checks
the audience (`swarm-otel.nix`), and the store's host reads the
minted secret back out of the store under a path composed from it.
Two spellings present as a healthy-looking 401.
'';
};
otel.telemetryPort = lib.mkOption {
type = lib.types.port;
default = 8890;
description = ''
Port the collector inside the store's container serves its **own**
metrics on — queue depth, refused and dropped samples, exporter
failures. How you find out that telemetry is being lost, so it is
worth keeping rather than switching off.
⚠️ **Deliberately neither 8888 nor 8889.** 8888 is the collector
binary's built-in default, which the hive tier
({option}`services.hyperhive.otel.telemetryPort`) already binds, and
8889 is the swarm tier's
({option}`services.hyperhive.swarm.otel.telemetryPort`). This
container runs with `privateNetwork = false`, so all three share the
host's network namespace whenever they are co-located — and unlike
the OTLP ports this one appears nowhere in either config when it is
left undeclared, so nothing that compares configured ports can see
the clash. The second collector to start simply dies with
`bind: address already in use`.
'';
};
};
}

View file

@ -40,7 +40,6 @@ let
deployCfg = hyperhiveCfg.deploy; deployCfg = hyperhiveCfg.deploy;
baoDeploy = deployCfg.bao; baoDeploy = deployCfg.bao;
networkCfg = hyperhiveCfg.network; networkCfg = hyperhiveCfg.network;
swarmDomain = hyperhiveCfg.swarm.domain;
# What this container's own collector stamps as `service.name` on every # What this container's own collector stamps as `service.name` on every
# log line and metric it forwards, and the value the shipped Grafana # log line and metric it forwards, and the value the shipped Grafana
@ -125,11 +124,6 @@ let
}; };
}; };
# Total on a null swarm domain for the same reason every sibling module is:
# the required-domain assertion in hive-network.nix should be what an operator
# sees, not a coercion error from here.
domainBase = if swarmDomain == null then "invalid" else swarmDomain;
# Where the leaf lands for openbao to read. `tlsDir` is bind-mounted at the # Where the leaf lands for openbao to read. `tlsDir` is bind-mounted at the
# same path on both sides, so the delivery below needs no second mount, and # same path on both sides, so the delivery below needs no second mount, and
# nothing has to bind `deploy.hive-controller.tls.stateDir`, which holds the # nothing has to bind `deploy.hive-controller.tls.stateDir`, which holds the
@ -1214,9 +1208,10 @@ in
# Reading this block as store-runner-only is what makes an off-host reader # Reading this block as store-runner-only is what makes an off-host reader
# look inexpressible when it is already supported. # look inexpressible when it is already supported.
# #
# `swarm.bao.*` below is what every host in the swarm has to agree on — the # `swarm.bao.*`, in ./swarm-bao-service.nix, is what every host in the swarm
# name the store answers to, its port, its container. A host that is purely # has to agree on — the name the store answers to, its port, its container.
# a *client* needs all of that, because it is how the client finds the store. # A host that is purely a *client* needs all of that, because it is how the
# client finds the store.
options.services.hyperhive.deploy.bao = { options.services.hyperhive.deploy.bao = {
package = lib.mkOption { package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
@ -1948,140 +1943,6 @@ in
}; };
}; };
options.services.hyperhive.swarm.bao = {
machine = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao";
description = ''
Container name. Read-only: the name appears in host paths and in
`machinectl`, so it is a fact other modules may read rather than a knob.
'';
};
domain = lib.mkOption {
type = lib.types.str;
default = "bao.${domainBase}";
defaultText = lib.literalExpression ''"bao.''${services.hyperhive.swarm.domain}"'';
description = ''
Name the store is reached on. A **sibling** of the swarm's other
service names, not a child of any hive domain: an authority whose
`nameConstraints` permit one hive's domain cannot issue for a sibling
of it, so the shape of this name decides which authorities could ever
sign for the store. That is a property of the name, not a choice of
issuer — this module makes no such choice.
'';
};
ui.domain = lib.mkOption {
type = lib.types.str;
default = "bao-ui.${domainBase}";
defaultText = lib.literalExpression ''"bao-ui.''${services.hyperhive.swarm.domain}"'';
description = ''
Name the gateway serves the store's browser UI on, to members of
authelia's `admins` group only. Swarm-wide because authelia's host
writes the access rule for it and the store's host serves it.
Must differ from {option}`services.hyperhive.swarm.bao.domain`: that
name is the mutual-TLS endpoint every reader dials, and it has no
vhost.
'';
};
ui.oidc.clientId = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao-ui";
description = ''
OAuth2 client id the store's `oidc` auth method logs browser users
in as, at authelia.
Swarm-wide and read-only because two hosts have to agree on it:
authelia registers the client (`glue-bao-ui-oidc-client.nix`) and
mints its secret, and the store's host reads that secret back out of
the store under a path composed from this id.
'';
};
ui.oidc.redirectUri = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "https://${cfg.ui.domain}/ui/vault/auth/oidc/oidc/callback";
defaultText = lib.literalExpression ''"https://''${services.hyperhive.swarm.bao.ui.domain}/ui/vault/auth/oidc/oidc/callback"'';
description = ''
Where authelia sends the browser back to after an OIDC login, and the
URI both authelia and the store's `oidc` role match **exactly**.
The format is the OpenBao UI's own route,
`/ui/vault/auth/<mount>/oidc/callback`, with the mount `oidc`. The
UI composes it from the page's origin, so it only matches when the
gateway serves the UI on port 443.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8200;
description = ''
TCP port the store listens on. Upstream's own default, kept so an
operator reading OpenBao documentation finds what they expect.
Swarm-wide because a client has to know it to reach the store, and
the same port on every listener: which *addresses* the store answers
on is the running host's business
({option}`services.hyperhive.deploy.bao.extraListenAddresses`), but
which port it answers on is something the whole swarm agrees.
'';
};
otel.clientId = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "swarm-bao-collector";
description = ''
OAuth2 client id the collector inside the store's container
authenticates as, and — self-referentially, the shape every hive's
client already uses — the audience it asks its token for.
**Its own, not the swarm collector's and not
`swarm-controller`'s.** One identity per principal: this forwarder
runs wherever the store runs, which is not where either of those
two runs, and the receiver it pushes to
(`swarm.otel.storeProducerName`) admits this id alone.
Swarm-wide and read-only because three hosts have to agree on it:
authelia registers the client
(`glue-swarm-bao-otel-oidc-client.nix`), the swarm collector checks
the audience (`swarm-otel.nix`), and the store's host reads the
minted secret back out of the store under a path composed from it.
Two spellings present as a healthy-looking 401.
'';
};
otel.telemetryPort = lib.mkOption {
type = lib.types.port;
default = 8890;
description = ''
Port the collector inside the store's container serves its **own**
metrics on — queue depth, refused and dropped samples, exporter
failures. How you find out that telemetry is being lost, so it is
worth keeping rather than switching off.
⚠️ **Deliberately neither 8888 nor 8889.** 8888 is the collector
binary's built-in default, which the hive tier
({option}`services.hyperhive.otel.telemetryPort`) already binds, and
8889 is the swarm tier's
({option}`services.hyperhive.swarm.otel.telemetryPort`). This
container runs with `privateNetwork = false`, so all three share the
host's network namespace whenever they are co-located — and unlike
the OTLP ports this one appears nowhere in either config when it is
left undeclared, so nothing that compares configured ports can see
the clash. The second collector to start simply dies with
`bind: address already in use`.
'';
};
};
# ⚠️ Gated on `deploy.bao.enable`, and that is load-bearing rather than # ⚠️ Gated on `deploy.bao.enable`, and that is load-bearing rather than
# tidiness: an unconditional `config` block would evaluate the seal # tidiness: an unconditional `config` block would evaluate the seal
# assertion on EVERY hive, so a hive that runs no secret store at all # assertion on EVERY hive, so a hive that runs no secret store at all

View file

@ -8,7 +8,7 @@
}: }:
let let
swarmDomain = config.services.hyperhive.swarm.domain; swarmDomain = config.services.hyperhive.swarm.domain;
# Total on a null swarm domain, for the reason ./swarm-bao.nix gives. # Total on a null swarm domain, for the reason ./swarm-bao-service.nix gives.
domainBase = if swarmDomain == null then "invalid" else swarmDomain; domainBase = if swarmDomain == null then "invalid" else swarmDomain;
in in
{ {