# 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//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`. ''; }; }; }