<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Scrutineer</title>
    <link>http://scrutineer.sh/</link>
    <description>Recent content on Scrutineer</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 12 Jul 2026 00:00:00 -0700</lastBuildDate>
    <atom:link href="http://scrutineer.sh/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Core concepts</title>
      <link>http://scrutineer.sh/docs/concepts/core-concepts/</link>
      <pubDate>Tue, 07 Jul 2026 00:00:00 -0700</pubDate>
      <guid>http://scrutineer.sh/docs/concepts/core-concepts/</guid>
      <description>Sessions, policies, profiles, the two locks, and evidence assurance.</description>
      <content:encoded><![CDATA[<p>Five objects, two locks, one rule about evidence. This is the mental model — the
<a href="https://github.com/grantbarry29/scrutineer/tree/main/docs/design">design docs</a> carry
the detail.</p>
<details class="docs-details" id="the-object-model">
  <summary>The object model</summary>
  <div class="docs-details-body">
    <p>Kubernetes CRDs, API group <code>scrutineer.sh/v1alpha1</code>:</p>
<ul>
<li><strong><code>AgentSession</code></strong> — one governed run: the agent image, the task, references to
policy and profile. All runtime evidence lands in its <code>status</code>.</li>
<li><strong><code>AgentPolicy</code></strong> — the rules, plus the <strong>mode</strong>: <code>enforced</code> (violations blocked) or
<code>audit-only</code> (recorded as <code>dry-run</code>, nothing blocked).</li>
<li><strong><code>RuntimeProfile</code></strong> — how the workload runs: container hardening and which
enforcement backends are on.</li>
<li><strong><code>ApprovalPolicy</code> / <code>ApprovalRequest</code></strong> — scoped human approvals for actions that
need a person in the loop.</li>
</ul>

  </div>
</details>

<details class="docs-details" id="bring-your-own-agent">
  <summary>Bring your own agent</summary>
  <div class="docs-details-body">
    <p>The container image <em>is</em> the agent — reasoning loop, model calls, tools. Scrutineer
schedules it and governs what it can reach; nothing in the image needs to cooperate.
The demo agent is plain busybox.</p>

  </div>
</details>

<details class="docs-details" id="the-two-locks">
  <summary>The two locks</summary>
  <div class="docs-details-body">
    <p>Enforcement and credentials both live outside the agent&rsquo;s trust domain:</p>
<p><strong>Routing lock.</strong> A default-deny egress NetworkPolicy makes the per-session Envoy
proxy the agent&rsquo;s <em>only</em> network path. The <code>HTTP_PROXY</code> env is a convenience; the
kernel-level deny is the control. Raw sockets and direct DNS die at the CNI.</p>
<p><strong>Capability lock.</strong> The agent pod is credential-empty. Secrets that authorize
governed actions live outside its reach — a compromised agent has nothing to
exfiltrate that would let it act ungoverned.</p>

  </div>
</details>

<details class="docs-details" id="verified-or-refused">
  <summary>Verified or refused</summary>
  <div class="docs-details-body">
    <p>The routing lock is only real if the CNI enforces NetworkPolicy — some don&rsquo;t.
Scrutineer proves it with a canary probe instead of assuming it. Fail the probe and
enforced sessions are <strong>refused</strong> with a loud <code>EgressLockVerified=False</code> condition.
Nothing silently degrades to advisory.</p>

  </div>
</details>

<details class="docs-details" id="evidence-assurance">
  <summary>Evidence assurance</summary>
  <div class="docs-details-body">
    <p>Every decision in <code>status.policyDecisions</code> carries a provenance label:</p>
<ul>
<li><strong><code>observed</code></strong> — reported by the egress-reporter in the proxy pod, authenticated as
<em>that pod&rsquo;s own identity</em> (token review + ownership checks).</li>
<li><strong><code>self-reported</code></strong> — anything from inside the agent&rsquo;s trust domain.</li>
</ul>
<p>The label is derived from the caller&rsquo;s authenticated identity, never from the
payload — a report can <em>claim</em> <code>observed</code>, and the server overwrites the claim with
what the caller&rsquo;s token proves. The doctrine throughout: a control is either
untamperable, or it is labeled for what it is.</p>

  </div>
</details>

]]></content:encoded>
    </item>
    <item>
      <title>Quickstart</title>
      <link>http://scrutineer.sh/docs/getting-started/quickstart/</link>
      <pubDate>Tue, 07 Jul 2026 00:00:00 -0700</pubDate>
      <guid>http://scrutineer.sh/docs/getting-started/quickstart/</guid>
      <description>One command to a running, lock-verified Scrutineer on a local kind cluster — about five minutes.</description>
      <content:encoded><![CDATA[<p>One command, a local <a href="https://kind.sigs.k8s.io/">kind</a> cluster, about five minutes —
and the enforcement guarantee is <em>proved</em>, not assumed, before it reports success.
(Prefer to see every step on your own cluster?
<a href="/docs/getting-started/install/">Install on your cluster</a>.)</p>
<details class="docs-details">
  <summary>0 · Prerequisites</summary>
  <div class="docs-details-body">
    <ul>
<li><strong>Docker</strong>, <strong>kind</strong>, <strong>kubectl</strong>, and <strong>Go ≥ 1.21</strong> on your PATH (kind v0.31–v0.32
tested). Any modern Go works — the build pins its own exact toolchain.
Missing something? <code>make quickstart</code> names it and shows the install command.</li>
<li>Internet egress from the cluster — the sample agents fetch <code>example.com</code>.</li>
<li>~5 minutes on a first run — the images are always built from your checkout, never
pulled, so what runs is exactly what you cloned. Repeats are faster.</li>
</ul>

  </div>
</details>

<details class="docs-details">
  <summary>1 · Run it</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git clone https://github.com/grantbarry29/scrutineer.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> scrutineer
</span></span><span class="line"><span class="cl">make quickstart
</span></span></code></pre></div><p>Creates a <code>scrutineer-quickstart</code> kind cluster, installs Scrutineer, then runs a
canary probe to prove the CNI actually enforces NetworkPolicy. It ends with the
verdict:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt;&gt; routing-lock enforcement VERIFIED on this cluster (enforced sessions will run).
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>2 · If it says REFUSED</summary>
  <div class="docs-details-body">
    <p>Your CNI doesn&rsquo;t enforce NetworkPolicy, so Scrutineer refuses to claim enforcement
rather than pretend. Retry on Calico:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">make quickstart-down
</span></span><span class="line"><span class="cl">make quickstart <span class="nv">QUICKSTART_CNI</span><span class="o">=</span>calico
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>3 · Try a sample session</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl apply -f config/samples/scrutineer_v1alpha1_agentsession.yaml
</span></span><span class="line"><span class="cl">kubectl get agentsessions -w
</span></span><span class="line"><span class="cl">kubectl get agentsession github-readme-update -o yaml   <span class="c1"># the run&#39;s record, evidence included</span>
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>4 · Tear down</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">make quickstart-down
</span></span></code></pre></div>
  </div>
</details>

<p>Next: the <a href="/docs/getting-started/demo/">demo</a> — a live denial, a dead bypass attempt,
and evidence the agent couldn&rsquo;t forge, in one run.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Demo: untamperable egress governance</title>
      <link>http://scrutineer.sh/docs/getting-started/demo/</link>
      <pubDate>Tue, 07 Jul 2026 00:00:00 -0700</pubDate>
      <guid>http://scrutineer.sh/docs/getting-started/demo/</guid>
      <description>Two sessions, same bring-your-own agent: a live denial, a bypass attempt killed by the routing lock, and evidence the agent couldn&amp;rsquo;t forge.</description>
      <content:encoded><![CDATA[<p>One run, two sessions, same plain-busybox agent: one <code>enforced</code>, one <code>audit-only</code>.
You&rsquo;ll see a denied request rejected live, a bypass attempt die in the kernel, and all
of it recorded as evidence the agent couldn&rsquo;t forge.</p>
<details class="docs-details">
  <summary>0 · Prerequisites</summary>
  <div class="docs-details-body">
    <p>A <a href="/docs/getting-started/quickstart/">quickstart</a> cluster that ended <code>VERIFIED</code>, with
internet egress (the demo fetches <code>example.com</code>).</p>

  </div>
</details>

<details class="docs-details">
  <summary>1 · Run it</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">make demo
</span></span></code></pre></div><p>Applies one hardened <code>RuntimeProfile</code>, two <code>AgentPolicy</code> objects differing <strong>only in
<code>mode</code></strong>, and two sessions running the same busybox agent — nothing in the image
cooperates with enforcement. Each agent probes three paths and prints what it
experienced (~2 minutes):</p>
<table>
	<thead>
			<tr>
					<th>probe</th>
					<th><code>demo-enforced</code></th>
					<th><code>demo-audit</code></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>example.com</code> via the proxy (allowlisted)</td>
					<td><code>SUCCEEDED</code></td>
					<td><code>SUCCEEDED</code></td>
			</tr>
			<tr>
					<td><code>example.net</code> via the proxy (not allowlisted)</td>
					<td><code>BLOCKED</code></td>
					<td><code>SUCCEEDED</code>, recorded as <code>dry-run</code></td>
			</tr>
			<tr>
					<td>direct DNS, skipping the proxy (bypass attempt)</td>
					<td><code>BLOCKED</code></td>
					<td><code>BLOCKED</code></td>
			</tr>
	</tbody>
</table>
<p>Notice: the proxy env is a convenience, not the control — the default-deny
NetworkPolicy is why the bypass dies. And it dies in <strong>both</strong> modes: <code>audit-only</code>
relaxes blocking, never the routing lock, or the observations couldn&rsquo;t be trusted.</p>

  </div>
</details>

<details class="docs-details">
  <summary>2 · Read the evidence</summary>
  <div class="docs-details-body">
    <p><code>make demo</code> ends by printing <code>status.policyDecisions</code> for both sessions.</p>
<ul>
<li><strong>action</strong> — <code>deny</code> (enforced) vs <code>dry-run</code> (audit) for <code>example.net</code>. Mode changed
what <em>happened</em>, never what was <em>seen</em>.</li>
<li><strong>assurance</strong> — every entry is <code>observed</code>: reported by the proxy pod under its own
identity. The agent has no path to inject or launder evidence.
(<a href="/docs/concepts/core-concepts/#evidence-assurance">Why.</a>)</li>
<li><strong>what&rsquo;s absent</strong> — the bypass attempt left no entry. The CNI drops it silently;
recording attempts unforgeably is a tracked roadmap item, stated rather than hidden.</li>
</ul>
<p>Dig further:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl get agentsession demo-enforced -o yaml
</span></span><span class="line"><span class="cl">kubectl get events --field-selector involvedObject.name<span class="o">=</span>demo-enforced
</span></span><span class="line"><span class="cl">kubectl get pods -l scrutineer.sh/session
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>3 · Honest boundaries</summary>
  <div class="docs-details-body">
    <ul>
<li>External TLS is tunneled: filtering is by domain, not request bodies.</li>
<li>Tool and file governance have no enforcement backend yet — removed rather than
shipped as advisory; they return as out-of-pod chokepoints.</li>
<li>The guarantee assumes an enforcing CNI (proved by the gate) and an uncompromised
node — spelled out in the
<a href="https://github.com/grantbarry29/scrutineer/tree/main/docs/design">design docs</a>.</li>
</ul>

  </div>
</details>

<details class="docs-details">
  <summary>4 · Clean up</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">make demo-down        <span class="c1"># remove the demo sessions/policies/profile</span>
</span></span><span class="line"><span class="cl">make quickstart-down  <span class="c1"># delete the kind cluster entirely</span>
</span></span></code></pre></div>
  </div>
</details>

<p>Next: <a href="/docs/getting-started/install/">install on your cluster</a> — the same story,
every command visible, on your own cluster.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Install on your cluster, step by step</title>
      <link>http://scrutineer.sh/docs/getting-started/install/</link>
      <pubDate>Sun, 12 Jul 2026 00:00:00 -0700</pubDate>
      <guid>http://scrutineer.sh/docs/getting-started/install/</guid>
      <description>Every command visible, on your existing cluster: install, deny one domain, run an agent, read the proof.</description>
      <content:encoded><![CDATA[<p>The <a href="/docs/getting-started/quickstart/">quickstart</a> is one command that hides the
steps. Here they all are, on your own cluster. You&rsquo;ll install Scrutineer, deny one
domain, run an agent that tries it, and read proof of the block.</p>
<details class="docs-details">
  <summary>0 · Prerequisites</summary>
  <div class="docs-details-body">
    <ul>
<li>A cluster and <code>kubectl</code>.</li>
<li>A CNI that enforces egress NetworkPolicy (Calico, Cilium, and kind&rsquo;s default all
do). Unsure? Scrutineer checks for you — step 2.</li>
<li>Nodes that can pull from <code>ghcr.io</code> and Docker Hub.</li>
<li>Internet egress from pods — the audit-mode run in step 7 really fetches
<code>example.com</code>.</li>
</ul>
<p>No cert-manager, no webhooks — the install is just CRDs, RBAC, and one controller.</p>

  </div>
</details>

<details class="docs-details">
  <summary>1 · Install the CRDs and controller</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git clone --depth <span class="m">1</span> --branch v0.2.0 https://github.com/grantbarry29/scrutineer.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> scrutineer
</span></span><span class="line"><span class="cl">kubectl apply -k config/crd
</span></span><span class="line"><span class="cl">kubectl apply -k config/default
</span></span><span class="line"><span class="cl">kubectl -n scrutineer-system rollout status deployment/scrutineer-controller-manager
</span></span></code></pre></div><p>Want to inspect before applying? <code>kubectl kustomize config/default | less</code>.</p>

  </div>
</details>

<details class="docs-details">
  <summary>2 · Check the enforcement verdict</summary>
  <div class="docs-details-body">
    <p>Scrutineer tests your CNI with canary pods instead of trusting it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl -n scrutineer-system logs deployment/scrutineer-controller-manager <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep <span class="s1">&#39;lock probe verdict&#39;</span> <span class="p">|</span> tail -1
</span></span></code></pre></div><p><code>Verified</code> — enforcement is real. <code>Refused</code> — your CNI ignores NetworkPolicy, so
Scrutineer will only run <code>audit-only</code> sessions rather than fake enforcement.</p>

  </div>
</details>

<details class="docs-details">
  <summary>3 · Create a runtime profile</summary>
  <div class="docs-details-body">
    <p>How governed agents run. The <code>envoy</code> entry gives each session its own proxy pod and
locks the agent&rsquo;s network to it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># runtimeprofile.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">scrutineer.sh/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">RuntimeProfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">enforced-egress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runAsNonRoot</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runAsUser</span><span class="p">:</span><span class="w"> </span><span class="m">65532</span><span class="w">        </span><span class="c"># busybox runs as root by default; remap it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runAsGroup</span><span class="p">:</span><span class="w"> </span><span class="m">65532</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">readOnlyRootFilesystem</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">allowPrivilegeEscalation</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">capabilities</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">drop</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;ALL&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pod</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">seccompProfile</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RuntimeDefault</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enforcement</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">envoy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">envoy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl apply -f runtimeprofile.yaml
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>4 · Write a rule</summary>
  <div class="docs-details-body">
    <p>Deny one domain, enforced:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># policy.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">scrutineer.sh/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">AgentPolicy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">deny-example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">enforced</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deniedDomains</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">example.com</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl apply -f policy.yaml
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>5 · Run a governed session</summary>
  <div class="docs-details-body">
    <p>The agent is plain busybox — it doesn&rsquo;t know Scrutineer exists. It tries the denied
domain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># session.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">scrutineer.sh/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">AgentSession</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">first-session</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">task</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Try one denied egress and exit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">prompt</span><span class="p">:</span><span class="w"> </span><span class="l">noop</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">model</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">openai</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gpt-4.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runtime</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">orchestrator</span><span class="p">:</span><span class="w"> </span><span class="l">kubernetes-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">busybox:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- -<span class="l">c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      wget -q -O /dev/null http://example.com/ \
</span></span></span><span class="line"><span class="cl"><span class="sd">        &amp;&amp; echo &#34;egress succeeded&#34; \
</span></span></span><span class="line"><span class="cl"><span class="sd">        || echo &#34;egress blocked&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      exit 0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runtimeProfileRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">enforced-egress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">policyRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">AgentPolicy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">deny-example</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl apply -f session.yaml
</span></span><span class="line"><span class="cl">kubectl get agentsessions -w        <span class="c1"># Pending → Running → Succeeded</span>
</span></span></code></pre></div><p>While it runs, <code>kubectl get pods,networkpolicies</code> shows the agent pod, the
<code>first-session-egress</code> proxy pod, and two NetworkPolicies: the routing lock on the
agent, a backstop on the proxy.</p>

  </div>
</details>

<details class="docs-details">
  <summary>6 · Read the evidence</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl get agentsession first-session -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.policyDecisions}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq <span class="s1">&#39;.[] | select(.phase==&#34;runtime&#34;)&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;network&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;target&#34;</span><span class="p">:</span> <span class="s2">&#34;example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;deny&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;assuranceLevel&#34;</span><span class="p">:</span> <span class="s2">&#34;observed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="err">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>(The reporter batches; if the output is empty, wait a moment and re-run. The <code>select</code>
skips <code>merge</code>-phase entries — control-plane records of how your policies were
resolved — and keeps what the proxy <em>observed</em>.)</p>
<p><code>observed</code> = reported by the proxy under its own identity. The agent can&rsquo;t forge or
suppress it; anything the agent submits is stamped <code>self-reported</code> instead.
(<a href="/docs/concepts/core-concepts/#evidence-assurance">Why.</a>)</p>
<p>The block itself, at the proxy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl logs first-session-egress -c envoy <span class="p">|</span> grep example.com   <span class="c1"># → 403</span>
</span></span></code></pre></div>
  </div>
</details>

<details class="docs-details">
  <summary>7 · Flip the rule to audit-only</summary>
  <div class="docs-details-body">
    <p>One field turns enforcement into rehearsal — traffic flows, the denial is recorded as
<code>dry-run</code>. Flip the policy, then run a <strong>second</strong> session under it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl patch agentpolicy deny-example --type merge -p <span class="s1">&#39;{&#34;spec&#34;:{&#34;mode&#34;:&#34;audit-only&#34;}}&#39;</span>
</span></span><span class="line"><span class="cl">sed <span class="s1">&#39;s/name: first-session/name: first-session-audit/&#39;</span> session.yaml &gt; session-audit.yaml
</span></span><span class="line"><span class="cl">kubectl apply -f session-audit.yaml
</span></span><span class="line"><span class="cl">kubectl get agentsessions -w
</span></span></code></pre></div><p>Same agent, same rule: this time the wget goes through. Read the new record:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl get agentsession first-session-audit -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.policyDecisions}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq <span class="s1">&#39;.[] | select(.phase==&#34;runtime&#34;)&#39;</span>
</span></span></code></pre></div><p><code>&quot;action&quot;: &quot;dry-run&quot;</code> — still <code>observed</code>. And <code>first-session</code> still holds its <code>deny</code>:
re-run step 6 and compare them side by side. Mode changed what <em>happened</em>, never what
was <em>seen</em>.</p>
<p>A second session — not a delete-and-rerun — on purpose: the session object <strong>is</strong> the
audit record, and today the evidence lives only in its <code>status</code>. Delete the session
and the record is gone. Rerun by creating a new session; keep the old one.</p>

  </div>
</details>

<details class="docs-details">
  <summary>8 · Clean up</summary>
  <div class="docs-details-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">kubectl delete -f session.yaml -f session-audit.yaml -f policy.yaml -f runtimeprofile.yaml
</span></span><span class="line"><span class="cl">kubectl delete -k config/default
</span></span></code></pre></div><p>(<code>config/default</code> includes the CRDs, so the second command removes everything —
including any sessions still on the cluster, and their evidence with them.)</p>

  </div>
</details>

<p><strong>What you just saw:</strong> governance from outside the agent&rsquo;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&rsquo;t forge.</p>
<p>Next: the <a href="/docs/getting-started/demo/">demo</a> adds a live bypass attempt;
<a href="/docs/concepts/core-concepts/">core concepts</a> explains the objects you just used.</p>
]]></content:encoded>
    </item>
    <item>
      <title>FAQ</title>
      <link>http://scrutineer.sh/docs/faq/</link>
      <pubDate>Sun, 12 Jul 2026 00:00:00 -0700</pubDate>
      <guid>http://scrutineer.sh/docs/faq/</guid>
      <description>Why not just a firewall, a service mesh, or a CNI? Honest comparisons and the reasoning behind the enforcement model.</description>
      <content:encoded><![CDATA[<p>The questions a technical evaluator asks first — answered with the boundaries stated,
not hidden.</p>
<details class="docs-details" id="diy">
  <summary>Couldn&#39;t I build this myself with an egress proxy and a NetworkPolicy?</summary>
  <div class="docs-details-body">
    <p>The <em>mechanism</em> — yes, deliberately so. Scrutineer&rsquo;s data plane is commodity on
purpose: a per-session Envoy and a default-deny NetworkPolicy, portable to any
conformant cluster. A competent team can stand that up in an afternoon.</p>
<p>The product is what makes the mechanism a <em>guarantee</em>:</p>
<table>
	<thead>
			<tr>
					<th>A proxy pod + NetworkPolicy gives you</th>
					<th>What the DIY version is missing</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>A policy object that <em>should</em> deny traffic</td>
					<td>Proof that it does. On a CNI that ignores NetworkPolicy the lock is a <strong>silent no-op</strong>. Scrutineer probes the CNI and <strong>refuses</strong> enforced sessions rather than degrade.</td>
			</tr>
			<tr>
					<td>One shared, long-lived proxy</td>
					<td>Per-session isolation: a dedicated proxy per session, own identity, torn down with it. Attribution is structural; blast radius is one session.</td>
			</tr>
			<tr>
					<td>Firewall <em>logs</em></td>
					<td><em>Evidence</em>: records authenticated as the proxy pod&rsquo;s own identity, stamped <code>observed</code> server-side, landing in the session&rsquo;s <code>status</code> as an API object.</td>
			</tr>
			<tr>
					<td>DNS egress you probably left open</td>
					<td>No direct DNS at all — the proxy resolves. An open DNS allowance is a ready-made exfil tunnel.</td>
			</tr>
			<tr>
					<td>An allow-list you edit by hand</td>
					<td>Policy as an API: <code>enforced</code> vs <code>audit-only</code> dry-runs, fail-closed live updates, injection-safe validation.</td>
			</tr>
			<tr>
					<td>Blocking</td>
					<td>A session lifecycle: verified-or-refused admission, mid-run approvals, cancellation, credential-empty agent pods.</td>
			</tr>
	</tbody>
</table>
<p>Build all of that and you haven&rsquo;t disproven the product — you&rsquo;ve rebuilt it.</p>

  </div>
</details>

<details class="docs-details" id="cilium">
  <summary>Why not Cilium with toFQDN policies and Hubble?</summary>
  <div class="docs-details-body">
    <p>The strongest existing alternative — and a fine CNI <em>under</em> Scrutineer; the probe will
happily verify it. The differences are the layer above:</p>
<ul>
<li><strong>Portability</strong> — Scrutineer&rsquo;s guarantee is CNI-agnostic; toFQDN ties policy to one CNI.</li>
<li><strong>Name semantics</strong> — toFQDN maps DNS answers to IPs, with the races that implies.
Scrutineer&rsquo;s proxy is handed the <em>name</em> and resolves it itself.</li>
<li><strong>Flows vs. evidence</strong> — Hubble exports flows. Scrutineer records
identity-authenticated decisions bound to a session object, with assurance labels.</li>
<li><strong>No session model</strong> — admission gating, approvals, audit-vs-enforce, one API object
per run.</li>
</ul>

  </div>
</details>

<details class="docs-details" id="cooperation">
  <summary>Does the agent have to cooperate with the proxy?</summary>
  <div class="docs-details-body">
    <p>No — the design assumes it won&rsquo;t. Proxy env is injected so well-behaved HTTP stacks
route automatically; everything else gets nothing. Direct connections die at the CNI,
DNS doesn&rsquo;t resolve. <strong>Through the proxy, or nowhere</strong> — security holds at zero
cooperation; cooperation only buys the agent functionality.</p>
<p>One pain deliberately avoided: Envoy tunnels TLS rather than intercepting it — no MITM
CA to distribute, certificate pinning keeps working.</p>

  </div>
</details>

<details class="docs-details" id="non-http">
  <summary>What about non-HTTP protocols — databases, SSH, raw TCP?</summary>
  <div class="docs-details-body">
    <p>Covered when the client can speak an HTTP <code>CONNECT</code> tunnel (most can, or can be
wrapped): same proxy, same policy, same lock, same <code>observed</code> evidence. Recipes:
<a href="https://github.com/grantbarry29/scrutineer/blob/main/docs/egress-non-http.md">non-HTTP egress guide</a>.</p>
<p>Tools that honor no proxy config at all <strong>fail closed</strong> — no egress rather than
ungoverned egress. Transparent interception for those is
<a href="https://github.com/grantbarry29/scrutineer/issues/64">future work</a>.</p>

  </div>
</details>

<details class="docs-details" id="transparent">
  <summary>Why an explicit proxy instead of transparent interception?</summary>
  <div class="docs-details-body">
    <p>Every transparent option costs privilege somewhere sensitive: iptables in the agent
pod needs <code>NET_ADMIN</code> in the workload being governed; node-level interception needs a
privileged data plane on every node; CNI-native redirect couples the product to one
CNI. The explicit proxy adds <strong>nothing privileged anywhere</strong>.</p>
<p>It also sees destinations as clean <em>names</em> — the policy vocabulary — where transparent
interception sees addresses and reconstructs names from SNI or DNS correlation. The
trade: proxy-oblivious tools fail closed until the
<a href="https://github.com/grantbarry29/scrutineer/issues/64">transparent backend</a> exists.
But the steering mechanism was never the security boundary — the lock is.</p>

  </div>
</details>

]]></content:encoded>
    </item>
  </channel>
</rss>
