swarm-otel: authenticate ingest per hive, and stamp the hive from the receiver

The swarm collector accepted OTLP from anyone who could reach it, and took
the `hive` resource attribute from the payload. So any writer on the swarm
network could attribute metrics to any hive, and nothing downstream could
tell.

The label now comes from which receiver accepted the sample: one receiver
per hive, each behind an `oidc` extension verifying a token minted for that
hive's audience, each feeding a pipeline whose `resource` processor upserts
a constant. A sender cannot influence it, because the only input is which
authenticated port the bytes arrived on.

That multiplicity is forced rather than preferred. A processor cannot read
the token's claims — `from_context` reads request metadata, and asking it
for an auth claim yields nothing, silently, with a healthy startup — and
one receiver holding several credentials never reveals which one matched.

The per-hive ports are internal: a hive reaches its receiver as a path
under this collector's existing gateway name, so nginx (rendered from this
same evaluation) is the only thing that names a port. Fronting each hive
with its own vhost would need a certificate, a DNS name and a gateway entry
per hive to express routing the gateway already does.

Turning this on removes the unauthenticated receiver. While an open port
still accepts samples the per-hive receivers are decoration, so this is the
switch itself rather than a hardening layer beside it; a swarm that wants
the open receiver says so.

`hive-ca-trust.nix` grows `bundlePathFor`, because a consumer taking its own
CA argument has to name the bundle rather than just have `SSL_CERT_FILE`
exported at it.
This commit is contained in:
atlas 2026-08-19 14:30:23 +02:00 committed by mara
commit 08faa0970e
2 changed files with 302 additions and 23 deletions

View file

@ -39,10 +39,26 @@ let
# the bundle next to the CA and explains the split.
caHostPath = "${tlsCfg.stateDir}/trust-bundle.pem";
caContainerPath = "/run/hive-ca/trust-bundle.pem";
# One definition, used by `trustBundle` to WRITE the bundle and published
# below so a caller can NAME it. Two copies of this path would be two
# things to keep in step, and the one that drifts is the reader.
bundleDirFor = name: "/run/${name}-ca";
bundlePathFor = name: "${bundleDirFor name}/trust-bundle.pem";
in
{
inherit useSelfSigned caContainerPath;
# Where `trustBundle` below puts the assembled bundle, for the callers
# that must NAME it rather than just have it exported. `SSL_CERT_FILE` is
# set for you and needs no path here; a consumer that takes its own CA
# argument (an OIDC verifier's `issuer_ca_path`, a client's `--cacert`)
# does, and the alternative is copying `/run/<name>-ca/…` to the call
# site. That copy breaks silently: the bundle keeps being written, the
# consumer keeps reading a path that no longer exists, and the failure
# surfaces as a TLS error naming the peer rather than the file.
inherit bundlePathFor;
# Fold into the container's `bindMounts` via `//`. Binds ONLY the public
# CA cert (never the `hive-tls` state dir — it holds the CA + leaf private
# keys), read-only. Empty when not self-signed, so the whole trust path
@ -106,8 +122,8 @@ in
enable ? true,
}:
let
dir = "/run/${name}-ca";
bundlePath = "${dir}/trust-bundle.pem";
dir = bundleDirFor name;
bundlePath = bundlePathFor name;
unit = "${name}-ca-bundle";
source = if hostUnit then caHostPath else caContainerPath;
in