feat(observability): serve Grafana over TLS with the lab CA - #35
Merged
Conversation
Grafana published plain HTTP on the management VLAN, so every login sent its password in cleartext to anything with a port on VLAN 99. It now serves the grafana.matrix.elysium leaf issued by the lab's own CA. Four things had to move together, and each one silently breaks the stack on its own: The cookie. GF_SECURITY_COOKIE_SECURE was false because a secure cookie over plain HTTP is never sent at all — the login page accepts the password and then loops. It flips to true in the same commit that makes TLS available, never before. The healthcheck. busybox wget cannot be handed a CA, and the probe is testing that Grafana is serving rather than that a self-signed chain builds, so it uses --no-check-certificate against its own loopback. The scrape. Prometheus reaches Grafana as `grafana:3000`, not by FQDN, so the leaf carries `grafana` as an additional SAN and the job verifies properly with ca_file and server_name rather than insecure_skip_verify. Verification that is switched off is not verification. The key permissions. Grafana runs as 472:0 and cannot read a key owned by the operator at 0600. The container is given the operator's gid as a supplementary group and leaf keys are now 0640 — certificates/ is 0700, so nothing else on the host can traverse to them regardless. The alternatives were a world-readable private key, or running Grafana as the operator's uid and stranding it from its own 472-owned data volume. gen-certs.sh gains a repeatable --dns for exactly this: a service reached under more than one name needs every one of them, or verification fails for whichever is missing. Verified end to end: curl against the CA with no -k, all ten scrape targets up, Prometheus scraping https://grafana:3000/metrics with no error, login and both working datasources fine over TLS, 5 dashboards still provisioned.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last roadmap item from the certificate work. Deployed and verified.
What changed
Grafana published plain HTTP on the management VLAN — every login sent its
password in cleartext to anything with a port on VLAN 99. It now serves the
grafana.matrix.elysiumleaf issued by the lab CA.Four things that had to move together
Each of these silently breaks the stack on its own, which is why they are one
commit rather than four:
The cookie.
GF_SECURITY_COOKIE_SECUREwasfalsebecause a secure cookieover plain HTTP is never sent — you get a login page that accepts your
password and loops forever. It flips to
truein the same change that makesTLS available, never before it.
The healthcheck. busybox
wgetcannot be handed a CA, and the probe istesting that Grafana is serving rather than that a self-signed chain builds — so
it uses
--no-check-certificateagainst its own loopback.The scrape. Prometheus reaches Grafana as
grafana:3000, not by FQDN. Theleaf therefore carries
grafanaas an additional SAN, and the job verifiesproperly with
ca_file+server_namerather thaninsecure_skip_verify.Verification that is switched off is not verification.
The key permissions. Grafana runs as
472:0and cannot read a key owned bythe operator at
0600. The container gets the operator’s gid as a supplementarygroup and leaf keys are now
0640;certificates/is0700, so nothing elseon the host can traverse to them whatever the file mode says. The alternatives
were a world-readable private key, or running Grafana as the operator’s uid and
stranding it from its own 472-owned data volume.
gen-certs.shgains a repeatable--dnsfor exactly this case.Blast radius
grafanaandprometheusare recreated. Grafana’s URL changes tohttps://— an existing bookmark on
http://gets a 400, and browsers will warn untilcertificates/ca.pemis trusted (see the certificates runbook).secrets/*.sops.yamlVerification
Against the lab CA with no
-k:All ten scrape targets up, none down:
Authenticated API over TLS works (so the now-secure cookie is fine), Prometheus
and Loki datasources both report OK, a query through the Grafana proxy returns
12 series, and all 5 dashboards are still provisioned.
./scripts/validate.shpasses;docker compose configandpromtool check configclean.make validatepassesdeploy-stack.md,images/README.md,make upbanner)Noticed, unrelated
The Alertmanager datasource health endpoint returns
plugin unavailable(
errorMessageID=plugin.unavailable). It points at internalhttp://alertmanager:9093and is untouched by this change — the failure is a Grafana plugin issue, not a
connection one. Worth its own issue rather than being folded in here.