Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/host-modules/glue-bao-readers-policy-order.nix
atlas 6aab2ebb65 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
2026-10-15 10:48:20 +02:00

105 lines
4.1 KiB
Nix

# Glue: where the store and one of its readers share a host, the reader waits
# for the unit that writes the cert-auth role it logs in with, and for any
# other policy unit whose rules it relies on.
#
# ONE PAIRING PER FILE — the store's policy units ← the readers that rely
# on them, and nothing else. Deleting this leaves every reader as it is on a
# host whose store is remote: it may log in before its role exists, and its own
# retries are what carry it past that.
#
# ⚠️ Gated on BOTH the store and that reader being here. Off the store's host
# there is no local policy unit to order against. On the store's host without
# the reader, setting `systemd.services.<reader>.after` would define a unit
# with no ExecStart, so each gate below restates the one the reader's own
# module puts on it. A reader whose gate changes must change here too.
#
# Ordering, never a requirement: a policy unit that failed still counts as
# done, and the reader's own retries carry it past that. `wants` as well as
# `after`, so a reader started on its own pulls its policy unit into the same
# transaction.
{
lib,
config,
...
}:
let
hyperhiveCfg = config.services.hyperhive;
deployCfg = hyperhiveCfg.deploy;
baoDeploy = deployCfg.bao;
havePair = cert: key: cert != null && key != null;
# Reader unit → the gate its own module defines it under.
readersHere = {
# ./glue-queue-agent-credential.nix
swarm-bao-queue-agent =
deployCfg.hive-controller.enable
&& havePair baoDeploy.queueAgentClientCertFile baoDeploy.queueAgentClientKeyFile;
# ./swarm-grafana.nix
swarm-bao-grafana-oidc = deployCfg.grafana.enable;
# ./swarm-otel.nix
swarm-bao-otel-oidc =
deployCfg.swarm-otel.enable
&& havePair baoDeploy.otelOidcClientCertFile baoDeploy.otelOidcClientKeyFile;
# ./swarm-bao.nix: its block is gated on the store, which `orderAfterPolicies`
# already checks.
swarm-bao-forwarder-oidc = true;
# ./swarm-nats.nix: the queue's TLS leaf, not a secret, but the same wait.
swarm-bao-nats-tls = deployCfg.nats.enable;
# ./swarm-ci-runner.nix
swarm-bao-ci-runner = deployCfg.ci-runner.enable;
# ./hive-forge/default.nix
swarm-bao-forge-publisher = deployCfg.forgejo.enable;
};
# Readers that log in under another unit's principal: reader unit → the
# gate its own module defines it under, and the policy units it waits for.
readersOfOtherPolicy = {
# ./swarm-nats.nix, under the queue responder's leaf. Also after the
# publisher's policy, which denies the publisher the callout seeds' paths:
# until it is written the publisher may create them, and this unit keeps
# a seed it finds there.
swarm-bao-nats-callout-xkey = {
here = deployCfg.nats.enable;
policies = [
"swarm-bao-nats-auth-policy"
"swarm-bao-secret-publisher-policy"
];
};
# ./swarm-grafana.nix, under Grafana's leaf, which may create the admin
# password and the metrics bearer only once that policy is written.
swarm-bao-grafana-admin = {
here = deployCfg.grafana.enable;
policies = [ "swarm-bao-grafana-oidc-policy" ];
};
swarm-bao-grafana-metrics-bearer = {
here = deployCfg.grafana.enable;
policies = [ "swarm-bao-grafana-oidc-policy" ];
};
# ./hive-matrix.nix, under matrix-ctl's leaf.
swarm-bao-matrix-oidc = {
here = deployCfg.matrix.enable;
policies = [ "swarm-bao-matrix-ctl-policy" ];
};
# ./hive-forge/default.nix, under the forge publisher's leaf.
swarm-bao-forge-oidc = {
here = deployCfg.forgejo.enable;
policies = [ "swarm-bao-forge-publisher-policy" ];
};
};
orderAfterPolicies =
reader: here: policies:
lib.mkIf (baoDeploy.enable && here) {
systemd.services.${reader} = {
after = map (p: "${p}.service") policies;
wants = map (p: "${p}.service") policies;
};
};
in
{
config = lib.mkMerge (
lib.mapAttrsToList (reader: here: orderAfterPolicies reader here [ "${reader}-policy" ]) readersHere
++ lib.mapAttrsToList (reader: r: orderAfterPolicies reader r.here r.policies) readersOfOtherPolicy
);
}