MAQPNADocs

Governed cluster operations (AI SRE)

Give an SRE agent typed, read-only-by-default Kubernetes tools through the gateway's built-in maqpna-k8s server, with writes as GitOps proposals or human-approved changes, and every call audited.

flowchart LR
  A[SRE agent] -->|tools:maqpna-k8s| G[Gateway pipeline<br/>scope, revocation, DLP,<br/>namespace scope, ToolPolicy]
  G -->|read| K[maqpna-k8s<br/>typed tools]
  G -->|write| W{writes.mode}
  W -->|proposal| P[YAML patch<br/>audited, not applied]
  W -->|apply| H[Human approval<br/>always]
  H --> K
  K -->|Impersonate-User:<br/>maqpna-k8s-ops SA| API[Kubernetes API<br/>RBAC decides]
  K --> L[(Audit ledger<br/>resource refs, patch)]

Goal#

Let an AI agent diagnose a cluster, and propose or make small fixes, in a way an operator can trust:

  • Read-only by default, enforced by RBAC. The gateway acts as a dedicated ServiceAccount whose ClusterRole only has get and list. A prompt cannot widen that.
  • Narrow typed tools. There is no generic kubectl tool and no free-form flag or API path, so an argument can never become a command-line option (the class of CVE-2026-47250 in community Kubernetes MCP servers). Kinds come from a fixed table and names must be DNS names.
  • Every write needs a human or goes through GitOps. Writes return the YAML patch for a pull request (proposal, the default), or wait for an approval and then apply (apply).
  • Everything is audited with the resource it touched, and container output taints the session.

Prerequisites#

  • MAQPNA installed with Helm in a cluster, and the admin role for approvals.
  • metrics-server, if you want get_resource_usage.

Tools#

The built-in server is maqpna-k8s at POST /mcp/maqpna-k8s. Agents need the scope tools:maqpna-k8s (listing maqpna-k8s under Agent.spec.tools grants it); ToolPolicy rules use servers: [maqpna-k8s].

Tool Kind What it does
list_resources read Compact rows for one kind (kind, namespace, labelSelector, limit).
get_resource read One object: metadata (no managedFields, no last-applied configuration), spec, status.
describe_resource read get_resource plus the object's recent events.
get_pod_logs read The tail of a container log, bounded by maxLogLines and maxLogBytes; previous for a crashed instance. Taints the session.
list_events read Events, newest first, optionally for one object or only Warning.
get_resource_usage read CPU and memory of pods or nodes from metrics.k8s.io.
explain_failure read Why a pod, deployment, statefulset, daemonset or job fails: conditions, container states (CrashLoopBackOff, ImagePullBackOff, OOMKilled, exit codes), warning events and the failing container's last log lines. Taints the session.
scale_workload write spec.replicas of a deployment or statefulset (at most writes.maxReplicas).
rollout_restart write The kubectl rollout restart annotation on a deployment, statefulset or daemonset.
cordon_node, uncordon_node write spec.unschedulable of a node.
delete_pod write Deletes one pod (its controller recreates it); optional gracePeriodSeconds.

Kinds: pod, service, configmap, secret, persistentvolumeclaim, serviceaccount, event, node, namespace, persistentvolume, deployment, statefulset, daemonset, replicaset, job, cronjob, ingress, networkpolicy, endpointslice, horizontalpodautoscaler, poddisruptionbudget.

What the agent never sees:

  • Secret values. Secrets are requested as metadata only (PartialObjectMetadata), and any data keys are shown as <redacted>. Reading Secrets at all needs secretsMetadata: true.
  • ConfigMap values. Only keys and sizes are returned.
  • Container env values. They are replaced by <redacted>; valueFrom references stay.

Steps#

1. Enable the server#

# values.yaml
gateway:
  k8sOps:
    enabled: true
    namespaces: [shop, payments]   # empty = every namespace
    writes:
      mode: proposal               # disabled | proposal | apply
helm upgrade maqpna oci://ghcr.io/maqpna/charts/maqpna -n maqpna-system -f values.yaml

The chart creates:

Object Purpose
ServiceAccount <release>-k8s-ops The identity the tools act as; its token is never mounted.
Role <release>-gateway-impersonate-k8s-ops The gateway may impersonate that one account and nothing else.
ClusterRole <release>-k8s-ops-read get, list on workloads, pods, pods/log, events, services, config maps, claims; bound cluster-wide or per namespace.
ClusterRole <release>-k8s-ops-cluster-read Nodes, namespaces, persistent volumes; only when every namespace is in scope or allowClusterScoped is set.
ClusterRole <release>-k8s-ops-write Only with writes.rbac: true: patch on workloads and their scale, delete on pods, patch on nodes (nodes only when neither namespaces nor writes.namespaces is restricted, or allowClusterScoped is set).

2. Create the agent and its policy#

kubectl apply -f config/samples/toolpolicy-sre.yaml -f config/samples/agent-sre.yaml

The sample policy allows the read tools (rate-limited), requires approval for every write, and denies writes in kube-* and prod-* namespaces.

3. Diagnose#

maqpna call --server maqpna-k8s --tool explain_failure \
  --arg kind=deployment --arg namespace=shop --arg name=checkout
deployment/shop/checkout: MinimumReplicasUnavailable (Deployment does not have minimum availability.)
- [error] deployment/shop/checkout NotReady: 1 of 3 replicas ready
- [error] pod/shop/checkout-7d9c container app OOMKilled: restarted 4 times; last exit code 137
- [warning] pod/shop/checkout-7d9c BackOff: Back-off restarting failed container

4. Propose a fix#

With writes.mode: proposal, a write returns the patch and changes nothing:

Proposed change (NOT applied; commit it to the GitOps repository that manages deployment/shop/checkout):

# maqpna-k8s scale_workload deployment/shop/checkout
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "checkout"
  namespace: "shop"
spec:
  replicas: 5

The patch is a strategic-merge patch (kustomize patches:); deletions use $patch: delete. Commit it through your normal review. The ledger has an observe record k8s_write_proposed with the patch and its SHA-256.

5. Or apply with approval#

gateway:
  k8sOps:
    writes:
      mode: apply
      rbac: true          # bind the write ClusterRole
      approval: required  # the default

Every applied write is held as an approval (JSON-RPC -32002), even when a ToolPolicy allows it: approval: required is a floor the policy cannot lower. Approve it in the console or with maqpna approvals approve --gateway URL ID; the agent's retry applies the change. A ToolPolicy deny still wins. approval: policy lets the ToolPolicy decide alone.

Verify#

  • maqpna audit tail --agent sre shows every call with ext.k8sResource (for example deployment/shop/checkout) and ext.k8sVerb; writes also carry ext.k8sMode, and an observe record k8s_write_proposed or k8s_write_applied with ext.k8sPatch and ext.k8sPatchSha256.
  • kubectl auth can-i --as system:serviceaccount:maqpna-system:maqpna-k8s-ops patch deployments -n shop answers no until writes.rbac is set.
  • After get_pod_logs or explain_failure, maqpna taint list shows the session tainted untrusted-input, so a policy can gate exfiltration-capable tools.

Troubleshooting#

Symptom Cause Fix
-32001 … k8s_namespace_denied The namespace (or a cluster-scoped kind, or "all namespaces") is outside k8sOps.namespaces Add the namespace, or set allowClusterScoped for nodes
Tool error kubernetes API: HTTP 403 Forbidden The ServiceAccount's RBAC does not allow it (by design for writes and Secrets) writes.rbac: true for writes; secretsMetadata: true for Secrets
-32001 … k8s_writes_disabled writes.mode: disabled Use proposal or apply
the metrics API … is not available No metrics-server Install metrics-server, or set metrics: false
Helm fails: writes.mode apply needs … writes.rbac: true Apply mode without write RBAC Set writes.rbac: true

Next steps#