Grafana's own metrics were scraped over loopback through the `grafana-metrics` nginx vhost, declared only where grafana and the collector share a host, so a collector on any other host had no job for it. Its target is now declared on every host as `https://<swarm.grafana.domain>/metrics`, next to the other published targets, and grafana's vhost serves `= /metrics` behind the collector's auth subrequest (`/__grafana_metrics_authz`, the forge's shape). The rules derived from `publishedScrapeTargets` admit the collector's client there and deny every other subject. The loopback scrape is gone with the vhost that served it and its `swarm.grafana.metricsPort` option, so a single host still scrapes grafana once, now by name. A config that sets that option now fails evaluation with "option does not exist". On the published vhost `/metrics` was answered with 403 for everyone. It now answers the collector's token, and a plain session gets authelia's `deny`. The design note that ruled the published vhost out for scraping predates the collector's credential; it is replaced by the location's own comment and a line on `swarm.grafana.domain`. Part of #5205
118 lines
4.6 KiB
Nix
118 lines
4.6 KiB
Nix
# The swarm's metrics UI as every hive sees it: the name it is served under
|
||
# and the OIDC client it is registered as, identical on every host. What the
|
||
# host running it decides, and the container itself, are in
|
||
# ./swarm-grafana.nix.
|
||
{
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.swarm.grafana;
|
||
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
|
||
{
|
||
# `enable` is `services.hyperhive.deploy.grafana.enable` — see
|
||
# ./deploy.nix. Whether this host runs the swarm's Grafana is a
|
||
# deployment decision, and `swarm.*` has to be identical on every host.
|
||
# Here is what the service IS to every hive: the name it is served under
|
||
# and the OIDC client it is registered as. What the host running it
|
||
# decides — which build it runs, where its datasources point, which plugins
|
||
# are in its store path, and the socket directory it shares with nginx — is
|
||
# `deploy.grafana`, in ./swarm-grafana.nix.
|
||
options.services.hyperhive.swarm.grafana = {
|
||
machine = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = "swarm-grafana";
|
||
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 = "grafana.${domainBase}";
|
||
defaultText = lib.literalExpression ''"grafana.''${services.hyperhive.swarm.domain}"'';
|
||
description = ''
|
||
Name the gateway serves this on. A sibling of the swarm's other
|
||
service names, so the swarm-services sub-CA can issue for it — see
|
||
`hive-tls.nix` for why a service name being a sibling rather than a
|
||
child decides which CA may sign it.
|
||
|
||
⚠️ Changing this changes the OAuth redirect URI, which authelia
|
||
matches exactly. Both sides move together because both derive from
|
||
this option; an operator who pins one by hand breaks the login.
|
||
|
||
The swarm collector scrapes Grafana's own metrics at
|
||
`https://<this name>/metrics`, from whichever host it runs on.
|
||
Grafana serves that endpoint to anyone, so the gateway puts it
|
||
behind authelia, whose rules admit only the collector's OAuth2
|
||
client there: see `publishedScrapeTargets` in
|
||
./swarm-otel-service.nix.
|
||
'';
|
||
};
|
||
|
||
oidc = {
|
||
clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "swarm-grafana";
|
||
description = ''
|
||
The authelia OIDC client id. Names the application rather than
|
||
the protocol, per the convention in
|
||
{option}`services.hyperhive.swarm.authelia.oidc.clients`.
|
||
'';
|
||
};
|
||
|
||
redirectUri = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = "https://${cfg.domain}/login/generic_oauth";
|
||
defaultText = lib.literalExpression ''"https://''${services.hyperhive.swarm.grafana.domain}/login/generic_oauth"'';
|
||
description = ''
|
||
OAuth callback authelia sends the browser back to, and the URI it
|
||
matches **exactly**.
|
||
|
||
Read-only, like {option}`services.hyperhive.swarm.grafana.machine`
|
||
and for the same reason: Grafana derives it from its own
|
||
`root_url` (`<root_url>/login/generic_oauth`), so it is a fact
|
||
other modules may read rather than a knob. The glue that registers
|
||
this client wherever authelia runs reads it from here instead of
|
||
restating the format — a second spelling of it is a silently
|
||
rejected login.
|
||
'';
|
||
};
|
||
|
||
role = lib.mkOption {
|
||
type = lib.types.enum [
|
||
"Viewer"
|
||
"Editor"
|
||
"Admin"
|
||
];
|
||
default = "Admin";
|
||
example = "Editor";
|
||
description = ''
|
||
Grafana org role every SSO user is assigned.
|
||
|
||
`Admin` by default, and that is a considered default rather than
|
||
a permissive one: the login form is disabled whenever SSO is
|
||
configured, so this is the *only* way anyone reaches Grafana —
|
||
a `Viewer` default would produce a swarm nobody can administer.
|
||
Passing authelia already means being an operator of this swarm;
|
||
its user store is the small, `swarmctl`-managed one.
|
||
|
||
Lower it if a swarm ever grows read-only operators, which is a
|
||
one-line change here.
|
||
'';
|
||
};
|
||
};
|
||
|
||
};
|
||
}
|