Grafana's OIDC client secret only existed where authelia did. One `ssoLocal` gate — `grafana.enable && authelia.enable` — decided the client registration, the minted secret's delivery and the whole `auth.generic_oauth` block, so a swarm whose authelia runs on another host got Grafana with no SSO wiring at all. The local login form is disabled unconditionally, so that is no way in. Split the one gate into the two questions it was conflating: - `ssoConfigured` — does this SWARM have an identity provider (`swarm.authelia.url`, which is swarm-wide and whose own description makes null mean "no SSO configured"). With a delivery route present this is what emits Grafana's OIDC block. - `ssoLocal` — is authelia on THIS host, now spelled as the forge and matrix modules spell it. It decides only which unit delivers the secret. Where authelia is elsewhere, `swarm-bao-grafana-oidc.service` reads the secret from the swarm secret store, shaped after glue-queue-agent-credential.nix: cert login fails loudly because a retry fixes every state it fails on, the read degrades quietly because no retry turns "no value there" into a value, and nothing writes a stand-in. The producer is the publisher that already runs on authelia's host, which gains the swarm's service clients beside the per-hive ones at `swarm/services/<id>/oidc/client` — with the write grant in swarm-bao.nix and the hive read grant in `policy::render` to match. Registration moved to glue-grafana-oidc-client.nix. It has to be declared where authelia's config is rendered, and swarm-grafana.nix's config block hangs off this host running Grafana. Two judgement calls stated rather than buried: a hive's read policy now grants the whole `services` prefix, because a service's path names the service and nothing swarm-wide records which hive runs it (cost recorded in docs/trust-boundary/security.md); and the client is registered on any authelia host, because no swarm-wide "this swarm has a Grafana" fact exists to gate it on. Refs #4234 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
2.4 KiB
Nix
51 lines
2.4 KiB
Nix
# Glue: register the swarm's Grafana as an OIDC client wherever authelia runs.
|
|
#
|
|
# ONE PAIRING PER FILE — grafana ← authelia, and nothing else. Deleting this
|
|
# leaves a swarm whose metrics UI is not a client authelia has ever heard of, so
|
|
# no login against it can complete and nothing minted its secret either.
|
|
#
|
|
# ⚠️ Gated on authelia being HERE, and deliberately NOT on this host running
|
|
# Grafana. A client is a row in THIS host's provider config, so it can only be
|
|
# declared where that config is rendered — and ./swarm-grafana.nix's whole
|
|
# `config` block hangs off `deploy.grafana.enable`, so a swarm with Grafana and
|
|
# authelia on different hosts registered the client nowhere at all.
|
|
# ./hive-forge/default.nix is already on the right side of that line: its module
|
|
# is gated on `hyperhive.enable` and only the registration asks about authelia.
|
|
# This file puts Grafana there without moving the rest of its module.
|
|
#
|
|
# ⚠️ Registered whether or not the swarm has a Grafana, because nothing in
|
|
# `swarm.*` records that — `deploy.grafana.enable` answers "does THIS host run
|
|
# it", and a swarm-wide answer does not exist. The cost is one unused client
|
|
# and one unused minted secret in a swarm with no metrics UI. The alternative
|
|
# was a new swarm-wide option an operator must set before a split deployment
|
|
# works, which leaves the reported failure in place for everyone who does not
|
|
# know to set it. ./swarm-otel.nix made the same call for its own client when it
|
|
# dropped the published-scrape-target guard: a client that may go unused beats a
|
|
# guard that reads as done and renders nothing.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
hyperhiveCfg = config.services.hyperhive;
|
|
deployCfg = hyperhiveCfg.deploy;
|
|
grafanaCfg = hyperhiveCfg.swarm.grafana;
|
|
in
|
|
{
|
|
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
|
|
# One declaration, two readers. Grafana's callback URL is format-locked to
|
|
# its own root URL, and the read-only option it is taken from is where that
|
|
# format is spelled — restating it here would be a second source of truth
|
|
# for a string whose mismatch is a silently rejected login.
|
|
#
|
|
# `kind` is left at its `interactive` default: a person logs in here.
|
|
services.hyperhive.swarm.authelia.oidc.clients = [
|
|
{
|
|
id = grafanaCfg.oidc.clientId;
|
|
description = "HyperHive swarm metrics";
|
|
redirectUris = [ grafanaCfg.oidc.redirectUri ];
|
|
}
|
|
];
|
|
};
|
|
}
|