Watch
0
0
Fork
You've already forked hyperhive
0

swarm metrics store: the read API needs Grafana's bearer or an operator session

The metrics vhost forwarded the store's read API (query, series, labels,
export, vmui, ...) with no credential, so anything that could reach
metrics.<swarm> could read every hive's and agent's metrics. mara ruled on
checked by an auth_request responder, and operators use their authelia
session.

- swarm-metrics-auth (new crate): auth_request responder on the metrics
  store's host. Compares the presented bearer, in constant time, with the
  one stored at swarm/services/<grafana client id>/metrics/bearer, read
  under its own bao leaf and cached for 60s. Static user, the post-#5264
  swarm-logs-auth unit shape.
- swarm-bao-grafana-metrics-bearer: on Grafana's host, creates that bearer
  in bao if absent (create without update, Grafana's existing leaf) and
  writes "Bearer <value>" into the container; the datasource sends it as
  its Authorization header via $__file{}.
- The vhost: read locations take auth_request through a map (a bearer goes
  to swarm-metrics-auth, anything else to authelia, no login redirect);
  vmui takes an authelia session with the login redirect; forceSSL like
  the logs vhost. The authelia subrequest location is renamed from
  /__metrics_push_authz to /__metrics_authelia since it now serves reads.
- Authelia: the collector keeps every path; everyone else is refused the
  collector's push and /metrics paths; group admins is admitted to the
  rest; then deny.
- bao: a swarm-metrics-auth cert role and policy (read on the one leaf),
  its leaf minted by glue-bao-tls, the CN reserved as a hive name, and
  Grafana's grant gains create on the bearer leaf.

Fixes #3596
This commit is contained in:
atlas 2026-10-14 19:53:36 +02:00 • committed by mara
commit 6aab2ebb65
24 changed files with 1126 additions and 200 deletions

View file

@ -169,7 +169,7 @@ let
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
) baoGrantWithConsumers.systemd.services;
# The eighteen units that write a `swarm-*` grant, by name, for the discovery
# The nineteen units that write a `swarm-*` grant, by name, for the discovery
# control below.
grantingUnitNames = [
"swarm-bao-controller-policy"
@ -187,6 +187,7 @@ let
"swarm-bao-hive-leaf-policy"
"swarm-bao-ci-runner-policy"
"swarm-bao-logs-auth-policy"
"swarm-bao-metrics-auth-policy"
"swarm-bao-forge-publisher-policy"
"swarm-bao-review-classifier-policy"
"swarm-bao-builder-policy"
@ -593,10 +594,10 @@ let
# both, and the two are separate principals precisely because a
# dashboard is not entitled to a collector's credential.
#
# The admin password is the one leaf it may create, and without `update`:
# a grant that could replace it would let a second run rotate the
# password Grafana's database already holds.
name = "the Grafana reader reads its own client secret and creates its admin password, and nothing else";
# The admin password and the metrics bearer are the leaves it may create,
# and without `update`: a grant that could replace one would let a second
# run rotate a value Grafana or the metrics store already holds.
name = "the Grafana reader reads its own client secret and creates its admin password and metrics bearer, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-grafana-oidc-policy.script;
@ -613,6 +614,13 @@ let
"read"
];
}
{
path = "secret/data/swarm/services/swarm-grafana/metrics/bearer";
caps = [
"create"
"read"
];
}
]
&& !(lib.hasInfix "secret/data/swarm/agents" s)
&& !(lib.hasInfix "secret/data/swarm/hives" s)
@ -731,6 +739,32 @@ let
in
lib.hasInfix "/logs-auth.pem ]" s && lib.hasInfix "swarm-logs-auth \"\" clientAuth" s;
}
{
# Read-only on the one leaf Grafana's host creates, and nothing else:
# this principal checks a presented bearer and does nothing with it.
name = "the metrics store responder's grant is Grafana's metrics bearer, read-only, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-metrics-auth-policy.script;
in
grantsIn s == [
{
path = "secret/data/swarm/services/swarm-grafana/metrics/bearer";
caps = [ "read" ];
}
]
&& lib.hasInfix "auth/cert/certs/swarm-metrics-auth" s
&& lib.hasInfix "allowed_common_names=swarm-metrics-auth" s
&& lib.hasInfix "token_policies=swarm-metrics-auth" s;
}
{
name = "the PKI unit signs the metrics store responder's leaf under its own subject";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-pki.script;
in
lib.hasInfix "/metrics-auth.pem ]" s && lib.hasInfix "swarm-metrics-auth \"\" clientAuth" s;
}
{
# 🩸 The half that makes the policies above bind: a policy grants only
# through a token that carries it, and a token is minted by a cert-auth
@ -942,24 +976,24 @@ let
}
{
# A store host without the granter's pair writes its grants some other
# way, so none of the eighteen units may exist. Without this arm
# way, so none of the nineteen units may exist. Without this arm
# `lib.mkIf haveGranter` could be dropped from any of them and every other
# case here would still pass.
name = "without the granter's pair none of the eighteen granting units render";
name = "without the granter's pair none of the nineteen granting units render";
ok =
let
s = baoGranterOptOut.systemd.services;
in
lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ])
# The control: the same store with the pair renders all eighteen.
# The control: the same store with the pair renders all nineteen.
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
}
{
# 🩸 What replaced the silent skip. With no bootstrap token the eighteen still
# 🩸 What replaced the silent skip. With no bootstrap token the nineteen still
# render, and a refused granter fails them with the step that fixes it.
# A store host that never named a token is told to name one, since the
# unit that sets the granter up renders only where it has.
name = "a store host without a bootstrap token renders the eighteen, each failing loudly with the one-time step";
name = "a store host without a bootstrap token renders the nineteen, each failing loudly with the one-time step";
ok =
let
s = baoGranterNoToken.systemd.services;
@ -1049,6 +1083,7 @@ let
"swarm-services-cert"
"swarm-bao-forwarder-oidc"
"swarm-bao-grafana-admin"
"swarm-bao-grafana-metrics-bearer"
]
++ policyReaders
);
@ -1776,8 +1811,8 @@ let
}
{
# What makes the case above mean something: discovery by the granter's
# certificate reaches all eighteen units, and each yields calls.
name = "the granter-policy check sees all eighteen granting units, and parses calls from each";
# certificate reaches all nineteen units, and each yields calls.
name = "the granter-policy check sees all nineteen granting units, and parses calls from each";
ok =
lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames
&& lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits)