The quickstart is one command that hides the steps. Here they all are, on your own cluster. You’ll install Scrutineer, deny one domain, run an agent that tries it, and read proof of the block.

0 · Prerequisites
  • A cluster and kubectl.
  • A CNI that enforces egress NetworkPolicy (Calico, Cilium, and kind’s default all do). Unsure? Scrutineer checks for you — step 2.
  • Nodes that can pull from ghcr.io and Docker Hub.
  • Internet egress from pods — the audit-mode run in step 7 really fetches example.com.

No cert-manager, no webhooks — the install is just CRDs, RBAC, and one controller.

1 · Install the CRDs and controller
git clone --depth 1 --branch v0.2.0 https://github.com/grantbarry29/scrutineer.git
cd scrutineer
kubectl apply -k config/crd
kubectl apply -k config/default
kubectl -n scrutineer-system rollout status deployment/scrutineer-controller-manager

Want to inspect before applying? kubectl kustomize config/default | less.

2 · Check the enforcement verdict

Scrutineer tests your CNI with canary pods instead of trusting it:

kubectl -n scrutineer-system logs deployment/scrutineer-controller-manager \
  | grep 'lock probe verdict' | tail -1

Verified — enforcement is real. Refused — your CNI ignores NetworkPolicy, so Scrutineer will only run audit-only sessions rather than fake enforcement.

3 · Create a runtime profile

How governed agents run. The envoy entry gives each session its own proxy pod and locks the agent’s network to it:

# runtimeprofile.yaml
apiVersion: scrutineer.sh/v1alpha1
kind: RuntimeProfile
metadata:
  name: enforced-egress
  namespace: default
spec:
  container:
    runAsNonRoot: true
    runAsUser: 65532        # busybox runs as root by default; remap it
    runAsGroup: 65532
    readOnlyRootFilesystem: true
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
  pod:
    seccompProfile:
      type: RuntimeDefault
  enforcement:
  - name: envoy
    type: envoy
    enabled: true
kubectl apply -f runtimeprofile.yaml
4 · Write a rule

Deny one domain, enforced:

# policy.yaml
apiVersion: scrutineer.sh/v1alpha1
kind: AgentPolicy
metadata:
  name: deny-example
  namespace: default
spec:
  mode: enforced
  deniedDomains:
  - example.com
kubectl apply -f policy.yaml
5 · Run a governed session

The agent is plain busybox — it doesn’t know Scrutineer exists. It tries the denied domain:

# session.yaml
apiVersion: scrutineer.sh/v1alpha1
kind: AgentSession
metadata:
  name: first-session
  namespace: default
spec:
  task:
    description: Try one denied egress and exit
    prompt: noop
  model:
    provider: openai
    name: gpt-4.1
  runtime:
    orchestrator: kubernetes-job
    image: busybox:latest
    command:
    - sh
    - -c
    - |
      wget -q -O /dev/null http://example.com/ \
        && echo "egress succeeded" \
        || echo "egress blocked"
      exit 0
  runtimeProfileRef:
    name: enforced-egress
  policyRefs:
  - kind: AgentPolicy
    name: deny-example
kubectl apply -f session.yaml
kubectl get agentsessions -w        # Pending → Running → Succeeded

While it runs, kubectl get pods,networkpolicies shows the agent pod, the first-session-egress proxy pod, and two NetworkPolicies: the routing lock on the agent, a backstop on the proxy.

6 · Read the evidence
kubectl get agentsession first-session -o jsonpath='{.status.policyDecisions}' \
  | jq '.[] | select(.phase=="runtime")'
{
  "type": "network",
  "target": "example.com",
  "action": "deny",
  "assuranceLevel": "observed",
  ...
}

(The reporter batches; if the output is empty, wait a moment and re-run. The select skips merge-phase entries — control-plane records of how your policies were resolved — and keeps what the proxy observed.)

observed = reported by the proxy under its own identity. The agent can’t forge or suppress it; anything the agent submits is stamped self-reported instead. (Why.)

The block itself, at the proxy:

kubectl logs first-session-egress -c envoy | grep example.com   # → 403
7 · Flip the rule to audit-only

One field turns enforcement into rehearsal — traffic flows, the denial is recorded as dry-run. Flip the policy, then run a second session under it:

kubectl patch agentpolicy deny-example --type merge -p '{"spec":{"mode":"audit-only"}}'
sed 's/name: first-session/name: first-session-audit/' session.yaml > session-audit.yaml
kubectl apply -f session-audit.yaml
kubectl get agentsessions -w

Same agent, same rule: this time the wget goes through. Read the new record:

kubectl get agentsession first-session-audit -o jsonpath='{.status.policyDecisions}' \
  | jq '.[] | select(.phase=="runtime")'

"action": "dry-run" — still observed. And first-session still holds its deny: re-run step 6 and compare them side by side. Mode changed what happened, never what was seen.

A second session — not a delete-and-rerun — on purpose: the session object is the audit record, and today the evidence lives only in its status. Delete the session and the record is gone. Rerun by creating a new session; keep the old one.

8 · Clean up
kubectl delete -f session.yaml -f session-audit.yaml -f policy.yaml -f runtimeprofile.yaml
kubectl delete -k config/default

(config/default includes the CRDs, so the second command removes everything — including any sessions still on the cluster, and their evidence with them.)

What you just saw: governance from outside the agent’s trust domain (stock busybox, zero cooperation) · enforcement verified against your CNI, not assumed · a block that really happened, with a record the agent couldn’t forge.

Next: the demo adds a live bypass attempt; core concepts explains the objects you just used.