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.
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.
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 #
- anything touching etcd, storage classes, or cluster deletion;
- cross-cluster traffic shifting during a live incident;
- auto-approval, even for "safe" change classes.
The rule of thumb, stated once more: automate diagnosis aggressively, mutation conservatively.
Next: Getting Started.
Source: wiki/Guardrails-Deep-Dive.md — this page is generated from it, so edit the markdown, not the site.