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:
parent
eed53a2b59
commit
3bfba1925c
4 changed files with 159 additions and 144 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
153
nix/host-modules/swarm-bao-service.nix
Normal file
153
nix/host-modules/swarm-bao-service.nix
Normal 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`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue