🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Kubernetes Admission Controller with Claude API and OPA

Combine OPA Gatekeeper's policy enforcement with Claude API's reasoning to catch risky Kubernetes manifests that static rules miss — and explain the violation in plain English instead of a cryptic denial message.

Shubham3 min read
Share:Tweet

OPA Gatekeeper is great at "this field must equal that value" checks. It is bad at "this deployment looks risky because the image tag is latest, there is no resource limit, and it mounts the host network — taken together that is a red flag." Claude can reason about the combination; OPA can block the request. This tool wires them together.

Architecture

kubectl apply → API server → ValidatingWebhookConfiguration
                                    ↓
                          FastAPI webhook service
                                    ↓
                    Claude API reasons over the full manifest
                                    ↓
                    allow / deny + human-readable reason

Setup

bash
pip install fastapi uvicorn anthropic kubernetes

Webhook Service

python
import base64
import json
import anthropic
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
 
app = FastAPI()
client = anthropic.Anthropic()
 
REVIEW_PROMPT = """You are a Kubernetes security reviewer. Review this manifest
being submitted to the cluster and decide if it should be ALLOWED or DENIED.
 
Deny if you see combinations of risk, not just single flags:
- image tag "latest" combined with no resource limits
- hostNetwork or hostPID set to true without an obvious system-level reason
- privileged: true or allowPrivilegeEscalation: true
- containers running as root (no runAsNonRoot) in a namespace that isn't kube-system
- secrets mounted as environment variables instead of volumes (softer flag)
 
Manifest:
{manifest}
 
Respond with ONLY valid JSON: {{"allowed": true/false, "reason": "one sentence, specific"}}"""
 
 
def review_with_claude(manifest: dict) -> dict:
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": REVIEW_PROMPT.format(manifest=json.dumps(manifest, indent=2))
        }]
    )
    text = response.content[0].text.strip()
    if text.startswith("```"):
        text = text.split("```")[1].replace("json", "", 1).strip()
    return json.loads(text)
 
 
@app.post("/validate")
async def validate(request: Request):
    body = await request.json()
    admission_request = body["request"]
    uid = admission_request["uid"]
    manifest = admission_request["object"]
 
    # Skip system namespaces — Claude should not gate kube-system
    namespace = manifest.get("metadata", {}).get("namespace", "default")
    if namespace in ("kube-system", "kube-public"):
        return admission_response(uid, allowed=True)
 
    verdict = review_with_claude(manifest)
 
    return admission_response(uid, allowed=verdict["allowed"], reason=verdict["reason"])
 
 
def admission_response(uid: str, allowed: bool, reason: str = "") -> JSONResponse:
    return JSONResponse({
        "apiVersion": "admission.k8s.io/v1",
        "kind": "AdmissionReview",
        "response": {
            "uid": uid,
            "allowed": allowed,
            "status": {"message": reason} if reason else {},
        },
    })

Register the Webhook

yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: claude-manifest-reviewer
webhooks:
- name: review.claude.devopsboys.com
  clientConfig:
    service:
      name: claude-reviewer
      namespace: security
      path: "/validate"
    caBundle: <base64-ca-cert>
  rules:
  - apiGroups: ["apps", ""]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["deployments", "pods"]
  failurePolicy: Ignore    # Never block the cluster if the webhook is down
  timeoutSeconds: 5
  admissionReviewVersions: ["v1"]

failurePolicy: Ignore is not optional here — an LLM call can time out or the API can rate-limit you. If that happens, you want deployments to go through, not the whole cluster to freeze.

What This Catches That Gatekeeper Rego Misses

Manifest: image "myapp:latest", no resource limits, runAsUser not set

Claude verdict:
{
  "allowed": false,
  "reason": "Image tag 'latest' with no resource limits and no runAsUser means
  an untested image can run as root with unbounded memory — combine a pinned
  tag, resource limits, and runAsNonUser: true before resubmitting."
}

A Rego rule catching "latest" alone gives a flood of false positives on dev namespaces. Claude reasoning over the combination is closer to what a human reviewer would flag.

Guardrails to Add Before Production

  • Cache verdicts per image digest for 10 minutes — most CI pipelines redeploy the same image many times
  • Set a hard 3-second timeout and fail open on timeout
  • Log every denial with the full manifest for audit — never let an LLM decision be a black box
  • Run this alongside Gatekeeper, not instead of it — Gatekeeper handles the deterministic rules, Claude handles judgment calls

More AI DevOps tools? Read our Build AI deployment validator with Claude API and OPA and Build AI Kubernetes resource optimizer with Claude API.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments