Skip to content

feat(prometheus-stack): guard Prometheus, Alertmanager and exporters behind authentication - #67

Open
Hanson-Tsai wants to merge 17 commits into
mainfrom
feat/OTTERSCALE-288-kube-rbac-proxy
Open

Hanson-Tsai wants to merge 17 commits into
mainfrom
feat/OTTERSCALE-288-kube-rbac-proxy

Conversation

@Hanson-Tsai

@Hanson-Tsai Hanson-Tsai commented Oct 8, 2026 •

Copy link
Copy Markdown

Summary

Jira: OTTERSCALE-288. Any Pod in the cluster can read prometheus-stack directly today, a tenant's Pod included:

  • Prometheus :9090: every namespace's metrics.
  • Alertmanager :9093: including silence creation.
  • kube-state-metrics :8080: resource names across namespaces.
  • node-exporter :9100 on each node.

This PR puts an authenticating, authorizing guard in front of each of them, in the chart defaults, so Dashboard installs on external clusters and open-source users get it too.

prometheus-stack 1.1.0 → 1.2.0

component before after who may read
Prometheus http :9090 listenLocal, plus a kube-rbac-proxy sidecar on https :9091 get/create on monitoring.coreos.com prometheuses/api
Alertmanager http :9093 listenLocal, plus a kube-rbac-proxy sidecar on https :9095 alertmanagers/api
node-exporter :9100 on the host subchart kubeRBACProxy on :9100; the exporter moves to 127.0.0.1:8100 get services/prometheus-stack-prometheus-node-exporter
kube-state-metrics http :8080 KSM's own --auth-filter plus --tls-config nonResourceURL /metrics get
  • One cert-manager CA (templates/kube-rbac-proxy-tls.yaml) signs all four serving certs.
  • Prometheus scrapes and sends alerts through the guards with its own ServiceAccount token, verifying the certificates (ca.crt + serverName).
  • Prometheus and Alertmanager ServiceAccounts are bound to system:auth-delegator.
  • Prometheus gets get on prometheuses/api (to scrape itself) and get/create on alertmanagers/api (to scrape and send alerts) via additionalRulesForClusterRole. It has no create on its own API, so its token cannot POST /-/reload or /-/quit through the guard.
  • externalUrl is https, so alert links work.
  • config-reloader moves to localhost along with listenLocal. Its metrics and the ConfigReloaderSidecarErrors alert are given up; the dead scrape endpoints are dropped by relabeling.
  • validate.yaml refuses to render if any of these is changed, because the guard wiring hardcodes them:
    • a release name other than prometheus-stack;
    • a namespace other than monitoring;
    • any name override;
    • a namespaceOverride on kube-state-metrics or node-exporter;
    • a Prometheus or Alertmanager ServiceAccount other than the default name.
  • Prometheus image pinned to quay.io/prometheus/prometheus:v3.14.0. With listenLocal, the operator's probes run sh -c, and the upstream default -distroless image has no shell, so Prometheus would never become Ready. A renovate rule keeps the tag off -distroless.

Why KSM is not behind kube-rbac-proxy. In the subchart's kubeRBACProxy mode, the KSM container declares no ports, but its liveness and readiness probes name http and metrics. kubelet resolves probe port names per container, so the pod never becomes Ready (strconv.Atoi: parsing "http"). This is the case in the bundled 8.4.2 and in the latest 8.6.0. KSM's built-in --auth-filter does the same TokenReview + SubjectAccessReview (controller-runtime WithAuthenticationAndAuthorization) on /metrics, and leaves /livez//readyz open.

otterscale-agent 1.1.2 → 1.2.0

  • New ClusterRole <release>-metrics:
    • get/create on prometheuses/api for the Prometheus
    • get on the CA Secret, restricted by resourceNames
      It is a ClusterRole rather than a Role because monitoring may not exist yet when the agent is installed.
  • New values metrics.{prometheusURL,prometheusName,caSecret}, passed to the agent as env. An empty metrics.prometheusURL fails rendering: the agent would treat an empty env as unset and use its default.
  • New ClusterRole otterscale:metrics-cluster-view: get on prometheuses/api for that Prometheus. It is the same attribute the guard checks and the hub's cluster scope asks about (feat: guard metrics in the cluster and authorize them per scope at the hub otterscale#310). Bind it to give someone cluster-wide metrics without cluster-admin. Do not bind it to system:authenticated: that includes every ServiceAccount, so any Pod could then query Prometheus directly. Bind an oidc:-prefixed group instead.

kubevirt 1.1.1 → 1.2.0

  • The PodMonitor sets honorLabels: true, so a VM metric's namespace is the VM's own, not kubevirt. The hub's namespace scope and the new dashboard select VM metrics by namespace.
  • This changes label semantics (exported_namespace is gone), hence a minor bump. Upgrade it together with the new hub and dashboard: an old dashboard reads exported_namespace.

⚠️ Breaking for anyone reading Prometheus directly

  • :9090 is gone. Use https :9091 with a token authorized as above, and the CA from Secret prometheus-stack-kube-prom-prometheus-tls.
  • Alertmanager moved from :9093 to :9095.
  • A bundled Grafana, if enabled, needs its datasource reconfigured.

The details are in templates/NOTES.txt. Should this be 2.0.0 instead of 1.2.0?

🚧 Draft: do not merge yet

release.yml publishes every new chart version on push to main. The agent chart's default image.tag is still v1.5.0, and that agent cannot use the https path, so the dashboards would be empty. No token leaks: v1.5.0 sends none.

  1. Merge the otterscale PR and cut a release.
  2. Bump otterscale-agent appVersion and image.tag here to that release.
  3. Merge. CI then publishes prometheus-stack 1.2.0, otterscale-agent 1.2.0 and kubevirt 1.2.0.

Testing

  • make test runs the render tests now committed under tests/ (also in CI, .github/workflows/test.yml): 26 checks.
    • prometheus-stack, 16 checks:
      • guards, TLS, ServiceMonitors and RBAC;
      • probe port names resolving in the same container;
      • the validate and override failures;
      • the Prometheus image has a shell while listenLocal is on;
      • kube-prometheus-stack.enabled=false;
      • names matching across the two charts and the agent defaults.
    • otterscale-agent, metrics-cluster-view, and the kubevirt PodMonitor.
    • make test builds the prometheus-stack subchart dependency when it is missing.
  • helm lint passes on prometheus-stack, otterscale-agent and kubevirt.
  • Deployed to a single-node lab cluster (RKE2 1.35), upgrading from 1.1.0 / 1.1.2 with the helm CLI; this update was re-applied there:
    • From a Workspace Pod: :9090/:9093 refuse connections. All four guards return 401 without a token and 403 with the Pod's own token.
    • Targets: Prometheus, Alertmanager, KSM and node-exporter are all up over https. Watchdog reaches Alertmanager over https :9095.
    • The 85 dashboard queries return the same results as before.
    • Re-rendering this update changed only the Prometheus ClusterRole; no Pod restarted.
  • Not verified: a Flux-driven upgrade from 1.1.0, multi-node clusters, and externally managed clusters.

Related

🤖 Generated with Claude Code

Hanson-Tsai and others added 6 commits October 8, 2026 10:24
Prometheus and Alertmanager listen on 127.0.0.1 behind a kube-rbac-proxy
sidecar (Services move to 9091 and 9095); kube-state-metrics and
node-exporter turn on their subcharts' kubeRBACProxy. One cert-manager CA
signs the four guards and Prometheus scrapes and alerts through them with
its ServiceAccount token (OTTERSCALE-288).

BREAKING CHANGE: Prometheus is no longer reachable on :9090. Clients need
a token authorized for prometheuses/api and the CA in
prometheus-stack-kube-prom-prometheus-tls, on :9091.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Under listenLocal config-reloader binds to localhost, but
kube-prometheus-stack renders a reloader-web endpoint unconditionally in the
Prometheus and Alertmanager self-monitoring ServiceMonitors. Drop it by
relabeling instead of relying on how an unresolvable port behaves
(OTTERSCALE-288).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cs path

The agent now queries prometheus-stack's kube-rbac-proxy with its own
ServiceAccount token (OTTERSCALE-288). Bind it get/create on
prometheuses/api for the Prometheus and get on the Secret holding the
proxy's CA, and pass the URL and Secret explicitly instead of relying on
the binary's defaults.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The kube-state-metrics chart's readiness probe targets the port named
"metrics", which only the selfMonitor guard declares, so with
kubeRBACProxy alone the pod never became Ready. Enable selfMonitor and
scrape both the http and the telemetry endpoint through their guards
(OTTERSCALE-288).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The kube-state-metrics subchart hardcodes probes on the named ports http and
metrics, but with kubeRBACProxy the main container declares no ports and
kubelet resolves a named probe port only against the probed container, so
the pod never became Ready. Use KSM's --auth-filter (TokenReview and a
SubjectAccessReview on nonResourceURL /metrics) plus --tls-config with a
web-config ConfigMap instead, serving the same cert-manager certificate
(OTTERSCALE-288).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… external URLs, upgrade notes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hanson-Tsai and others added 11 commits October 8, 2026 19:08
otterscale-agent ships otterscale:metrics-cluster-view (get
prometheuses/api), bound to nobody, for cluster-scope metrics through the
OtterScale server. kubevirt 1.1.2 keeps VMI namespace labels (honorLabels)
so VM metrics land in the VM's namespace. Render tests run in CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…server checks

The role's resourceNames followed metrics.prometheusName while the server
always checks monitoring/prometheus-stack-kube-prom-prometheus: an
override silently broke every binding to the role, and an http
prometheusURL with an empty name rendered resourceNames: [""]. The rule
is now the server's fixed attribute, and the values comment says that
prometheusName moves only the agent's own grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ubuntu-latest's system Python is externally managed (PEP 668), so a bare
pip install can refuse to run. setup-python is pinned to an exact release
like setup-helm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- listenLocal makes the operator's probes exec sh in the container
- the subchart's default -distroless image has no shell, never ready
- pin quay.io/prometheus/prometheus:v3.14.0 and fix the comment
- keep renovate from moving that image to a -distroless tag
- move the guard render tests into tests/ and run them in make test
- check that a listenLocal Prometheus does not run a distroless image
- fetch the prometheus-stack subchart for make test and in ci
- split the prometheus clusterrole rule by resource
- prometheuses/api gets get only, enough for scraping itself
- alertmanagers/api keeps get and create for scraping and alerts
- its token can no longer post /-/reload or /-/quit through the guard
- test that scrape and alert verbs stay and create on itself is gone
- fail on kube-state-metrics and node-exporter namespaceOverride
- their -tls certificate and web-config stay in the release namespace
- fail when prometheus or alertmanager runs as another serviceaccount
- the auth-delegator binding names the default ones
- mirror the subchart helper so create=false falls back to default
- naming the default account explicitly still renders
- an empty url did not disable the metrics proxy
- the agent reads an empty env var as unset and uses its default
- fail at render time instead and test it
- system:authenticated includes every serviceaccount
- any pod could then query prometheus directly through the guard
- point to an oidc:-prefixed group for opening it to all users
- remove the ticket key and a private release id from values comments
- say why an override restates the sidecar list instead
- state the config-reloader trade-off without citing a private spec
- honorLabels changes what namespace means on vm metrics
- exported_namespace goes away, so this is not a patch release

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant