From 51e3e3c648b1174b466f94622a5cda98f9123981 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 5 Nov 2026 00:03:48 +0100 Subject: [PATCH 1/4] feat(coroot): give agents read-only MCP access with a Viewer service account Coroot's /mcp endpoint authenticates every request itself: a bearer OAuth token or a service-account API key, never the anonymous role the UI uses behind oauth2-proxy, and every tool call is checked against that identity's role. Agents cannot complete browser SSO, so they could not reach it. Add an agentic-engineer service account with the Viewer role, whose API key the operator generates into the coroot-mcp-agent Secret, so the key is never committed. A Viewer can read topology, alerts, incidents, traces, metrics and logs but cannot resolve alerts. Route exactly /mcp from the Gateway to Coroot, keeping every other path behind oauth2-proxy, and admit Gateway traffic to the Coroot app pod only. Co-Authored-By: Claude Opus 5.5 --- ...rk-policy-allow-gateway-to-coroot-mcp.yaml | 24 +++++++++++++++++++ .../controllers/coroot/http-route.yaml | 17 +++++++++++++ .../controllers/coroot/kustomization.yaml | 1 + k8s/bases/infrastructure/coroot/coroot.yaml | 16 +++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 k8s/bases/infrastructure/controllers/coroot/cilium-network-policy-allow-gateway-to-coroot-mcp.yaml diff --git a/k8s/bases/infrastructure/controllers/coroot/cilium-network-policy-allow-gateway-to-coroot-mcp.yaml b/k8s/bases/infrastructure/controllers/coroot/cilium-network-policy-allow-gateway-to-coroot-mcp.yaml new file mode 100644 index 0000000000..af9d2be9e1 --- /dev/null +++ b/k8s/bases/infrastructure/controllers/coroot/cilium-network-policy-allow-gateway-to-coroot-mcp.yaml @@ -0,0 +1,24 @@ +# The Gateway reaches the Coroot app pod directly for exactly one route: the +# /mcp rule in http-route.yaml, which bypasses oauth2-proxy because Coroot's MCP +# endpoint authenticates every request with a bearer token itself. Only the +# Coroot app pod is selected, so no other observability workload becomes +# reachable from the Gateway. Every other path still arrives through oauth2-proxy +# and the auth-proxy, which allow-coroot admits. +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: allow-gateway-to-coroot-mcp + namespace: observability +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/part-of: coroot + app.kubernetes.io/component: coroot + ingress: + - fromEntities: + - ingress + toPorts: + - ports: + - port: "8080" + protocol: TCP diff --git a/k8s/bases/infrastructure/controllers/coroot/http-route.yaml b/k8s/bases/infrastructure/controllers/coroot/http-route.yaml index 998e27fc5c..5578c9b421 100644 --- a/k8s/bases/infrastructure/controllers/coroot/http-route.yaml +++ b/k8s/bases/infrastructure/controllers/coroot/http-route.yaml @@ -39,6 +39,23 @@ spec: hostnames: - observability.${domain} rules: + # Coroot's MCP endpoint authenticates every request itself — a bearer OAuth + # token or a service-account API key, never the anonymous role — so agents + # reach exactly /mcp directly, without the browser SSO they cannot complete. + # Every other path still goes through oauth2-proxy. + - matches: + - path: + type: Exact + value: /mcp + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: Strict-Transport-Security + value: max-age=63072000; includeSubDomains; preload + backendRefs: + - name: coroot-coroot + port: 8080 - filters: - type: ResponseHeaderModifier responseHeaderModifier: diff --git a/k8s/bases/infrastructure/controllers/coroot/kustomization.yaml b/k8s/bases/infrastructure/controllers/coroot/kustomization.yaml index e57d2113cb..cbd07620b3 100644 --- a/k8s/bases/infrastructure/controllers/coroot/kustomization.yaml +++ b/k8s/bases/infrastructure/controllers/coroot/kustomization.yaml @@ -12,6 +12,7 @@ resources: - helm-release.yaml - http-route.yaml - cilium-network-policy.yaml + - cilium-network-policy-allow-gateway-to-coroot-mcp.yaml - cron-job.yaml # Heartbeat monitor URL, held in a Secret rather than substituted into the # CronJob spec above (see that file and secret-cluster-heartbeat.yaml): diff --git a/k8s/bases/infrastructure/coroot/coroot.yaml b/k8s/bases/infrastructure/coroot/coroot.yaml index c7c1c09bd5..bdc4206364 100644 --- a/k8s/bases/infrastructure/coroot/coroot.yaml +++ b/k8s/bases/infrastructure/coroot/coroot.yaml @@ -77,6 +77,22 @@ spec: # as Admin, mirroring the old single-operator "everyone -> Grafana Admin". authAnonymousRole: Admin + # A Viewer service account for the agents' MCP access. Coroot's /mcp endpoint + # never uses the anonymous role above: every request needs a bearer token (an + # OAuth token or a service-account API key), and every tool call is checked + # against that identity's role. A Viewer can read topology, alerts, incidents, + # traces, metrics and logs but cannot resolve alerts. The operator generates + # the key into the coroot-mcp-agent Secret when it is missing; nothing about + # the key is committed. http-route.yaml routes /mcp past oauth2-proxy for it. + serviceAccounts: + - name: agentic-engineer + role: Viewer + apiKeys: + - description: Agentic Engineer MCP access (agent-plugins coroot server) + keySecret: + name: coroot-mcp-agent + key: api-key + # Metric resolution. The operator default is 15s; 30s halves the bundled # Prometheus ingest, the node-agent scrape volume, and Coroot's continuous # query load on a memory-constrained cluster, while staying at the From 40030e3f436de3b4da4d3ecf23e2d88e79ffc520 Mon Sep 17 00:00:00 2001 From: "botantler-1[bot]" <185060876+botantler-1[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:12:57 +0000 Subject: [PATCH 2/4] chore: Apply megalinter fixes --- scripts/capture-openbao-replacement/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/capture-openbao-replacement/README.md b/scripts/capture-openbao-replacement/README.md index 7331e3d857..9e622146b4 100644 --- a/scripts/capture-openbao-replacement/README.md +++ b/scripts/capture-openbao-replacement/README.md @@ -16,7 +16,7 @@ workflow runs it in `preflight` mode and then runs both judges for the server it The expected servers and release come from the reviewed render, never from the cluster. | Exit | Line | Meaning | -|---|---|---| +| --- | --- | --- | | 0 | `OPENBAO_REPLACEMENT_EVIDENCE=CAPTURED` | `health.json` and `quorum.json` are in `--out`, readable only by the owner. | | 2 | `OPENBAO_REPLACEMENT_EVIDENCE=UNVERIFIED stage=` | A read failed, so nothing was written. Invalid arguments also exit 2. | From 58e8cde5ac22eb59a966f1e3e744a3befcf898cf Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 5 Nov 2026 00:29:24 +0100 Subject: [PATCH 3/4] fix(coroot): restart Coroot when the agents' MCP key rotates The operator hands the coroot-mcp-agent key to Coroot through an env var, which is read only at start, so a rotated key would stay unusable until a restart. Add the Secret to the existing Reloader annotation. Co-Authored-By: Claude Opus 5.5 --- k8s/bases/infrastructure/coroot/coroot.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/k8s/bases/infrastructure/coroot/coroot.yaml b/k8s/bases/infrastructure/coroot/coroot.yaml index bdc4206364..43ae2a0de5 100644 --- a/k8s/bases/infrastructure/coroot/coroot.yaml +++ b/k8s/bases/infrastructure/coroot/coroot.yaml @@ -30,7 +30,9 @@ spec: # cluster-wide, `--reload-strategy=annotations`) watches the named Secret and # rolls the server's pods on change; the annotation goes on the pod template via # podAnnotations, matching the platform's existing convention (auth-proxy, - # oauth2-proxy, coredns). + # oauth2-proxy, coredns). It also watches coroot-mcp-agent: the operator passes + # the agents' MCP service-account key to Coroot through an env var too, so a + # rotated key takes effect only after a restart. # # The DNS-policy rollout token is deliberately repeated on every Coroot-owned # pod template below. set-observability-dns-ndots is admission-only, so this @@ -39,7 +41,7 @@ spec: # before this rollout. Change the token only after a new admission-time DNS # policy is Ready; combining both changes can recreate pods under the old rule. podAnnotations: - secret.reloader.stakater.com/reload: coroot-db-app + secret.reloader.stakater.com/reload: coroot-db-app,coroot-mcp-agent platform.devantler.tech/dns-policy-rollout: 2026-09-30-ndots-3 # COMPONENT IMAGE PINS. The operator does NOT bake a fixed tag into its From 36481ef2ea6b2379bb5d289b1d50b10e4a8cf7f1 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 5 Nov 2026 00:55:47 +0100 Subject: [PATCH 4/4] test(coroot): expect the MCP key Secret in Coroot's reload annotation The reservation-refresh regression pins Coroot's credential reloads so a sizing refresh cannot drop one. The reload list now deliberately also names coroot-mcp-agent; pin the new list. Co-Authored-By: Claude Opus 5.5 --- scripts/tests/test-coroot-reservation-refresh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests/test-coroot-reservation-refresh.sh b/scripts/tests/test-coroot-reservation-refresh.sh index fb96436259..1ce5e84f72 100755 --- a/scripts/tests/test-coroot-reservation-refresh.sh +++ b/scripts/tests/test-coroot-reservation-refresh.sh @@ -20,7 +20,7 @@ kubectl kustomize "${root_dir}/k8s/providers/hetzner/infrastructure" >"${scratch yq -e ' select(.kind == "Coroot" and .metadata.name == "coroot") | .spec.podAnnotations."platform.devantler.tech/vpa-recommendation-rollout" == "2026-10-11-coroot-reservation-refresh" and - .spec.podAnnotations."secret.reloader.stakater.com/reload" == "coroot-db-app" and + .spec.podAnnotations."secret.reloader.stakater.com/reload" == "coroot-db-app,coroot-mcp-agent" and .spec.replicas == 2 and .spec.resources.requests.memory == "1Gi" and .spec.resources.limits.memory == "6Gi" and