Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions k8s/bases/infrastructure/controllers/coroot/http-route.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
22 changes: 20 additions & 2 deletions k8s/bases/infrastructure/coroot/coroot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -77,6 +79,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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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
Expand Down
2 changes: 1 addition & 1 deletion scripts/capture-openbao-replacement/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<stage>` | A read failed, so nothing was written. Invalid arguments also exit 2. |

Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/test-coroot-reservation-refresh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading