ocm-mcp-server
GitHub

5. Guardrails Deep Dive

Four independent layers stand between the model and your clusters. A change must pass all four, in order. The point of four layers is that each fails differently, so a gap in one is caught by the next.

Two writes meet the same four gates: the privileged, unpinned one is refused at layer one and never reaches a cluster; the compliant one is signed by a person, applied and verified

flowchart TD
    P[Agent proposes a change] --> L1
    L1{1. Static checks<br/>in the server} -->|fail| R1[Rejected, reason returned]
    L1 -->|pass| L2
    L2{2. Kyverno dry-run<br/>on the hub} -->|deny| R2[Rejected, policy message]
    L2 -->|allow| L3
    L3{3. Human approval<br/>content-bound token} -->|no token| R3[Waits, never applies]
    L3 -->|valid token| L4
    L4{4. RBAC<br/>on apply} -->|denied| R4[Blocked by Kubernetes]
    L4 -->|allowed| OK[Applied, traced, logged]
    classDef allowed stroke:#3fb950,stroke-width:2.5px,fill:#3fb95022
    classDef refused stroke:#f85149,stroke-width:2.5px,fill:#f8514922
    classDef waiting stroke:#d29922,stroke-width:2.5px,fill:#d2992222
    class OK allowed
    class R1,R2,R4 refused
    class R3 waiting

Layer 1: static checks (fast, local) #

Runs in the server before anything else, with no cluster round-trip, so the agent gets instant, specific feedback. Enforces a Restricted-Pod-Security baseline on every workload (no root/privilege-escalation, drop-ALL, seccomp, no host access including hostPort, procMount: Default only, only the namespaced-safe sysctls, no arbitrary service account or Secret access), an exact GVK allow-list, protected/platform namespaces, a volume, projected-source and Service-type allow-list, image pinning, and per-proposal limits. See Implementation for the full list.

Layer 2: Kyverno policy admission #

The 9 policies in deploy/policies/ validate the workload inside the ManifestWork envelope using Kyverno's foreach over spec.workload.manifests. This matters: normal pod-security policies only see resources after they land on a cluster. Here we validate on the hub, at propose time, via server-side dry-run, so bad content is rejected before it is ever delivered, and the agent gets the policy message to self-correct.

The policies are scoped by the label app.kubernetes.io/managed-by: ocm-mcp-server, so human platform engineers are not affected. They ship with an offline CLI test suite (make policy-test, 46 cases) that runs in CI.

Because it is just Kyverno - a CNCF policy engine whose policies are ordinary Kubernetes resources in YAML and CEL, enforced by the cluster rather than by the prompt - your existing organizational policies apply here too with no extra wiring. And you do not have to write them from scratch: the community library kyverno/policies and the searchable Kyverno Policies catalog are a ready source of validation, Pod Security Standards, and best-practice policies to adopt or adapt.

A write is two calls with a person between them, and a replayed token is rejected

Layer 3: human approval (content-bound) #

An Ed25519-signed token whose claims bind the proposal's content hash, the operation (apply or rollback), and an expiry. It approves that exact change, not a conversation. Approval is asymmetric: the CLI holds the private signing key and the server only the public key, so the server can verify a token but can never mint one. Minted only by the ocm-mcp CLI on a trusted terminal. See How It Works for the token mechanics.

That "can never mint" holds only while the signing key is genuinely off the server. Co-located under one OCM_MCP_HOME it is a filesystem convention, so OCM_MCP_STRICT=1 turns it into a precondition: the server refuses to start while the private key is readable beside it, or while the issuer and audience are left at their defaults. Both were already reported at startup, and both went to stderr - which an MCP client, launching the server over stdio, usually swallows into a log nobody opens.

Layer 4: least-privilege RBAC #

The server's hub identity can read across the OCM API and create/delete ManifestWorks and add-ons. RBAC cannot scope this to "only objects it created", so ownership of a specific ManifestWork is enforced in the application (the managed-by label plus the approved UID, checked before rollback), not by RBAC. What RBAC does guarantee is the hard boundary: no Secret reads, no exec, no arbitrary delete - even a bug in the server cannot exceed that. This is the backstop that holds when the other three are somehow bypassed.

Deliberate absences #

There is no tool for: reading Secrets, exec/port-forward, deleting arbitrary resources, cluster lifecycle operations, or approving proposals. A capability that does not exist cannot be prompt-injected into use. This is a design choice, not an oversight.

Threat model #

mindmap
  root((Threats))
    Hallucinated/destructive fix
      Layers 1-3 stop it
    Prompt injection
      Rules aren't in the prompt
      Layers 1,2,4
    Approval replay on changed content
      Token binds to content hash
    Stolen token
      TTL + single-proposal binding
    Compromised server host
      RBAC scope, no Secrets/exec to steal
    Audit tampering by agent
      Audit file outside the tool surface

What we refuse to automate, for now #

The rule of thumb, stated once more: automate diagnosis aggressively, mutation conservatively.

Next: Getting Started.