<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Josh Waldrep</title>
    <description>The latest articles on DEV Community by Josh Waldrep (@luckypipewrench).</description>
    <link>https://dev.to/luckypipewrench</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3760698%2Ffa927480-f508-46a4-850d-e1102ab83263.png</url>
      <title>DEV Community: Josh Waldrep</title>
      <link>https://dev.to/luckypipewrench</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luckypipewrench"/>
    <language>en</language>
    <item>
      <title>Your benign set should look malicious</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:33:58 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/your-benign-set-should-look-malicious-pnk</link>
      <guid>https://dev.to/luckypipewrench/your-benign-set-should-look-malicious-pnk</guid>
      <description>&lt;p&gt;A 0.1% false-positive rate against clean traffic means nothing. The test proved a detector can ignore a health-check ping and a plain JSON API call. Nobody was worried about those. The traffic that trips a real detector is the stuff that looks like an attack and isn't one, and if your benign test set has none of that in it, your false-positive rate is a participation trophy.&lt;/p&gt;

&lt;p&gt;I write detections for AI agent egress, so I see this a lot. An agent reads a security advisory that quotes a prompt-injection string. It calls a scanner whose tool description lists SQL injection, XSS, and SSRF. A tutorial it pastes has AWS's own fake key in it, &lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt;. A log it summarizes says "token expired" and "401 unauthorized" a few hundred times. Normal work, all of it, covered in the exact features a naive rule is built to catch.&lt;/p&gt;

&lt;p&gt;That's the problem. Your benign set has to look malicious, or it isn't testing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy-negative trap
&lt;/h2&gt;

&lt;p&gt;Precision and recall are old news. Detection engineers have been fighting false positives in SIEM and EDR for years, so this isn't new math. The trap is quieter than that.&lt;/p&gt;

&lt;p&gt;When you build a benign corpus, most people grab obvious clean traffic. A GET to a public API. A form submission. A CDN fetch. Your detector sails through all of it, you write down a beautiful number, and you ship. But you never asked it the ugly question: what does the rule do when a harmless input is wearing an attacker's clothes? Test only the easy negatives and you measured the wrong thing. You measured whether the rule stays quiet when nothing looks scary. That was never the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard negatives
&lt;/h2&gt;

&lt;p&gt;A hard negative is a benign input that carries the same surface features as an attack. The word "token" with no token behind it. A base64 blob that decodes to a PNG, not a secret. A tool schema that names ten attack types because naming them is the tool's job. A quoted injection payload sitting in a paragraph that explains the injection.&lt;/p&gt;

&lt;p&gt;These are the samples that expose a jumpy detector. Everything else is filler. If your benign set is 400 clean API calls and zero lookalikes, you can ace the false-positive side with a regex that matches nothing, and you'll have proven exactly that much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent egress makes it worse
&lt;/h2&gt;

&lt;p&gt;Classic network detection had a lot more boring benign traffic to lean on. Agent traffic gives you less of it.&lt;/p&gt;

&lt;p&gt;An agent's whole job is to move text between systems. It reads docs, runs scanners, opens source code, and forwards the mess somewhere else. So the benign stream fills up with attack-shaped text that isn't an attack, and credential-shaped text that leaks nothing. The signal a naive DLP or injection rule keys on shows up in legitimate traffic all day. Skip testing against that and your first week in production is one long false-positive incident, and your operators rip the rule out. I've watched that failure mode kill more rules than misses ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The examples that break lazy detectors
&lt;/h2&gt;

&lt;p&gt;Here is the shape of a hard-negative table. Every row is benign, and every row looks like it shouldn't be.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;Why it looks dangerous&lt;/th&gt;
&lt;th&gt;Why it can be legitimate&lt;/th&gt;
&lt;th&gt;What a real detector inspects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A doc that quotes &lt;code&gt;ignore all previous instructions...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;It contains a known injection string&lt;/td&gt;
&lt;td&gt;It's describing the attack, not issuing it&lt;/td&gt;
&lt;td&gt;Is the string quoted or fenced, or is it live in the instruction path?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tutorial with &lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;It matches the AWS key format&lt;/td&gt;
&lt;td&gt;It's AWS's published example value, in every guide&lt;/td&gt;
&lt;td&gt;Is it the documented constant, in a code-example context?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanner tool description listing XSS, SSRF, injection&lt;/td&gt;
&lt;td&gt;The schema is full of attack terms&lt;/td&gt;
&lt;td&gt;The tool advertises what it detects&lt;/td&gt;
&lt;td&gt;Is the tool invoking those, or naming them?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;token expired&lt;/code&gt;, &lt;code&gt;401 unauthorized&lt;/code&gt; in a log&lt;/td&gt;
&lt;td&gt;The keywords "token" and "auth"&lt;/td&gt;
&lt;td&gt;It's an error message with no credential in it&lt;/td&gt;
&lt;td&gt;Is there an actual secret, or only the word?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A base64 image data URI&lt;/td&gt;
&lt;td&gt;A long high-entropy blob&lt;/td&gt;
&lt;td&gt;It decodes to an inline PNG&lt;/td&gt;
&lt;td&gt;Does it decode to an image, or to a key or a command?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A UUID in a URL path&lt;/td&gt;
&lt;td&gt;A long random-looking string&lt;/td&gt;
&lt;td&gt;It's a resource id&lt;/td&gt;
&lt;td&gt;Is the entropy in a credential position or an id position?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this is rare. This is where a rule earns its keep or gets uninstalled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build your own set
&lt;/h2&gt;

&lt;p&gt;You cannot download the negatives that matter for you. The ones that break your rules come from your own mess: docs, runbooks, CI logs, scanner output, support tickets, example credentials, encoded fixtures, tool schemas. That's where your agents' benign traffic comes from, so that's where your lookalikes live.&lt;/p&gt;

&lt;p&gt;Pull from those sources. Label each sample against the policy you mean to enforce. Keep a private holdout the detector never sees during tuning. The holdout is the part people skip, and it's the part that stops you from overfitting to your own test set and calling it precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report it honestly
&lt;/h2&gt;

&lt;p&gt;A single false-positive rate lets you hide the important part. Split it. Report the rate on easy negatives and the rate on hard negatives as two numbers, because the gap is the finding. A detector at 0% on clean traffic and 30% on lookalikes panics the second benign traffic looks dangerous. Calling that 0.3% is laundering the result.&lt;/p&gt;

&lt;p&gt;Declare the unit while you're at it. False-positive rate per what: per request, per session, per URL, per document chunk, per tool call, per alert? A rate without a denominator is mush, and vendors pick the denominator that flatters them.&lt;/p&gt;

&lt;p&gt;Then show the misses. When I run my own tests, the failures get published right next to the score. If nobody can see the failures behind a number, that's a claim, not a measurement. Report only the wins and you're doing marketing, not detection engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The annoying caveat
&lt;/h2&gt;

&lt;p&gt;Whether a given hard negative should be allowed or blocked depends on your policy, not on some universal truth. Plenty of shops block any AWS-key-shaped string on sight, example value or not, and that's a defensible call. So label each sample against your policy and don't pretend it's universally safe. The benchmark has to state that policy out loud. A hard-negative set with no declared policy boundary still beats clean traffic, but it's an argument waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;A detection earns its false-positive rate by staying calm when benign traffic looks like an attack. If it never met dangerous-looking benign traffic in testing, it hasn't earned anything yet, no matter how good the number looks.&lt;/p&gt;

&lt;p&gt;I keep a public starter set of these lookalikes in &lt;a href="https://github.com/luckyPipewrench/agent-egress-bench" rel="noopener noreferrer"&gt;agent-egress-bench&lt;/a&gt;. It's tool-neutral, so run it against whatever you use. But treat it as a seed. The negatives that will break your rules are the ones from your own docs and logs, your own tools, and nobody can build those for you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why Pipelock Is an Egress Agent Firewall, Not an Inbound WAF</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Wed, 15 Jul 2026 00:12:22 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/why-pipelock-is-an-egress-agent-firewall-not-an-inbound-waf-21cg</link>
      <guid>https://dev.to/luckypipewrench/why-pipelock-is-an-egress-agent-firewall-not-an-inbound-waf-21cg</guid>
      <description>&lt;h1&gt;
  
  
  Why Pipelock Is an Egress Agent Firewall, Not an Inbound WAF
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The question behind the word firewall
&lt;/h2&gt;

&lt;p&gt;Security teams hear "firewall" and picture something inbound. A firewall, WAF, or IPS sits in front of a service. Traffic comes from the outside world toward the protected app. The control inspects requests before they reach the app and blocks malicious payloads at the door.&lt;/p&gt;

&lt;p&gt;That is outside-in protection. It fits web applications, where many attacks have recognizable request shapes: SQL injection, cross-site scripting, known exploit signatures, or malformed protocol behavior. The web server is the thing being attacked, and the attacker sends requests into it.&lt;/p&gt;

&lt;p&gt;AI agents invert that model. The agent is not only a server receiving input. It reads external content, calls tools, sends HTTP requests, invokes MCP servers, and runs with credentials. The dangerous event is rarely that a hostile packet reached the agent. The dangerous event is that the agent got talked into doing something with outbound effects.&lt;/p&gt;

&lt;p&gt;That is why Pipelock is built as an egress agent firewall, not a WAF-style inbound firewall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why inbound filtering is the wrong primary model
&lt;/h2&gt;

&lt;p&gt;Prompt injection does not behave like a structured malware packet. It is natural-language instruction sitting in places the agent is supposed to read: a web page, a ticket, a search result, a tool response, an MCP server reply, or a user message. The channel is legitimate. The syntax is often normal. The attack is semantic and context-dependent.&lt;/p&gt;

&lt;p&gt;Solving that by filtering every input before it reaches the agent turns into an enumeration problem. You write patterns for "ignore previous instructions," then the attacker rephrases. You block one formatting trick, then the instruction is split across paragraphs, hidden in quoted text, encoded, or dressed up as policy text. Known phrases are worth catching, and Pipelock catches known injection markers in content it mediates, but input filtering cannot be the center of the security model.&lt;/p&gt;

&lt;p&gt;The conclusion is blunt: inspect inbound content, but do not bet the security model on recognizing every hostile instruction before the model sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Egress control: protect the consequence
&lt;/h2&gt;

&lt;p&gt;Pipelock's stronger boundary is egress control. Rather than assume every bad instruction can be stopped on the way in, Pipelock focuses on the harmful effect on the way out.&lt;/p&gt;

&lt;p&gt;A prompt injection matters when it produces an outbound consequence: exfiltrating a secret, reaching a cloud metadata endpoint such as &lt;code&gt;169.254.169.254&lt;/code&gt;, POSTing data to an attacker-controlled server, invoking a destructive tool, or sending unsafe tool arguments to an MCP server. If the agent cannot carry out the harmful action, the injection has lost its practical effect.&lt;/p&gt;

&lt;p&gt;The short framing: let it in, but don't let it out.&lt;/p&gt;

&lt;p&gt;The most durable part of egress control is the destination-and-action check. An attacker can rephrase a prompt injection endlessly. It is much harder to rephrase the destination. A request trying to reach a link-local metadata address, a blocklisted domain, or a denied tool gets stopped on the pattern that is hardest to disguise: where the action is going and what it is asking to do. Content inspection on the way out (DLP for a secret sitting in a request body) is a second layer that helps, but it faces the same evasion pressure as inbound scanning. An attacker can encode or split a leaking secret the same way they rephrase an injection. So the destination-and-action policy carries the weight; content DLP backs it up.&lt;/p&gt;

&lt;p&gt;Input filtering asks, "Can I recognize every instruction that might cause harm?" Egress control asks, "Is this action allowed to cross the boundary?" The second question is narrower, more concrete, and closer to what security teams need to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exit-guard analogy
&lt;/h2&gt;

&lt;p&gt;An inbound filter is a guard at the front door frisking everyone for weapons. That works when the weapon is a metal object. For an agent, the weapon is a whispered instruction, and a con artist talks his way past the front desk because words do not look like a knife.&lt;/p&gt;

&lt;p&gt;Pipelock puts the guard at the exit and checks what anyone tries to carry out.&lt;/p&gt;

&lt;p&gt;The con man can talk his way in and convince an employee to pick up the files. He still has to get the briefcase out the door. At the exit, the guard checks the consequence: what is leaving, where it is going, and whether policy allows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pipelock inspects inbound today
&lt;/h2&gt;

&lt;p&gt;Pipelock is not blind to inbound content. It inspects content flowing into the agent's context on mediated paths.&lt;/p&gt;

&lt;p&gt;For HTTP-style fetches, the fetch proxy scans fetched response content for prompt injection before the agent consumes it. For MCP, Pipelock scans tool results and tool metadata for injection and tool-poisoning patterns. A poisoned response can become the instruction that triggers a later leak, so the inbound side matters on those paths.&lt;/p&gt;

&lt;p&gt;That is different from being a classic inbound-request WAF. Pipelock is not positioned as a service-fronting filter where arbitrary internet clients send requests into an agent-hosted web endpoint and Pipelock decides whether those requests reach the service. The core threat model is agent egress and tool traffic.&lt;/p&gt;

&lt;p&gt;If you run a public web app, you still need web controls: WAF, auth, rate limits, request validation, app-layer authorization, and abuse handling. Pipelock covers the agent side: the traffic the agent sends, the tool calls it makes, and the responses it pulls into context through mediated paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability separation and the mediated-traffic caveat
&lt;/h2&gt;

&lt;p&gt;The clean Pipelock deployment separates capabilities. The agent environment holds secrets and does useful work, but it should not have direct network egress. Pipelock has network egress, but it should not hold agent secrets. The intended path to the internet and to MCP servers runs through Pipelock, where policy is enforced.&lt;/p&gt;

&lt;p&gt;Two limits define what that buys you.&lt;/p&gt;

&lt;p&gt;First, coverage is for mediated traffic: traffic that actually routes through Pipelock. If an HTTP request, WebSocket frame, MCP call, or tool result does not pass through Pipelock, Pipelock cannot inspect or block it.&lt;/p&gt;

&lt;p&gt;Content visibility is part of this. Pipelock scans a request body only when it can see the body: the fetch endpoint, plaintext forward-proxy traffic, or a CONNECT tunnel with TLS interception turned on. On a plain CONNECT tunnel without interception, Pipelock sees the destination host but not the encrypted body, so on that path it enforces destination policy rather than body DLP. Turn on TLS interception to get body and response scanning on CONNECT traffic.&lt;/p&gt;

&lt;p&gt;Second, blocking direct egress around Pipelock is deployment-enforced, not a property of the binary. The Pipelock binary enforces policy on traffic it mediates. It does not, on its own, rewire the host network so every process is forced through it. Operators enforce that with container networking, Kubernetes NetworkPolicy, firewall rules, or Pipelock's own containment command.&lt;/p&gt;

&lt;p&gt;On Linux, &lt;code&gt;pipelock contain install&lt;/code&gt; sets that up directly. It creates a separate low-privilege agent user and installs nftables owner-match rules so the agent's only route to the network is the Pipelock proxy. Direct egress from that user is dropped at the kernel, and &lt;code&gt;pipelock contain verify&lt;/code&gt; confirms the rules are live. That turns "traffic should go through Pipelock" into "traffic can only go through Pipelock" for the contained user. It is still a deployment step you run, not something the binary does to your host by itself.&lt;/p&gt;

&lt;p&gt;That distinction is the security model. Binary-enforced controls apply on the mediated path. Direct-egress prevention depends on how the agent is run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where inbound may fit later
&lt;/h2&gt;

&lt;p&gt;There is a reasonable future shape for inbound request filtering in hosted or multi-tenant agent deployments. An organization that exposes agent endpoints as network services could add an inbound agent-specific admission layer that combines normal API security with agent-specific context checks, signed mediator headers, and tenant isolation.&lt;/p&gt;

&lt;p&gt;That would sit next to Pipelock's core model, not replace it. The main reason to deploy an agent firewall is still the egress problem: agents read untrusted content, hold useful authority, and take outbound actions. The boundary that matters most is the point where those actions leave the agent environment.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Agent Evidence Levels (AEL): grading the evidence your AI agent leaves behind</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sat, 04 Jul 2026 02:15:07 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/agent-evidence-levels-ael-grading-the-evidence-your-ai-agent-leaves-behind-56n0</link>
      <guid>https://dev.to/luckypipewrench/agent-evidence-levels-ael-grading-the-evidence-your-ai-agent-leaves-behind-56n0</guid>
      <description>&lt;p&gt;I build an agent firewall, and the question I keep hitting is not "did it block the attack." It is "how would anyone else &lt;em&gt;know&lt;/em&gt; what my agent did, without taking my word for it." Most tools answer that with "we keep tamper-proof logs" and stop. That phrase claims the strongest property that still requires trusting whoever holds the signing key. So I wrote down a way to grade the gap, as an open standard, and shipped it with a checker so nobody has to trust me about it either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AEL grades
&lt;/h2&gt;

&lt;p&gt;Agent Evidence Levels (AEL) grades a record of what an AI agent did by one question: how much of it can an outside party verify, and how much omission can they detect, without trusting the vendor or the operator? It runs AEL-0 through AEL-4, and it ships with a runnable reference checker and a conformance corpus, so a grade is something you demonstrate, not something you assert.&lt;/p&gt;

&lt;h2&gt;
  
  
  The levels
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AEL-0, authentic and ordered.&lt;/strong&gt; Records are signed and hash-linked. Modification and interior deletion are detectable. Tail truncation and outright fabrication are not, because one keyholder produced everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEL-1, gap and truncation evident.&lt;/strong&gt; A signed open, heartbeats so silence is itself signed, and a signed close committing to a count. Now a missing tail or a silent gap within a run shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEL-2, cross-domain omission evident.&lt;/strong&gt; A second recorder under a different verified signing key. For declared covered event classes, an omission on one side that the other recorded becomes detectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEL-3, externally anchored.&lt;/strong&gt; Chain heads registered in a declared external append-only log under a different verified log key, so anchored history cannot be presented in conflicting versions without detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEL-4, counterparty-confirmed.&lt;/strong&gt; For declared confirmed flows, the destination attests what it received, including "nothing." AEL confirms receipt, not harmlessness or meaning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A grade is the minimum across the required dimensions, cumulative from AEL-0. There is a reproducibility suffix, R, for when the recorded decision can be re-derived from the recorded inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What no level claims
&lt;/h2&gt;

&lt;p&gt;No level proves completeness against the party holding the signing keys. A keyholder can construct a clean history, sign every part of it, and pass every internal check. Omission-evidence is bought only with additional signed evidence, one verified keyholder at a time, and organizational independence stays declared unless it is established outside AEL. Each level states plainly the limit it does not cover. That honesty is the point of the scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions it teaches you to ask a vendor
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What AEL does your evidence earn when the reference checker runs on an artifact you hand me?&lt;/li&gt;
&lt;li&gt;If a record were silently dropped, who outside your trust domain would detect it, and how?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Come poke holes in it
&lt;/h2&gt;

&lt;p&gt;The spec, the reference checker, and the conformance corpus are public and open-source. It is authored under my company and meant to be donated to a neutral home once the vocabulary has a life of its own. I would rather find the holes now than defend them later, so if a level claims more than the checker proves, open an issue and show me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/luckyPipewrench/agent-evidence-levels" rel="noopener noreferrer"&gt;github.com/luckyPipewrench/agent-evidence-levels&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the checker on your own agent's evidence, or on a vendor's, and read the grade for yourself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>standards</category>
    </item>
    <item>
      <title>Per-Pod NetworkPolicy in Practice: Migrating Five Agents in a Day</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Thu, 21 May 2026 00:02:04 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/per-pod-networkpolicy-in-practice-migrating-five-agents-in-a-day-35hh</link>
      <guid>https://dev.to/luckypipewrench/per-pod-networkpolicy-in-practice-migrating-five-agents-in-a-day-35hh</guid>
      <description>&lt;p&gt;A working cluster ran five AI agents, each with an in-pod Pipelock sidecar scanning their traffic. The boundary was real but advisory: every container in a pod shares a network namespace, so a NetworkPolicy that says "no internet for this pod" applies to the sidecar too. Tightening the agent's egress required loosening the sidecar's egress, which defeats the point.&lt;/p&gt;

&lt;p&gt;This post is a field report on migrating those agents from in-pod sidecars to a separate-pod model where the firewall lives in its own pod with its own NetworkPolicy, and the agent pod has no route to anywhere except the firewall. The migration took about a working day and surfaced six gotchas worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structural reason for separation
&lt;/h2&gt;

&lt;p&gt;NetworkPolicy is per-pod. The Kubernetes networking spec does not allow a NetworkPolicy to scope to a specific container in a pod, because the policy is enforced by the CNI plugin which sees pods as the unit of identity. Three containers in one pod are one Kubernetes "you" from the CNI's perspective.&lt;/p&gt;

&lt;p&gt;This means an in-pod sidecar architecture has a contradiction baked in: the agent container should have no internet, and the sidecar container needs internet because the sidecar is the agent's exit. Both are in the same pod. NetworkPolicy applies to both equally. You either let the whole pod out or block the whole pod, including the sidecar.&lt;/p&gt;

&lt;p&gt;In practice, this gets papered over with &lt;code&gt;HTTPS_PROXY&lt;/code&gt; configuration on the agent container and a wide-open NetworkPolicy on the pod. The sidecar can scan everything the agent sends through the proxy. The agent can clear &lt;code&gt;HTTPS_PROXY&lt;/code&gt; in a subprocess and dial direct, and the kernel will let it through because the pod's NetworkPolicy says yes to internet.&lt;/p&gt;

&lt;p&gt;The fix is structural. Move the firewall to its own pod. Tighten the agent pod's NetworkPolicy to allow egress only to the firewall pod's service. Now the agent pod has no internet route except through the firewall, the firewall pod has the internet it needs, and NetworkPolicy enforces both shapes correctly. This is the per-pod model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five agents and the migration sequence
&lt;/h2&gt;

&lt;p&gt;The fleet had five distinct agent deployments. Each had been running with an in-pod sidecar for months. The migration sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One agent first, fully end-to-end, including the bypass-closure verification probes from the &lt;a href="https://pipelab.org/blog/three-uid-agent-containment-linux/" rel="noopener noreferrer"&gt;three-UID containment&lt;/a&gt; playbook adapted to Kubernetes.&lt;/li&gt;
&lt;li&gt;Three agents in parallel, applying the same pattern.&lt;/li&gt;
&lt;li&gt;The fifth agent last, because it had a workload-specific quirk (anti-bot scraping, see below) that needed its own treatment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per agent, the steps were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a &lt;code&gt;pipelock-companion&lt;/code&gt; Deployment + Service in the same namespace.&lt;/li&gt;
&lt;li&gt;Generate a scoped &lt;code&gt;pipelock-companion-egress&lt;/code&gt; NetworkPolicy permitting TCP 443/80 only for that pod selector.&lt;/li&gt;
&lt;li&gt;Update the agent Deployment to drop the in-pod Pipelock sidecar.&lt;/li&gt;
&lt;li&gt;Update the agent's environment to point &lt;code&gt;HTTPS_PROXY&lt;/code&gt; at the companion service.&lt;/li&gt;
&lt;li&gt;Tighten the baseline NetworkPolicy on the agent pod to remove the wide internet-egress rules and keep only the egress to the companion service.&lt;/li&gt;
&lt;li&gt;Ship the change through the normal manifest pipeline.&lt;/li&gt;
&lt;li&gt;Run the bypass-closure probes: raw TCP dial, env-clear subprocess, NO_PROXY domain match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first agent took four hours including two false starts on init container egress. The fourth agent took less than an hour. The pattern of "first one is a slog, the rest fall in line" held.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: subPath ConfigMap mounts don't hot-reload
&lt;/h2&gt;

&lt;p&gt;The single biggest unexpected friction during the migration. Pipelock has fsnotify hot-reload on its config file. Kubernetes ConfigMap updates are supposed to propagate to mounted files. They do, for directory mounts. They do not, for &lt;code&gt;subPath:&lt;/code&gt; single-file mounts. The mount path stays pointing at an immutable file-handle from pod creation.&lt;/p&gt;

&lt;p&gt;A handful of namespaces had their Pipelock config mounted with &lt;code&gt;subPath:&lt;/code&gt;. Multiple commits added redaction allowlist entries that landed in the cluster ConfigMap object but never reached the running Pipelock instance. The fix was a &lt;code&gt;kubectl delete pod&lt;/code&gt; to force a fresh mount.&lt;/p&gt;

&lt;p&gt;The structural fix is to mount the ConfigMap as a directory whenever possible. The detailed writeup is in &lt;a href="https://pipelab.org/blog/subpath-configmap-no-hot-reload/" rel="noopener noreferrer"&gt;subPath ConfigMap Mounts Don't Hot-Reload&lt;/a&gt;. For this migration, the workaround was "always restart the pod after changing Pipelock config" until the mount shape gets fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: init containers that fetch from the internet
&lt;/h2&gt;

&lt;p&gt;Several agent deployments had init containers that fetched the Pipelock binary before the main container started. The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;initContainers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install-pipelock&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;wget -O /opt/bin/pipelock https://github.com/luckyPipewrench/pipelock/releases/...&lt;/span&gt;
        &lt;span class="s"&gt;chmod +x /opt/bin/pipelock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked fine when the pod's NetworkPolicy allowed wide-open egress. After the lockdown, the init container's wget was the first thing to fail, because the pod's NetworkPolicy now denied direct internet.&lt;/p&gt;

&lt;p&gt;The fix was to make the init container copy the binary from the same image used by the companion pod. Zero network needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;initContainers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install-pipelock&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock:VERSION&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/pipelock&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/opt/bin/pipelock&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chmod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+x&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/opt/bin/pipelock"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image is the same image the companion pod runs, so the binary is byte-for-byte identical to the running companion. That becomes the canonical pattern across agent pods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 3: browser automation that can't tolerate TLS interception
&lt;/h2&gt;

&lt;p&gt;One of the agents had a browser-driver container for workloads that fail under MITM. The browser stack did its own TLS handling and broke under interception. Pipelock could be configured with &lt;code&gt;passthrough_domains&lt;/code&gt; to skip MITM for those targets, but the browser's egress also needed direct internet, not loopback to a proxy.&lt;/p&gt;

&lt;p&gt;The structural fix mirrored the firewall split: the scraping tool moved into its own Deployment, with its own egress NetworkPolicy permitting TCP 443/80 directly. The agent's NetworkPolicy got an additional allow rule for traffic to the scraping service. The agent calls the scraper through the cluster service, the scraper reaches the internet directly for its scraping work, and Pipelock no longer sits in the path for that traffic.&lt;/p&gt;

&lt;p&gt;This is an architectural compromise the per-pod model accepts: tools that fundamentally cannot work behind MITM get their own pod with their own egress. Pipelock loses visibility into the scraping traffic, but the URLs the agent passes to the scraper go through Pipelock's MCP-stdio scanner, so the calling-side surface is still covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 4: identity binding with shared namespaces
&lt;/h2&gt;

&lt;p&gt;Two of the agents lived in the same namespace and shared PVCs and ConfigMaps but bound different identities to their Pipelock companion. A single companion deployment cannot bind two identities; the companion config has one &lt;code&gt;default_agent_identity&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;Two options: deploy two companions, one per agent, or use the Pro &lt;code&gt;agents.*.source_cidrs&lt;/code&gt; feature to match against pod IPs.&lt;/p&gt;

&lt;p&gt;The migration chose two companions. The cost is roughly one extra pod's worth of memory (~50 MiB per companion). The benefit is that every agent identity maps to one companion deployment, which keeps the namespace's manifest set fleet-consistent. The Pro source_cidrs feature would be more elegant but is more complex to dogfood-validate and has not been the test path.&lt;/p&gt;

&lt;p&gt;For a single-tenant namespace, one companion is enough. For a shared namespace where each tenant binds a distinct identity, the per-tenant companion is cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 5: projected secret volume modes
&lt;/h2&gt;

&lt;p&gt;The Pipelock companion runs as an unprivileged UID. The TLS-MITM CA is delivered via a Kubernetes Secret mounted into the companion pod. The first version of the manifest set the volume mode to &lt;code&gt;0444&lt;/code&gt; (world-readable), thinking that would let the companion read the file regardless of UID.&lt;/p&gt;

&lt;p&gt;Pipelock refused to start. Its validator (in &lt;code&gt;internal/config/validate.go&lt;/code&gt;) rejects CA key files whose mode permits world-read, any-write, or any-execute, by masking against &lt;code&gt;0o137&lt;/code&gt;. That allows &lt;code&gt;0o600&lt;/code&gt; (owner-read) and &lt;code&gt;0o640&lt;/code&gt; (owner-read plus group-read), and rejects &lt;code&gt;0o444&lt;/code&gt; and &lt;code&gt;0o644&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two patterns work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;0o600&lt;/code&gt; plus matching owner.&lt;/strong&gt; Set the pod's &lt;code&gt;runAsUser&lt;/code&gt; (or &lt;code&gt;runAsNonRoot&lt;/code&gt; plus an explicit &lt;code&gt;runAsUser&lt;/code&gt;) to a UID that matches the file's owner. Kubernetes Secret volumes default the file owner to root unless &lt;code&gt;securityContext.runAsUser&lt;/code&gt; is set on the pod, so this path needs that field plus a matching &lt;code&gt;runAsUser&lt;/code&gt; for the Pipelock container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;0o640&lt;/code&gt; plus &lt;code&gt;fsGroup&lt;/code&gt;.&lt;/strong&gt; Set the volume's &lt;code&gt;defaultMode&lt;/code&gt; to &lt;code&gt;0o440&lt;/code&gt; or &lt;code&gt;0o640&lt;/code&gt;, and set the pod's &lt;code&gt;securityContext.fsGroup&lt;/code&gt;. Kubernetes chowns the secret files to the fsGroup and adds the fsGroup to the container's supplementary groups. The container reads the file through the group bits, no matter what UID it runs as.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The companion deployment uses the &lt;code&gt;fsGroup&lt;/code&gt; pattern because it composes cleanly with &lt;code&gt;runAsNonRoot: true&lt;/code&gt; and arbitrary container UIDs. The narrow trap is &lt;code&gt;0o400&lt;/code&gt; plus &lt;code&gt;fsGroup&lt;/code&gt;: the file mode has no group-read bit, so the supplementary group does not grant access — the only reader would be the file owner, and Secret volumes do not put the container UID there by default.&lt;/p&gt;

&lt;p&gt;This pattern is consistent across Pipelock companion deployments now, but the first time it surfaces is a confusing thirty minutes of debugging "why does my secret mount fail to be readable by the only container that should read it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 6: VPN sidecar flake during companion bring-up
&lt;/h2&gt;

&lt;p&gt;One namespace's existing in-pod sidecar setup had been running a VPN sidecar for months without incident. The first time the new companion pod came up with a fresh VPN sidecar, the tunnel cycled for several minutes before stabilizing. Same image, same provider, same config. The difference was server selection, evidently a flaky one.&lt;/p&gt;

&lt;p&gt;The lesson: do not put VPN on the companion pod when the agent does not strictly need exit-IP rotation. The agent firewall's job is content scanning. The cluster's normal egress is fine for that. The companion pods do not need a VPN.&lt;/p&gt;

&lt;p&gt;The legacy VPN stays for one specific deployment that has other sidecars depending on it. New companion pods are VPN-free. This shaved another moving part out of the operational footprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the day produced
&lt;/h2&gt;

&lt;p&gt;End of day, the cluster had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Five companion pods running, one per agent identity.&lt;/li&gt;
&lt;li&gt;Five baseline NetworkPolicies tightened to remove direct internet-egress rules.&lt;/li&gt;
&lt;li&gt;Five agent deployments with the in-pod Pipelock sidecar removed.&lt;/li&gt;
&lt;li&gt;Two scraping deployments split out so the agent's NetworkPolicy could stay tight.&lt;/li&gt;
&lt;li&gt;A small set of manifest commits, each with a clear rollback path.&lt;/li&gt;
&lt;li&gt;A bypass-closure probe set verified on the first agent, partially run on the other four (full sweep is a v2.4.x followup).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The structural model is now consistent across the fleet: agent pod has no internet, companion pod has internet, NetworkPolicy enforces the separation, and the agent's runtime choices do not reach the kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently next time
&lt;/h2&gt;

&lt;p&gt;Three changes for the next migration like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit ConfigMap mount shapes before starting.&lt;/strong&gt; Every &lt;code&gt;subPath:&lt;/code&gt; on a hot-reload-bearing config file is a future "why is the change not propagating" debugging session. Switch them to directory mounts up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-build the migration commits per agent.&lt;/strong&gt; The pattern of "drop sidecars, add companion, tighten NetworkPolicy" is the same across all agents. The first agent is bespoke; the rest can be templated. A small Kustomize generator would have saved time on agents 2-5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the full bypass-closure probe set on every agent.&lt;/strong&gt; Doing it on agent 1 and skipping it on agents 2-5 because "the pattern is the same" is the kind of confidence that bites you. The probes catch one-off regressions that the boilerplate cannot. v2.4.x followup is to make the probe set a CI-runnable artifact so every namespace has one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The non-glamorous lessons compound. Most of the friction in this migration was operational, not architectural. The architectural model (per-pod separation) is correct and stable. The operational details (mount shapes, init container egress, secret modes, identity binding) are what consumed the day.&lt;/p&gt;

&lt;p&gt;If your fleet runs in-pod agent firewall sidecars, the per-pod migration is worth the day. The structural change closes a class of bypass that no amount of &lt;code&gt;HTTPS_PROXY&lt;/code&gt; tightening can close.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mediator Receipts: The Question to Ask About Agent Attestation</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Wed, 13 May 2026 23:28:07 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/mediator-receipts-the-question-to-ask-about-agent-attestation-24b1</link>
      <guid>https://dev.to/luckypipewrench/mediator-receipts-the-question-to-ask-about-agent-attestation-24b1</guid>
      <description>&lt;p&gt;If your AI agent signs its own decision receipts, the agent is its own witness. That matters when an auditor, regulator, or customer security team asks "who signed this." The cryptography is fine. The chain holds. The question is who held the pen.&lt;/p&gt;

&lt;p&gt;I'm not picking on any vendor. As more agent runtimes ship signed-receipt formats, the architecture question lives in the same place every time: where does the signing key sit, and what's its trust relationship to the agent process?&lt;/p&gt;

&lt;h2&gt;
  
  
  The two shapes
&lt;/h2&gt;

&lt;p&gt;A signed receipt has three parts: payload, signature, signer. Payload says what happened. Signature proves the payload wasn't altered after signing. Signer is the entity holding the key.&lt;/p&gt;

&lt;p&gt;Most formats pin down payload and signature up front. The thing that varies across formats is the signer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shape one: signer is the actor.&lt;/strong&gt; The agent runtime holds the key. The agent decides, generates a receipt, signs with its own key, ships to the log. Chain of custody is one entity wide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shape two: signer is not the actor.&lt;/strong&gt; A separate process holds the key. The agent makes a request, the request flows through that separate process, the separate process makes the policy decision and signs the receipt. The agent never sees the key. Chain of custody crosses a trust boundary.&lt;/p&gt;

&lt;p&gt;Both shapes produce signed receipts. Only one of them survives a question about the signer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "outside the trust boundary" buys you
&lt;/h2&gt;

&lt;p&gt;Trust boundary is the line around a set of components that share fate. If A is compromised, everything inside A's boundary is potentially compromised. A signature proves bytes came from the key holder. A signature doesn't prove the key holder is trustworthy.&lt;/p&gt;

&lt;p&gt;If the agent holds the key, the boundary around the signature is the same boundary around the agent. A prompt injection that gets the agent to send a bad request is in the same position to influence what gets signed. The signature proves the receipt came from the agent. It says nothing about whether the receipt is honest.&lt;/p&gt;

&lt;p&gt;Move the signer outside that boundary and the math changes. A separate process with its own key in its own memory writes the signature based on what it observed of the agent's traffic. The agent can't forge or rewrite mediator-signed receipts for traffic that passes through the mediator, because the agent never holds the signing key. A successful prompt injection still produces a bad request. The receipt records the mediator's verdict on that request, because the recorder is somebody else.&lt;/p&gt;

&lt;p&gt;The same separation that lets a proxy enforce policy on a compromised agent lets the proxy attest to actions on a compromised agent. The attestation is honest about a dishonest action. That's the property an auditor wants.&lt;/p&gt;

&lt;p&gt;That's why receipt architecture sits next to enforcement architecture in the three-UID containment pattern. Once the agent becomes the signer, the receipt is self-attestation. Move the signer behind the same boundary as the proxy and the receipt class flips.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't a feature comparison
&lt;/h2&gt;

&lt;p&gt;A vendor whose receipts are signed in-process isn't lying. The cryptography is sound. The chain is verifiable. The thing those receipts can't do is survive a question about who signed them.&lt;/p&gt;

&lt;p&gt;An auditor asks: "How do you prove the agent didn't produce a fraudulent receipt that says 'access denied' while the actual access was granted?" In-process answer: "We trust the agent runtime." Out-of-process answer: "The receipt is signed by a process the agent has no key to. The agent couldn't have signed that fraudulent receipt with the mediator key."&lt;/p&gt;

&lt;p&gt;The first answer is a statement of trust. The second is a structural property. For internal use, the first is plenty. For regulated industries, audit-bearing logs, or any case where the agent itself might be under scrutiny, the second one holds up.&lt;/p&gt;

&lt;p&gt;Same distinction shows up in financial audit (self-prepared statements vs auditor-signed reports) and in legal evidence (defendant's own records vs witness testimony). Records can be true either way. Evidentiary weight differs because chain of custody differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two pictures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In-process signer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent process
├── Decision logic
├── Signing key
└── Signature output → Receipt log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent decides, holds the key, signs. Compromise of the agent process compromises the signing path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out-of-process signer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent process              Mediator process
├── Decision logic     →    ├── Observes traffic
└── Action initiation       ├── Applies policy
                            ├── Signing key
                            └── Signature output → Receipt log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent initiates. Mediator observes, decides, signs. Compromise of the agent doesn't by itself compromise the signing path because the key sits in a different process.&lt;/p&gt;

&lt;p&gt;Out-of-process is harder to deploy. Traffic has to flow through the mediator (which is what an agent firewall does). The mediator has to live in a different trust boundary from the agent (Linux UID separation, separate Kubernetes pod, separate VM). The mediator needs its own threat model, key management, and operational story.&lt;/p&gt;

&lt;p&gt;The cost is real. The evidentiary weight is also real. Whether it's worth it depends on what you need the receipts for. Internal debugging log, in-process is fine. Audit evidence, capability separation is what makes the receipt worth more than its bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question to ask any vendor
&lt;/h2&gt;

&lt;p&gt;Evaluating an agent security or governance product that ships signed receipts boils down to one short question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is the signing key held, and what's the trust relationship between that location and the agent process?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three possible answers map to three shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The agent process holds the key." In-process signer. Class: self-attestation. Useful for internal logs, debugging, observability.&lt;/li&gt;
&lt;li&gt;"The runtime environment holds the key, separate from the agent process but inside the same operator's trust boundary." Operator-signer. Class: deployment-internal attestation. Stronger than self-attestation, weaker than independent.&lt;/li&gt;
&lt;li&gt;"An entity outside the operator's deployment, with its own threat model and key management, holds the key." Independent attestor. Class: third-party witness. Strongest weight, hardest to deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pipelock implements the second shape today when deployed correctly. The binary is a separate process from the agent and signs receipts with its own key. The deployment puts that process behind a capability boundary the agent can't cross: Linux UID separation, a separate Kubernetes pod with NetworkPolicy, or equivalent isolation. The boundary is deployment-enforced, not binary-enforced. A deployment that runs Pipelock as the same UID as the agent collapses shape two back into shape one. Independent third-party attestation (a hosted verifier outside the operator's deployment) is on my roadmap, not shipped, and there are open questions about who the third party is and what trust relationship the operator has with them.&lt;/p&gt;

&lt;p&gt;Pipelock doesn't require in-agent signing. It earns the second class when deployed behind a capability boundary the agent can't cross. The signing key lives in a process the agent has no access to. That's the property the architecture protects, and it depends on how you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for posture
&lt;/h2&gt;

&lt;p&gt;Three decisions follow if signed receipts are part of your AI agent posture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust scope.&lt;/strong&gt; Only the team running the agent? In-process is plenty. Auditors, customers, or regulators in the picture? Receipts have to survive the "who signed" question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threat model.&lt;/strong&gt; A compromised agent (prompt injection, tool poisoning, jailbreak) producing fraudulent self-signed receipts is something in-process can't address. Out-of-process can, because the agent never holds the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capability separation budget.&lt;/strong&gt; Out-of-process signing means the signer lives in a different trust boundary from the agent. Linux: separate UID. Kubernetes: separate pod. Managed runtime: maybe a different service entirely. Each adds operational footprint. That footprint is the cost of the evidentiary property.&lt;/p&gt;

&lt;p&gt;Decisions cascade. Internal use only gets you in-process. Audit-bearing evidence needs out-of-process. Third-party verifiable needs an attestor outside your deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've shipped
&lt;/h2&gt;

&lt;p&gt;The reason this question is worth asking right now is that the open-source pieces for verifying mediator-signed receipts exist today. Anyone can pull them and check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit Packet v0 JSON Schema&lt;/strong&gt; lives at &lt;a href="https://pipelab.org/schemas/audit-packet-v0.schema.json" rel="noopener noreferrer"&gt;pipelab.org/schemas/audit-packet-v0.schema.json&lt;/a&gt;. It pins the shape of a procurement-grade evidence bundle: receipt chain, verifier output, scanner config snapshot, posture metadata, plus tamper-detection cross-checks (claimed totals vs actual chain totals, claimed root hash vs actual, claimed final sequence vs actual).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three verifier codebases in the Pipelock repo, none dependent on the others.&lt;/strong&gt; &lt;a href="https://github.com/luckyPipewrench/pipelock/tree/main/internal/receipt" rel="noopener noreferrer"&gt;Go&lt;/a&gt; is the reference implementation. Both the built-in &lt;code&gt;pipelock verify-receipt&lt;/code&gt; subcommand and the standalone &lt;a href="https://github.com/luckyPipewrench/pipelock/tree/main/cmd/pipelock-verifier" rel="noopener noreferrer"&gt;&lt;code&gt;pipelock-verifier&lt;/code&gt;&lt;/a&gt; binary share it. &lt;a href="https://github.com/luckyPipewrench/pipelock/tree/main/sdk/verifiers/ts" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt; uses &lt;code&gt;@noble/ed25519&lt;/code&gt; and &lt;code&gt;ajv&lt;/code&gt;. &lt;a href="https://github.com/luckyPipewrench/pipelock/tree/main/sdk/verifiers/rust" rel="noopener noreferrer"&gt;Rust&lt;/a&gt; carries its own canonical JSON, schema validation, Ed25519, chain replay, and packet cross-checks. All three cross-validate against the same fixtures at &lt;a href="https://github.com/luckyPipewrench/pipelock/tree/main/sdk/conformance/testdata" rel="noopener noreferrer"&gt;sdk/conformance/testdata/&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python verifier&lt;/strong&gt; ships in a separate repo: &lt;a href="https://github.com/luckyPipewrench/pipelock-verify-python" rel="noopener noreferrer"&gt;pipelock-verify-python&lt;/a&gt;. 0.1.x on PyPI covers ActionReceipt v1. 0.2.0 covering EvidenceReceipt v2 plus RFC 8785 JCS and an RFC 9421 well-known signing-key directory is prepared in the repo, PyPI publish pending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standalone &lt;code&gt;pipelock-verifier&lt;/code&gt;&lt;/strong&gt; is self-contained. No network surface, no proxy, no scanner. CI runners and auditors can drop it into an isolated environment with &lt;code&gt;--offline&lt;/code&gt; and verify a packet against the public schema, zero vendor dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the point of putting all of it on the public surface. An attestation that depends on the agent's trustworthiness is a different thing from an attestation that survives a question about the agent. The verifier path is open source so anyone, including people who have no trust relationship with me at all, can check whether a receipt is honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;Signed receipts are a real defensive property. The signing architecture decides what class of evidence the receipts are.&lt;/p&gt;

&lt;p&gt;In-process signers produce self-attestations. Receipts are signed, the chain holds, the cryptography is sound. The trust model is "the agent is honest about its own decisions."&lt;/p&gt;

&lt;p&gt;Out-of-process signers produce attestations from a different actor. The trust model is "the proxy is honest about what the agent did." A proxy can be honest about a dishonest agent, which is what makes the receipts evidence and not self-report.&lt;/p&gt;

&lt;p&gt;Both classes have a role. Knowing which class your vendor ships is the input to your posture decision. The architectural question is upstream of features, performance, and price. It decides what the receipts are for. Worth asking before signing anything, including the contract.&lt;/p&gt;

&lt;p&gt;If you're evaluating Pipelock or anything else against this rubric, the conversation starts at "where is the signing key held." The answer should be one sentence, and it should be the same sentence regardless of who you ask on the vendor's team.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>compliance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Three Things "Set HTTPS_PROXY" Cannot Stop</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Wed, 13 May 2026 12:44:55 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/three-things-set-httpsproxy-cannot-stop-1bge</link>
      <guid>https://dev.to/luckypipewrench/three-things-set-httpsproxy-cannot-stop-1bge</guid>
      <description>&lt;p&gt;Three bypass shapes for HTTPS_PROXY-only agent egress controls. The kernel does not enforce any of them. Each is reachable in a default Linux process unless additional kernel-level controls are applied.&lt;/p&gt;

&lt;p&gt;This post is the listicle companion to &lt;a href="https://pipelab.org/blog/politeness-vs-enforcement-https-proxy/" rel="noopener noreferrer"&gt;Politeness vs Enforcement&lt;/a&gt;. The framing post is the long argument; this one is the short list of specific bypasses to know about and the kernel-level rule that would close each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Subprocess that clears the environment
&lt;/h2&gt;

&lt;p&gt;An agent process spawns a subprocess. If the spawn does not pass &lt;code&gt;HTTPS_PROXY&lt;/code&gt; through, the subprocess has a different environment than its parent. Cooperative HTTP libraries in the subprocess never see the variable, so they never route through the proxy.&lt;/p&gt;

&lt;p&gt;The bypass takes two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty &lt;code&gt;env={}&lt;/code&gt; clears every variable, including the proxy hint. &lt;code&gt;curl&lt;/code&gt; runs, dials directly, the kernel sees an outbound connection from the agent UID, the connection succeeds.&lt;/p&gt;

&lt;p&gt;If the agent is running as the same UID as the operator and the proxy, the kernel has no rule that distinguishes "should go through proxy" from "should not." Every outbound TCP from the agent UID is allowed. The proxy does not see the request, the audit log does not record it, the dashboards do not flag it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel-level fix:&lt;/strong&gt; put the agent process on a UID that is denied direct internet by an nftables rule. Loopback to the proxy is allowed; everything else is dropped at the kernel level. The subprocess inherits the agent UID and inherits the same restriction. Clearing the environment changes the agent's behavior, not the kernel's.&lt;/p&gt;

&lt;p&gt;The same fix in Kubernetes: separate the agent into its own pod with a NetworkPolicy that only allows egress to the proxy pod's service. The agent container can spawn subprocesses, the subprocesses share the agent pod's network namespace, the NetworkPolicy applies to all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Transport that does not consult HTTPS_PROXY
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;HTTPS_PROXY&lt;/code&gt; is documented to affect HTTP and HTTPS client libraries. Other transports do not consult it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw TCP sockets.&lt;/strong&gt; A direct &lt;code&gt;socket.connect((host, port))&lt;/code&gt; in any language is unaffected by the variable. Agents that use lower-level networking primitives (gRPC over a custom transport, custom protocol clients, anything calling &lt;code&gt;socket()&lt;/code&gt; directly) skip the proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP.&lt;/strong&gt; DNS queries, NTP, syslog, custom datagram protocols. The variable has nothing to say about UDP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QUIC and HTTP/3.&lt;/strong&gt; QUIC runs over UDP. Most QUIC client libraries do not consult HTTP_PROXY-style variables, even if the underlying request is HTTP/3-shaped. Browsers under modern Chromium can fall back to HTTP/3 for performance reasons; agent libraries that include QUIC support might prefer it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICMP.&lt;/strong&gt; Raw ICMP from a process with the right capabilities. Less common in agent code, but listed for completeness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A coding agent that wants to ping a host runs &lt;code&gt;ping&lt;/code&gt;. The kernel sends ICMP. The proxy is not in the path. A coding agent that uses a gRPC client to talk to a service over HTTP/2 might or might not respect proxy variables depending on the library. A coding agent that constructs a DNS query directly to encode data into the hostname (a known DNS-exfiltration pattern) is exfiltrating bytes via a transport the proxy does not see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel-level fix:&lt;/strong&gt; the same nftables rule that drops outbound from the agent UID drops every transport, not just TCP. UDP, ICMP, raw sockets, all get the same drop. The rule is &lt;code&gt;meta skuid &amp;lt;agent_uid&amp;gt; drop&lt;/code&gt;, with allow-list exceptions only for loopback and DNS to a local resolver. Anything not on the allow list is gone.&lt;/p&gt;

&lt;p&gt;The Kubernetes equivalent is a NetworkPolicy on the agent pod. NetworkPolicy is per-protocol and per-port: the egress section can specify TCP and UDP individually. A correctly tightened policy allows TCP to the proxy service on the proxy port and nothing else. UDP does not need to be allowed for an agent that does not need to send UDP.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Service whose hostname matches NO_PROXY
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NO_PROXY&lt;/code&gt; is the variable that tells cooperative HTTP clients to skip the proxy for matching destinations. Operators set it to keep cluster-internal traffic from hairpinning through the proxy. Common patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NO_PROXY=127.0.0.1,localhost,10.0.0.0/8,*.cluster.local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NO_PROXY=*.internal.example.com,internal-api&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bypass surface is whatever the operator has put on the list. If a NO_PROXY-listed destination has its own outbound, the agent has just reached the internet through it.&lt;/p&gt;

&lt;p&gt;A specific shape: a Kubernetes cluster has an internal LLM gateway service at &lt;code&gt;llm-gateway.cluster.local&lt;/code&gt;. The agent's NO_PROXY includes &lt;code&gt;*.cluster.local&lt;/code&gt;. The agent makes an HTTP call to &lt;code&gt;https://llm-gateway.cluster.local/chat/completions&lt;/code&gt;. The proxy never sees the call. The LLM gateway receives the call, forwards it to a third-party LLM API, and returns the response. From the agent's perspective, this is a successful API call. From Pipelock's perspective, no scanning happened.&lt;/p&gt;

&lt;p&gt;The same shape applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An internal MCP server with its own outbound, where the agent calls the MCP server directly.&lt;/li&gt;
&lt;li&gt;An internal proxy or gateway, where the agent uses the gateway to reach external services.&lt;/li&gt;
&lt;li&gt;An internal logging or metrics service, where the agent can encode data into log lines that get shipped to an external aggregator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any service in the NO_PROXY list with its own outbound is a path through the proxy boundary that the proxy cannot see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel-level fix:&lt;/strong&gt; route every cluster-internal service through the agent firewall too. The cost is a hop of latency on intra-cluster calls. The benefit is that the agent firewall is the only egress path for the agent process, regardless of whether the destination is internal or external. NO_PROXY on the agent is reduced to &lt;code&gt;127.0.0.1,localhost&lt;/code&gt;. Everything else, including cluster services, goes through the proxy.&lt;/p&gt;

&lt;p&gt;This requires the cluster's internal services to accept proxied requests, which most do. It also requires the proxy to be configured to scan internal traffic the same way it scans external traffic. The architectural pattern is "the agent firewall is the agent's only door, and the door applies the same scanning regardless of which floor you are going to."&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;All three bypasses share a shape: the agent firewall lives in the application layer, and the agent process can route around the application layer in ways the application layer does not see. The kernel sees them all but has no rule to apply.&lt;/p&gt;

&lt;p&gt;The kernel-level fixes for all three are versions of the same answer: put the agent process on an identity (UID or pod) that the kernel denies direct egress to anywhere except the proxy. The kernel does not care what the agent intended, what variable was set, what transport was chosen, or what hostname was the destination. The kernel sees the source identity, checks the rule, and drops or accepts.&lt;/p&gt;

&lt;p&gt;That is what enforcement looks like. The application-layer hint, however well-intentioned, is policy. The kernel rule is the control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-bypass severity
&lt;/h2&gt;

&lt;p&gt;Not every bypass is equally dangerous in every deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The subprocess env-clear bypass requires the agent to deliberately spawn a subprocess with a clean environment. Most coding-agent workflows do not do this by default, but a prompt-injection that says "run this command in a fresh shell" can produce it.&lt;/li&gt;
&lt;li&gt;The non-HTTP-transport bypass is rare in practice. Most agent workloads are HTTP-shaped. DNS exfiltration is the realistic version of this class for agents that handle high-value secrets.&lt;/li&gt;
&lt;li&gt;The NO_PROXY bypass is common in Kubernetes deployments. Internal cluster services are often wired into NO_PROXY, and any of them with outbound access can expand the agent's effective reach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A defense posture worth shipping does not pick one of these to address. All three are reachable in a default Linux configuration. The kernel-level fix that addresses them is one fix, not three: deny direct egress from the agent identity at the kernel layer, allow only loopback to the proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with this list
&lt;/h2&gt;

&lt;p&gt;If you operate AI agents and your egress story is &lt;code&gt;HTTPS_PROXY&lt;/code&gt; plus a hopeful &lt;code&gt;NO_PROXY&lt;/code&gt;, audit each of the three bypasses on your own environment. The audit takes minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Subprocess env-clear&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;agent-uid&amp;gt; &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; curl https://example.com/

&lt;span class="c"&gt;# 2. Non-HTTP transport&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;agent-uid&amp;gt; nc &lt;span class="nt"&gt;-z&lt;/span&gt; 1.1.1.1 53

&lt;span class="c"&gt;# 3. NO_PROXY service&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;agent-uid&amp;gt; curl http://&amp;lt;no-proxy-service&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each command that returns success when you expected blocking is a bypass to fix. The fix shape is described above, in &lt;a href="https://pipelab.org/blog/politeness-vs-enforcement-https-proxy/" rel="noopener noreferrer"&gt;Politeness vs Enforcement&lt;/a&gt;, and in the &lt;a href="https://pipelab.org/blog/three-uid-agent-containment-linux/" rel="noopener noreferrer"&gt;three-UID containment pattern&lt;/a&gt; for workstations and per-pod NetworkPolicy for clusters.&lt;/p&gt;

&lt;p&gt;The agents are going to find these bypasses on their own. Better to find them first.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>linux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Capture and Replay: Testing Security Policy Without Production Risk</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Tue, 12 May 2026 14:50:44 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/capture-and-replay-testing-security-policy-without-production-risk-2f8i</link>
      <guid>https://dev.to/luckypipewrench/capture-and-replay-testing-security-policy-without-production-risk-2f8i</guid>
      <description>&lt;p&gt;You cannot change a security policy in production without breaking somebody's workflow somewhere. Every allowlist update, every new DLP pattern, every tightened SSRF rule disagrees with at least one request that worked yesterday. The cost of finding the disagreements after promotion is the cost of a rollback under pressure: the agent fleet is paging, the dashboard is red, and the operator is editing YAML at 2 AM.&lt;/p&gt;

&lt;p&gt;Capture and replay shifts the disagreements left. The proxy records what it saw and what it decided. A candidate policy gets replayed against the captured journal. The diff between live and candidate verdicts becomes a report. The operator reviews the report before promotion, not after. By the time the new policy goes live, the only surprises are the ones the operator already accepted.&lt;/p&gt;

&lt;p&gt;The same deployment lesson appears in &lt;a href="https://pipelab.org/blog/subpath-configmap-no-hot-reload/" rel="noopener noreferrer"&gt;subPath ConfigMap Mounts Don't Hot-Reload&lt;/a&gt;: changing a policy object is not enough. You need proof that the running enforcement path will see and apply the change.&lt;/p&gt;

&lt;p&gt;Pipelock's learn-and-lock pipeline is this pattern, with signed receipts on the lifecycle steps that change contract state. This post is the architecture, the design choices behind the cardinality cap and fidelity gates, and the lifecycle commands an operator runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four phases
&lt;/h2&gt;

&lt;p&gt;Learn-and-lock has four phases. Activation is a two-step lifecycle inside the fourth. Each produces evidence the next step depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observe.&lt;/strong&gt; The proxy records URL verdicts, response verdicts, DLP verdicts, MCP tool-policy verdicts, and tool-scan verdicts to a JSONL journal. Each record carries the input summary that produced the verdict, the verdict itself, and a reference to the session and trace context. Encrypted payload sidecars can hold exact payloads when raw escrow is configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile.&lt;/strong&gt; The compile phase takes the journal and produces a candidate contract: a normalized description of the URL paths, MCP method shapes, and argument patterns the proxy observed. Path normalization caps cardinality so a contract built from a million &lt;code&gt;/users/{id}&lt;/code&gt; requests does not produce a million rules. Operators can pin or split paths the normalizer collapses incorrectly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow.&lt;/strong&gt; The candidate contract gets replayed against more traffic. The proxy continues to enforce the live policy; the shadow contract produces verdicts in parallel. The output is a delta receipt: a signed record of every request where the candidate disagrees with the live policy, with severity and reason for each disagreement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activate&lt;/strong&gt; (two steps). &lt;strong&gt;Ratify&lt;/strong&gt; — the operator reviews the candidate and marks rules as enforce, capture-only, or reject. Ratification emits a &lt;code&gt;contract_ratified&lt;/code&gt; evidence receipt. &lt;strong&gt;Promote&lt;/strong&gt; — the ratified contract becomes active policy. Promotion writes signed intent and committed receipts that identify the target manifest, prior manifest, operator key, and selector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sequence is observe -&amp;gt; compile -&amp;gt; review -&amp;gt; shadow -&amp;gt; ratify -&amp;gt; promote. Each step produces evidence the next step depends on. The contract that gets promoted is the one that was ratified, which is the one whose shadow report the operator reviewed, which was generated from observed traffic. There is no implicit step where a config edit slips into the active state without going through the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What capture actually records
&lt;/h2&gt;

&lt;p&gt;The capture journal is a stream of typed records, one per verdict site. The record fields cover the surface needed to replay deterministically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The transport.&lt;/strong&gt; Fetch, forward, CONNECT, WebSocket, MCP stdio, MCP HTTP, body scan. Replay needs to know which scanner to invoke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The subsurface.&lt;/strong&gt; A label for the specific hook site, like &lt;code&gt;forward_url&lt;/code&gt; or &lt;code&gt;dlp_mcp_input&lt;/code&gt;. Replay uses the subsurface to dispatch to the right scanner method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The input.&lt;/strong&gt; A replayable summary in the JSONL record, with optional encrypted sidecar payloads when raw escrow is configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verdict.&lt;/strong&gt; Allow, block, warn, or strip, with the matching pattern names and any classification details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The session and trace context.&lt;/strong&gt; Replay of stateful surfaces (rate limiting, cross-request exfiltration, MCP tool baselines) needs the order and grouping of records to match production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The record envelope reuses the existing Pipelock recorder, which gives the journal hash chaining, signing, retention, and rotation by default. The capture schema is versioned separately from the recorder envelope so the journal format can evolve without breaking older recordings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why path normalization caps cardinality
&lt;/h2&gt;

&lt;p&gt;A naive compiler would produce one rule per observed URL path. For a working agent fleet that is millions of distinct paths, most of which are the same shape with different IDs. The compiled contract would be useless: too long to review, too brittle to maintain, too slow to evaluate.&lt;/p&gt;

&lt;p&gt;Path normalization collapses paths with structural similarity. A request to &lt;code&gt;/users/123/profile&lt;/code&gt; and a request to &lt;code&gt;/users/456/profile&lt;/code&gt; collapse into &lt;code&gt;/users/{id}/profile&lt;/code&gt;. The normalizer is conservative; it only collapses when the variable component looks like an identifier (numeric, UUID-shaped, or a short token), and it caps the cardinality so a path with too many distinct values stays unnormalized. The cap exists because some paths really do have a small fixed set of values where each value is its own rule, and collapsing those would be wrong.&lt;/p&gt;

&lt;p&gt;Operators can pin a path that should not be normalized (&lt;code&gt;/admin&lt;/code&gt; is &lt;code&gt;/admin&lt;/code&gt;, not &lt;code&gt;/{role}&lt;/code&gt;) or split a path that should be more granular than the default (&lt;code&gt;/api/v1/foo&lt;/code&gt; is different from &lt;code&gt;/api/v2/foo&lt;/code&gt;, even though the normalizer might collapse them). The pin and split commands are the operator's escape hatch for cases where the heuristics get the wrong answer.&lt;/p&gt;

&lt;p&gt;The contract that comes out of the compile phase is something a human can review. Hundreds or low thousands of rules, not millions. Each rule corresponds to a request shape the proxy actually saw. The reviewer reads the contract and asks "is this the policy I want," instead of writing one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fidelity gates exist
&lt;/h2&gt;

&lt;p&gt;Replay produces verdicts against captured records. Replay is only useful if the verdicts it produces match the verdicts the live proxy would produce given the same inputs. The fidelity gates are the checks that hold the replay engine to that standard.&lt;/p&gt;

&lt;p&gt;A simple example: an SSRF check during replay needs to see the same DNS resolution as the live proxy. If replay re-resolves a hostname and gets a different IP, the SSRF verdict changes for reasons that have nothing to do with the policy under test. Fidelity gates handle this by either pinning the resolution from the captured record or skipping the SSRF check during replay and reporting the path as "stateful, not replayable."&lt;/p&gt;

&lt;p&gt;A harder example: cross-request exfiltration depends on session history. Replay has to process the journal in order so the second request in a sequence sees the first. The replay engine reads the journal as an ordered stream, not a parallel batch, to preserve the history. The fidelity gate flags any replay that deviates from order.&lt;/p&gt;

&lt;p&gt;The list of stateful surfaces is small but real: URL rate limiting, URL data budget, MCP chain detection, MCP session binding, adaptive escalation, HITL overrides. Each has its own fidelity gate. Replay against a journal where the order is wrong, the session is missing, or the surface is skipped is replay against a different policy than the live proxy ran. The gates make the difference visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow delta receipts
&lt;/h2&gt;

&lt;p&gt;The output of the shadow phase is a delta receipt, signed with the same chain as every other Pipelock receipt. The receipt has, for each request where the candidate disagreed with the live policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request shape (transport, subsurface, normalized input).&lt;/li&gt;
&lt;li&gt;The live verdict.&lt;/li&gt;
&lt;li&gt;The candidate verdict.&lt;/li&gt;
&lt;li&gt;The classification of the disagreement: false-positive, false-negative, allowed-now-blocked, blocked-now-allowed, or neutral.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipt does not contain the raw request bodies. Those stay in the encrypted payload sidecar. The receipt is a summary an operator can review at scale, with pointers into the sidecar for the cases that need closer inspection.&lt;/p&gt;

&lt;p&gt;A contract that produces zero deltas is one that exactly matches live. Useful for a baseline, less useful as a candidate for change. A contract with too many deltas is too aggressive a change for one promotion; the operator can split it into multiple smaller changes, each with its own shadow report. The right delta count depends on the change being made; the report makes the count visible so the operator can decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle commands
&lt;/h2&gt;

&lt;p&gt;Pipelock ships the lifecycle as CLI commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn observe&lt;/code&gt; runs observation and writes capture evidence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn compile&lt;/code&gt; builds a signed candidate contract.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn review&lt;/code&gt; renders deterministic review markdown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn shadow&lt;/code&gt; replays captured observations against the candidate and writes a shadow report.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn diff&lt;/code&gt; compares two shadow JSON reports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn ratify&lt;/code&gt; records operator approval choices.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn promote&lt;/code&gt; makes a ratified contract active.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn rollback&lt;/code&gt; returns to a previously accepted manifest.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn forget&lt;/code&gt; removes a rule from a candidate, signs the reduced candidate, and writes a tombstone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelock learn split&lt;/code&gt; and &lt;code&gt;pipelock learn pin&lt;/code&gt; fix over-broad path normalization before ratification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runtime evaluation hooks for active contracts run in the proxy on every request once promotion lands. The lifecycle receipts cover the promotion moment; the verifier can prove the contract running in production matches the contract that was ratified. If the artifact on disk has been modified since signing, promotion refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats blind config edits
&lt;/h2&gt;

&lt;p&gt;Three failure modes that capture and replay catches before promotion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tightened DLP that breaks a real workflow.&lt;/strong&gt; A new pattern matches a string the agent fleet was sending in legitimate requests. Without replay, promotion produces a wave of failed requests. With replay, the delta receipt shows every request the new pattern would have blocked, and the operator either keeps the pattern (blocking real traffic the operator now classifies as exfiltration) or refines the pattern (so the legitimate string stops matching).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loosened allowlist that exposes a path.&lt;/strong&gt; An operator adds a domain to the allowlist for a new integration. Replay shows the new domain catches not just the integration's traffic but a category of requests the operator did not anticipate. The allowlist gets a more specific rule before promotion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tool policy change that breaks a tool chain.&lt;/strong&gt; A tool gets added to the deny-list because the operator thinks it is unused. Replay shows the chain detector firing on a sequence the new deny-list would break. The operator either accepts the breakage (the chain was the use case being deprecated) or revises the deny-list (the chain is in active use).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In each case, the value is shifting the discovery from "production now" to "before promotion." The cost difference between those two timings is the cost of a paged-on call. Replay's purpose is to never have that call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this enables next
&lt;/h2&gt;

&lt;p&gt;Once the lifecycle is in place, the same capture journals power adjacent work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regression testing.&lt;/strong&gt; A change in scanner code can be replayed against historic journals to confirm the verdicts have not drifted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance evidence.&lt;/strong&gt; A captured journal plus a signed contract is a record of "this is what we observed and this is what we approved."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail.&lt;/strong&gt; The signed receipts cover contract lifecycle decisions, so an auditor can verify the chain from observation to promotion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those uses required new infrastructure. They fall out of the same capture-and-replay foundation, with the existing receipt chain providing the integrity story.&lt;/p&gt;

&lt;p&gt;If you are running a security policy in production today and your change-management story is "edit YAML, hot-reload, hope," capture and replay is the upgrade path. The change-management story becomes "observe, compile, review, shadow, ratify, promote, with signed receipts around lifecycle decisions." The proxy stops being a place where mistakes happen and starts being a place where mistakes are caught before they happen.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Pipelock Agent Egress Control: the missing CI primitive for AI agents</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sun, 10 May 2026 21:06:27 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/pipelock-agent-egress-control-the-missing-ci-primitive-for-ai-agents-27j4</link>
      <guid>https://dev.to/luckypipewrench/pipelock-agent-egress-control-the-missing-ci-primitive-for-ai-agents-27j4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Pipelock Agent Egress Control is a GitHub Action. It runs an agent script inside a Linux network namespace, forces supported egress through Pipelock, and writes a signed Audit Packet a security reviewer can verify offline with a pinned public key. v0.1.0 shipped 2026-05-09. Apache 2.0. &lt;a href="https://github.com/marketplace/actions/pipelock-agent-egress-control" rel="noopener noreferrer"&gt;Marketplace listing&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Agent jobs are starting to run inside pull requests, issue triage workflows, release pipelines, docs bots, and security automation. Those jobs touch source code, secrets, package registries, cloud APIs, MCP tools, and the public internet. A normal CI log can tell you what the agent said it did. An Audit Packet is meant to prove what the network boundary saw.&lt;/p&gt;

&lt;p&gt;This is the launch post for &lt;code&gt;pipelock-agent-egress-action&lt;/code&gt; v0.1.0, the first tagged release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Wraps a bash script in a Linux network namespace with iptables enforcement. The script runs as a non-root &lt;code&gt;pipelock-agent&lt;/code&gt; user with sudo denied and capabilities dropped. Pipelock listens on the loopback interface as a separate non-root &lt;code&gt;pipelock-host&lt;/code&gt; user. All HTTP, HTTPS, and WebSocket traffic from the script is routed through Pipelock at the kernel level. Direct network, DNS, and raw TCP are blocked inside the namespace.&lt;/p&gt;

&lt;p&gt;After the run, the action writes a signed Audit Packet: receipt chain, verifier output, policy hash, decision counts, scanner config snapshot, posture metadata, and a human-readable summary. The receipts are signed by Pipelock at the network boundary. The agent process never signs anything. With a pinned public key, a third party can verify the chain offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Pipelock&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl -fsSLO https://github.com/luckyPipewrench/pipelock/releases/download/v2.4.0/pipelock_2.4.0_linux_amd64.tar.gz&lt;/span&gt;
    &lt;span class="s"&gt;curl -fsSLO https://github.com/luckyPipewrench/pipelock/releases/download/v2.4.0/checksums.txt&lt;/span&gt;
    &lt;span class="s"&gt;grep 'pipelock_2.4.0_linux_amd64.tar.gz' checksums.txt | sha256sum -c -&lt;/span&gt;
    &lt;span class="s"&gt;tar -xzf pipelock_2.4.0_linux_amd64.tar.gz&lt;/span&gt;
    &lt;span class="s"&gt;sudo install -m 0755 pipelock /usr/local/bin/pipelock&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pipelock Agent Egress Control&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;luckyPipewrench/pipelock-agent-egress-action@v0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;script-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/agent.sh&lt;/span&gt;
    &lt;span class="na"&gt;pipelock-bin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/pipelock&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.pipelock/ci.yaml&lt;/span&gt;
    &lt;span class="na"&gt;audit-packet-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock-audit-packet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is in scope for v0
&lt;/h2&gt;

&lt;p&gt;On supported Linux runners (ubuntu-latest):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP and HTTPS egress through Pipelock at the kernel level.&lt;/li&gt;
&lt;li&gt;WebSocket destinations contained at the kernel level. Frame-level scanning when the script uses Pipelock's &lt;code&gt;/ws?url=...&lt;/code&gt; proxy path.&lt;/li&gt;
&lt;li&gt;Direct network, DNS, and raw TCP blocked inside the namespace.&lt;/li&gt;
&lt;li&gt;Non-root execution for both the action script and the listener, with sudo denied and capabilities dropped.&lt;/li&gt;
&lt;li&gt;Signed receipt chain verified at end-of-run; Audit Packet written locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is out of scope for v0
&lt;/h2&gt;

&lt;p&gt;Disclosed in the release notes rather than papered over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nested Docker workloads launched from the action script.&lt;/li&gt;
&lt;li&gt;GitHub service containers.&lt;/li&gt;
&lt;li&gt;Sibling steps in the caller workflow that run outside the action boundary.&lt;/li&gt;
&lt;li&gt;macOS and Windows runners.&lt;/li&gt;
&lt;li&gt;SSH egress (planned for v0.2).&lt;/li&gt;
&lt;li&gt;MCP stdio (no network surface to enforce on).&lt;/li&gt;
&lt;li&gt;MCP HTTP/SSE without explicit Pipelock MCP listener wiring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a path is not under the Pipelock control point, the action says so. No silent fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a separate action from Pipelock Scan
&lt;/h2&gt;

&lt;p&gt;The existing Pipelock Scan action is a static check on PR diffs: it grep-and-classify finds credential leaks and injection patterns in the changed code. Agent Egress Control is a runtime container: it executes an agent script inside Pipelock-enforced isolation and produces evidence about the run.&lt;/p&gt;

&lt;p&gt;Different primitives, different scope, different SemVer cadence. Both belong on a CI workflow that runs AI agents. The two-action split keeps each artifact's CISO due-diligence story narrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/pipelock-agent-egress-control" rel="noopener noreferrer"&gt;Marketplace listing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/luckyPipewrench/pipelock-agent-egress-action/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pipelab.org/learn/agent-egress-control/" rel="noopener noreferrer"&gt;Agent Egress Control setup guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pipelab.org/learn/action-receipt-spec/" rel="noopener noreferrer"&gt;Pipelock action receipt spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pipelab.org/pipelock/" rel="noopener noreferrer"&gt;Pipelock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Politeness vs Enforcement: Why "Set HTTPS_PROXY" Isn't a Security Control</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sat, 09 May 2026 23:11:07 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/politeness-vs-enforcement-why-set-httpsproxy-isnt-a-security-control-1hka</link>
      <guid>https://dev.to/luckypipewrench/politeness-vs-enforcement-why-set-httpsproxy-isnt-a-security-control-1hka</guid>
      <description>&lt;p&gt;If your agent egress story is "we set HTTPS_PROXY to point at the proxy," the proxy is asking nicely. The kernel has no opinion on what the agent does next.&lt;/p&gt;

&lt;p&gt;This post is about the line between asking nicely and actually preventing the thing. The line is whether the kernel agrees with you. Everything on the wrong side of that line is policy. Everything on the right side is a control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bestiary
&lt;/h2&gt;

&lt;p&gt;Plenty of common AI security controls live on the asking-nicely side. A short catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HTTPS_PROXY&lt;/code&gt;, &lt;code&gt;HTTP_PROXY&lt;/code&gt;, &lt;code&gt;NO_PROXY&lt;/code&gt; environment variables.&lt;/strong&gt; Cooperative libraries read them. Uncooperative subprocesses ignore them. There is no kernel hook that says "this UID's traffic must traverse 127.0.0.1:8888."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool deny-lists at the model layer.&lt;/strong&gt; "Do not call &lt;code&gt;curl&lt;/code&gt;." The model agrees and then writes a Python script that imports &lt;code&gt;requests&lt;/code&gt;. The deny-list never sees &lt;code&gt;requests&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System prompts that say "do not exfiltrate."&lt;/strong&gt; A system prompt is text inside a context window. The text shapes the model's output distribution. The model is free to be wrong, and a prompt injection further along in the context can rewrite the rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlists in the agent's own configuration.&lt;/strong&gt; A configuration the agent process can read, the agent process can edit. Anything under &lt;code&gt;~/.config&lt;/code&gt; is asking nicely with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server lists in &lt;code&gt;~/.mcp.json&lt;/code&gt;.&lt;/strong&gt; Same shape. The agent picks which MCP servers to talk to. If the agent picks a different one, nobody stops it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The asking-nicely controls all share a pattern: the agent process is the entity expected to comply. The check, the variable, the deny-list, the allowlist, all live somewhere the agent can read or modify. Compliance is voluntary.&lt;/p&gt;

&lt;p&gt;The enforcement-side controls share a different pattern: the agent process is the entity being constrained. The check lives somewhere below the agent. A short catalog of those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NetworkPolicy in Kubernetes.&lt;/strong&gt; The CNI drops traffic that does not match the policy. The pod cannot opt out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nftables&lt;/code&gt; rules with &lt;code&gt;meta skuid&lt;/code&gt; matches.&lt;/strong&gt; The kernel drops traffic from the agent UID that does not match the egress rule. The agent process cannot remove the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux user namespaces with no route to the internet.&lt;/strong&gt; A process inside the namespace has no IP routes that lead outside. Setting &lt;code&gt;HTTPS_PROXY&lt;/code&gt; would be a no-op for traffic that has nowhere to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability separation by UID.&lt;/strong&gt; The agent UID has access to a proxy on loopback. The proxy UID has access to the internet. The agent UID does not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The line is the kernel. Below the line, no agent runtime choice changes the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real bypass that landed
&lt;/h2&gt;

&lt;p&gt;A working coding agent on a real Linux host had &lt;code&gt;HTTPS_PROXY=http://127.0.0.1:8888&lt;/code&gt; set as policy. Every browser, every CLI, every script the operator ran picked up the variable and routed through the proxy. The proxy scanned the traffic, the audit log filled up, the dashboards showed coverage.&lt;/p&gt;

&lt;p&gt;The agent decided it needed to refresh an OAuth token. It spawned a subprocess. The subprocess did not inherit the proxy environment, by design or by accident. The subprocess dialed the OAuth endpoint directly. The OAuth refresh succeeded. The proxy never saw the request.&lt;/p&gt;

&lt;p&gt;The DLP scanner did not run. The audit log did not record the request. The dashboards still showed compliant traffic for the requests that used the proxy. The operator was looking at metrics that confirmed partial compliance with a policy the agent had already routed around.&lt;/p&gt;

&lt;p&gt;Nothing about this story requires the agent to be malicious or compromised. The agent did the thing agents do: it ran a process. The process did the thing processes do: it talked to the network. The kernel, watching the whole thing, had no policy to apply because the policy lived in an environment variable inside a process that no longer existed by the time the dial happened.&lt;/p&gt;

&lt;p&gt;This is not theoretical in modern agent deployments. The fix is not "set the variable harder."&lt;/p&gt;

&lt;h2&gt;
  
  
  What enforcement actually takes
&lt;/h2&gt;

&lt;p&gt;On a workstation that runs a coding agent, an AI CLI, and a browser-driver alongside the operator's normal applications, a kernel-enforced boundary takes a few specific things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent runs as a different Linux UID than the operator and the proxy.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;nftables&lt;/code&gt; chain matches &lt;code&gt;meta skuid &amp;lt;agent_uid&amp;gt;&lt;/code&gt; and drops everything except DNS to loopback.&lt;/li&gt;
&lt;li&gt;A separate &lt;code&gt;nftables&lt;/code&gt; rule allows the proxy UID to reach the internet, because the proxy is the agent's only legitimate exit.&lt;/li&gt;
&lt;li&gt;The operator's UID is unaffected, so the desktop continues to work normally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is load-bearing. If enforcement breaks the operator's daily flow, nobody runs it. The three-UID model exists because two UIDs is not enough: the proxy needs internet, so the proxy UID has internet, so an agent running as the proxy UID inherits internet. The agent UID has to be a third identity that can only see loopback.&lt;/p&gt;

&lt;p&gt;In Kubernetes, the same idea takes pod separation. NetworkPolicy is per-pod, not per-container. Every container in the same pod shares one network namespace, so a NetworkPolicy cannot say "agent container has no internet, proxy sidecar has internet." The proxy has to live in its own pod, and the agent pod gets a NetworkPolicy whose only egress is to the proxy pod's service IP.&lt;/p&gt;

&lt;p&gt;Both stories rhyme. The kernel layer below the agent is doing the refusing. The agent's runtime choices do not reach the kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this distinction matters
&lt;/h2&gt;

&lt;p&gt;If you are evaluating an agent security tool, ask the vendor what happens when the agent ignores the tool. The answer separates policy from enforcement.&lt;/p&gt;

&lt;p&gt;A vendor whose answer is "the agent is configured to use our proxy" is selling policy. That is fine if you trust your agent. If you are running production AI assistants that handle credentials, parse untrusted content, or execute attacker-controllable instructions, you should not.&lt;/p&gt;

&lt;p&gt;A vendor whose answer is "the agent process cannot reach the internet without going through us, because the kernel says so" is selling enforcement. The implementation might be Kubernetes NetworkPolicy, Linux UID separation, or a managed-runtime environment that controls the egress. The detail varies. The shape is consistent: the agent is the entity being constrained, not the entity expected to comply.&lt;/p&gt;

&lt;p&gt;This is not a critique of asking-nicely controls in general. They have a place. A correctly-set &lt;code&gt;HTTPS_PROXY&lt;/code&gt; is real coverage for compliant traffic. A clear deny-list raises the bar for casual misuse. They are policies, and policies are useful.&lt;/p&gt;

&lt;p&gt;They are not controls. Treating them as controls produces dashboards that confirm a policy the agent has already routed around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix model in two sentences
&lt;/h2&gt;

&lt;p&gt;On Linux: put the agent process on a UID the kernel firewall denies direct internet. Allow only loopback to the proxy.&lt;/p&gt;

&lt;p&gt;In Kubernetes: put the proxy in a different pod from the agent, and write a NetworkPolicy on the agent pod whose only egress destination is the proxy pod's service IP.&lt;/p&gt;

&lt;p&gt;The rest is wrappers, CA bundles, sudoers carve-outs, and operational care. Pipelock works inside both shapes today as the proxy that handles content scanning above the kernel-enforced boundary, and the &lt;a href="https://pipelab.org/agent-firewall/" rel="noopener noreferrer"&gt;agent firewall guide&lt;/a&gt; walks the layered model that sits on top of the egress boundary. The boundary itself is the load-bearing part. Without it, every layer above it is asking nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;If you run agents on a workstation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check whether the agent process and the proxy process run as the same UID. If yes, the agent has direct internet whenever it wants it.&lt;/li&gt;
&lt;li&gt;Check whether your firewall has a rule that mentions the agent UID. If no, the policy is in &lt;code&gt;HTTPS_PROXY&lt;/code&gt; and nowhere else.&lt;/li&gt;
&lt;li&gt;Try the bypass. Open a shell as the agent UID, run &lt;code&gt;env -u HTTPS_PROXY -u HTTP_PROXY curl https://example.com&lt;/code&gt;, and see what happens. If you get a 200, your enforcement layer is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run agents in Kubernetes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check whether the agent container and the proxy container live in the same pod. If yes, the proxy can scan but cannot prevent.&lt;/li&gt;
&lt;li&gt;Check whether the agent pod has a NetworkPolicy. If no, the agent has direct internet to anything inside or outside the cluster.&lt;/li&gt;
&lt;li&gt;Try the bypass from inside the agent pod. &lt;code&gt;kubectl exec&lt;/code&gt; in, &lt;code&gt;curl https://example.com&lt;/code&gt;. A 200 is the same problem in a different shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A green dashboard with no enforcement layer below it is the most expensive form of theater in security work. Worth knowing whether you are running it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>linux</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>What Pipelock Inspects, And What Tool Policy Inspects Instead</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sat, 09 May 2026 21:22:45 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/what-pipelock-inspects-and-what-tool-policy-inspects-instead-4joe</link>
      <guid>https://dev.to/luckypipewrench/what-pipelock-inspects-and-what-tool-policy-inspects-instead-4joe</guid>
      <description>&lt;p&gt;A wire-only proxy scans wire bytes. Opaque media bytes pass through the wire layer untouched. Anyone evaluating an agent firewall should know which class of attacks gets caught at which layer, because pretending the wire layer covers everything is the wrong sales pitch and the wrong mental model.&lt;/p&gt;

&lt;p&gt;This post is the layer split. Pipelock has two inspection layers that operate at different abstraction levels, and the marketing-friendly claim "we scan everything" is true for some shapes of attack and false for others. Saying so plainly is more useful to a buyer than saying nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wire layer
&lt;/h2&gt;

&lt;p&gt;Pipelock's wire layer scans bytes as they cross the proxy. Every transport Pipelock supports gets the same set of scanners:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP forward proxy.&lt;/strong&gt; CONNECT and absolute-URI requests, request and response bodies on intercept paths, headers on every transport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP stdio.&lt;/strong&gt; JSON-RPC frames on the subprocess pipe, both directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP HTTP and SSE.&lt;/strong&gt; JSON-RPC frames over HTTP, including streaming &lt;code&gt;text/event-stream&lt;/code&gt; responses scanned per-event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket.&lt;/strong&gt; Frames in both directions, fragment reassembly, A2A envelope payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy.&lt;/strong&gt; Any HTTP-shaped agent backend Pipelock fronts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What runs on those wire bytes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DLP.&lt;/strong&gt; Pattern matching for credentials, secret formats, and high-entropy strings. Runs on URLs, request bodies, response bodies, headers, MCP arguments, MCP responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injection detection.&lt;/strong&gt; Multi-pass content matching for prompt injection, jailbreak patterns, and tool-poisoning shapes. Runs on response bodies and MCP tool definitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redaction.&lt;/strong&gt; Class-preserving outbound scrub for known credential and PII shapes. Runs on request bodies and MCP &lt;code&gt;tools/call&lt;/code&gt; arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSRF.&lt;/strong&gt; Private-IP and metadata-endpoint protection on the URL pipeline. Runs on every transport with a URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wire layer is good at credentials in headers, secrets in JSON, prompt injection in responses, and DLP-pattern leaks in tool calls. It is what stops an agent from POSTing an API key to a third-party logging service or fetching a markdown file with embedded jailbreak instructions and feeding it back to the model.&lt;/p&gt;

&lt;p&gt;What the wire layer cannot do, and what no wire-only proxy can do without strapping on a perception model, is inspect the contents of opaque media:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images.&lt;/strong&gt; A PNG of a credential-bearing screen has the credential rendered in pixels. The proxy sees image bytes, not text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio.&lt;/strong&gt; A voice memo of a customer complaint contains words the proxy would have to transcribe to inspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video.&lt;/strong&gt; Same shape as audio plus pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDFs.&lt;/strong&gt; A PDF can hold images, vector text, embedded fonts, and text-as-shapes. Naive PDF text extraction misses all of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pipelock could in principle add OCR, ASR, and PDF extraction to the wire layer. None of those scans is free. OCR on every uploaded image multiplies proxy CPU by an order of magnitude. Latency budgets that work for text scanning collapse under perception. The architectural choice for the wire layer is to scan what is cheap, fast, and high-fidelity: text, structured data, and protocol headers. Opaque media gets a different treatment at a different layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool layer
&lt;/h2&gt;

&lt;p&gt;Above the wire layer, the agent makes deliberate choices: it picks a tool to call, it constructs an argument, it sends a JSON-RPC request that names a method and a payload. The tool layer inspects those choices, not the bytes the choices move.&lt;/p&gt;

&lt;p&gt;Two scanners run at this layer in Pipelock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mcp_tool_policy&lt;/code&gt;.&lt;/strong&gt; Pre-execution allow / deny / redirect rules that match on tool names, argument patterns, and URL shapes inside arguments. The "screenshot a URL" tool can have a rule that blocks calls whose URL matches a sensitive host pattern. The URL is text, even when the result will be image bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_chain_detection&lt;/code&gt;.&lt;/strong&gt; Sequence matchers that operate on the order in which an agent calls tools. A pattern like "screenshot the logged-in admin page, then upload the screenshot to a third-party host" is a sequence of calls whose individual calls are each plausibly fine. The chain matcher catches the shape of the sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both scanners operate on JSON-shaped data: method names, argument keys, URL strings inside arguments. None of them inspects the binary data the methods move. They operate one level above the bytes.&lt;/p&gt;

&lt;p&gt;The thing they catch that the wire layer cannot: an agent that wants to exfiltrate something the wire scanner cannot read. The agent screenshots a page, uploads the screenshot, and the wire scanner sees a content-type of &lt;code&gt;image/png&lt;/code&gt; and a stream of bytes. The wire scanner has nothing to say. The tool-policy rule, watching the URL the agent passes to the screenshot tool, can see "this is a sensitive page" and block before the screenshot happens. The chain detector, watching the sequence, can see "the agent is screenshotting and uploading" and break the chain.&lt;/p&gt;

&lt;p&gt;The two layers cooperate. Wire scanning catches the credential leak the agent attempts as JSON. Tool-policy catches the equivalent leak the agent tries to launder through a screenshot. Neither alone is enough. Both together cover the surface a wire-only or tool-only design leaves open.&lt;/p&gt;

&lt;p&gt;The enforcement boundary still matters. Tool policy and wire inspection only see traffic that reaches them, which is why the &lt;a href="https://pipelab.org/blog/three-uid-agent-containment-linux/" rel="noopener noreferrer"&gt;three-UID containment pattern&lt;/a&gt; and Kubernetes per-pod separation are part of the same posture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that means for the buyer
&lt;/h2&gt;

&lt;p&gt;If your evaluation rubric reads "does this tool inspect images," the honest answer is that Pipelock does not, and that is the right design. The right question to ask any agent firewall is which layer catches which class of attack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credentials in JSON request bodies: wire layer, DLP scanner.&lt;/li&gt;
&lt;li&gt;Credentials in screenshots uploaded as image bytes: tool layer, &lt;code&gt;mcp_tool_policy&lt;/code&gt; URL rule on the screenshot tool.&lt;/li&gt;
&lt;li&gt;Prompt injection in a markdown response: wire layer, injection scanner on response body.&lt;/li&gt;
&lt;li&gt;Prompt injection in a PDF the agent fetches and processes: tool layer, policy rule on the fetch tool, plus DLP and injection scanning on whatever text the PDF parser eventually emits in tool arguments.&lt;/li&gt;
&lt;li&gt;Tool poisoning via deceptive tool descriptions: wire layer, MCP tool scanner on &lt;code&gt;tools/list&lt;/code&gt; responses.&lt;/li&gt;
&lt;li&gt;Multi-step exfiltration where each step is plausibly benign: tool layer, chain detector on the call sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern: structured-text scanning belongs on the wire, semantic-action scanning belongs on the tool layer. Anyone selling a tool that claims to do both at the wire layer is either running an OCR pipeline that they are not budgeting for, or claiming coverage they do not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pipelock will not do
&lt;/h2&gt;

&lt;p&gt;There are two specific things Pipelock does not do, and operators should plan around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pipelock does not run OCR on uploaded images. The "screenshot of a credential" scenario relies on the tool-policy rule firing before the screenshot happens, not on inspecting the image after.&lt;/li&gt;
&lt;li&gt;Pipelock does not transcribe audio. The "voice memo of a sensitive conversation" scenario relies on the policy rule on whichever tool initiated the recording, not on inspecting the audio file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both gaps are honest. Both are catchable at the tool layer if the rule set is configured correctly. The &lt;a href="https://pipelab.org/learn/mcp-security-tools/" rel="noopener noreferrer"&gt;MCP security tools guide&lt;/a&gt; and &lt;a href="https://pipelab.org/learn/mcp-tool-poisoning/" rel="noopener noreferrer"&gt;MCP tool poisoning guide&lt;/a&gt; walk the surrounding control surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;A coding agent that handles customer data has tools for reading the database, screenshotting the admin UI, and uploading files to a code-review service. Three policies catch three different attacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wire DLP catches a database row that contains an API key in a JSON column being dumped to the code-review service.&lt;/li&gt;
&lt;li&gt;Tool policy on the screenshot tool catches a prompt injection that says "screenshot the admin user list and upload it for review."&lt;/li&gt;
&lt;li&gt;Chain detection catches the pattern "read the database, then screenshot, then upload" even when the individual calls each look legitimate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each policy lives at the right layer. The wire DLP runs on the bytes. The tool policy runs on the JSON-RPC arguments. The chain detector runs on the sequence. Together they cover three shapes of attack with three different mechanisms.&lt;/p&gt;

&lt;p&gt;A buyer who insists on a single-layer answer ("we scan everything at the wire") will end up with one of those three covered and the other two leaking. A buyer who asks "what catches each shape" gets a complete posture out of two scanners that each do their job at the right level of abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;Pipelock scans wire bytes for everything that looks like text, structured data, or a protocol header. Pipelock catches semantic actions involving opaque media at the tool layer through policy rules and chain detection. The combination is what produces real coverage. Saying "we scan everything" undersells the design and overpromises the capability. Saying "we inspect at two layers, one for bytes and one for actions" is the model that holds up under scrutiny.&lt;/p&gt;

&lt;p&gt;If your evaluation matrix has a column for "scans images," cross it off. Add a column for "blocks tool calls that produce images of sensitive content." That column is the one that matters.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Block-Reason Headers: Make Your Security Proxy Tell You Why</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sat, 09 May 2026 21:19:27 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/block-reason-headers-make-your-security-proxy-tell-you-why-1f1</link>
      <guid>https://dev.to/luckypipewrench/block-reason-headers-make-your-security-proxy-tell-you-why-1f1</guid>
      <description>&lt;p&gt;When a security proxy blocks an agent's request, the agent sees a 4xx and has to guess what happened. Was the destination wrong? The body? A header? Did the proxy timeout? Did the proxy itself crash? Without context, every block looks the same and the agent burns its retry budget on a single attempt's worth of information.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;X-Pipelock-Block-Reason&lt;/code&gt; is the header Pipelock emits on every block path so the agent knows. The vocabulary is small, the format is open-spec, and the impact on operator debugging is large. This post is about the design, the schema, and why making a security proxy explain itself is good for the security posture, not bad for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem the header solves
&lt;/h2&gt;

&lt;p&gt;A coding agent runs a tool that fetches a URL, parses the response, and feeds the output back to the model. The fetch goes through Pipelock. Pipelock decides the response contains a prompt-injection pattern and returns 403 with no body.&lt;/p&gt;

&lt;p&gt;The agent has no idea what happened. From the agent's perspective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The host could be unreachable.&lt;/li&gt;
&lt;li&gt;The proxy could be misconfigured.&lt;/li&gt;
&lt;li&gt;The proxy could be down.&lt;/li&gt;
&lt;li&gt;The destination could be returning 403 itself.&lt;/li&gt;
&lt;li&gt;The agent's request could have failed scanning.&lt;/li&gt;
&lt;li&gt;The agent's response could have failed scanning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these has a different correct response from the agent. "Host unreachable" might mean "try a different host." "Proxy misconfigured" might mean "tell the operator." "Scanning blocked the request" might mean "do not retry this exact body." Without a signal, the agent treats them all the same way: retry, hit the same block, retry again, eventually give up.&lt;/p&gt;

&lt;p&gt;The operator's view is no better. The audit log records the block, but correlating an agent's confused retry sequence with the proxy's decision tree means cross-referencing two log streams by timestamp and request ID. For one block in a quiet period, fine. For a fleet generating thousands of requests an hour, painful.&lt;/p&gt;

&lt;p&gt;A structured block reason on the response solves both sides. The agent knows what happened. The operator does not have to grep two logs to figure out what the agent saw.&lt;/p&gt;

&lt;p&gt;This is the operator-facing half of enforcement. &lt;a href="https://pipelab.org/blog/politeness-vs-enforcement-https-proxy/" rel="noopener noreferrer"&gt;Politeness vs Enforcement&lt;/a&gt; explains how to make the kernel refuse bypasses; block-reason headers explain what the agent should do after the refusal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The header schema
&lt;/h2&gt;

&lt;p&gt;The full schema lives at &lt;a href="https://github.com/luckyPipewrench/pipelock/blob/main/docs/specs/block-reason-header.md" rel="noopener noreferrer"&gt;docs/specs/block-reason-header.md&lt;/a&gt; in the Pipelock repo. The shape, in one paragraph:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;X-Pipelock-Block-Reason: &amp;lt;reason&amp;gt;&lt;/code&gt; with companion headers for version, severity, retry, and the layer that fired:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Pipelock-Block-Reason: dlp_match
X-Pipelock-Block-Reason-Version: 1
X-Pipelock-Block-Reason-Severity: critical
X-Pipelock-Block-Reason-Retry: none
X-Pipelock-Block-Reason-Layer: dlp
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;X-Pipelock-Block-Reason-Receipt&lt;/code&gt; is reserved in &lt;code&gt;v2.4&lt;/code&gt;: the schema and the &lt;code&gt;WithReceipt&lt;/code&gt; validator ship in this release, but production block paths leave the value unset until the receipt-pointer wiring lands. When populated, the value will be a 26-character Crockford-base32 ULID.&lt;/p&gt;

&lt;p&gt;The reason vocabulary is closed. Examples by category include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Egress.&lt;/strong&gt; &lt;code&gt;ssrf_private_ip&lt;/code&gt;, &lt;code&gt;ssrf_metadata&lt;/code&gt;, &lt;code&gt;ssrf_dns_rebind&lt;/code&gt;, &lt;code&gt;domain_blocklist&lt;/code&gt;, &lt;code&gt;scheme_blocked&lt;/code&gt;, &lt;code&gt;subdomain_entropy&lt;/code&gt;, &lt;code&gt;url_length&lt;/code&gt;, &lt;code&gt;path_entropy&lt;/code&gt;, &lt;code&gt;rate_limit&lt;/code&gt;, &lt;code&gt;data_budget&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content.&lt;/strong&gt; &lt;code&gt;dlp_match&lt;/code&gt;, &lt;code&gt;prompt_injection&lt;/code&gt;, &lt;code&gt;redaction_failure&lt;/code&gt;, &lt;code&gt;media_policy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP.&lt;/strong&gt; &lt;code&gt;tool_policy_deny&lt;/code&gt;, &lt;code&gt;tool_poisoning&lt;/code&gt;, &lt;code&gt;tool_chain_blocked&lt;/code&gt;, &lt;code&gt;session_binding&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Posture.&lt;/strong&gt; &lt;code&gt;airlock_active&lt;/code&gt;, &lt;code&gt;kill_switch_active&lt;/code&gt;, &lt;code&gt;envelope_verify_failed&lt;/code&gt;, &lt;code&gt;outbound_envelope_failed&lt;/code&gt;, &lt;code&gt;redirect_scan_denied&lt;/code&gt;, &lt;code&gt;authority_mismatch&lt;/code&gt;, &lt;code&gt;session_anomaly&lt;/code&gt;, &lt;code&gt;cross_request_deny&lt;/code&gt;, &lt;code&gt;compressed_response&lt;/code&gt;, &lt;code&gt;browser_shield_oversize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract.&lt;/strong&gt; &lt;code&gt;contract_default_deny&lt;/code&gt;, &lt;code&gt;contract_enforce_default&lt;/code&gt;, &lt;code&gt;contract_non_default_port&lt;/code&gt;, &lt;code&gt;contract_invalid_path&lt;/code&gt;, &lt;code&gt;contract_observed_only&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic.&lt;/strong&gt; &lt;code&gt;parse_error&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;bad_request&lt;/code&gt;, &lt;code&gt;pattern_unavailable&lt;/code&gt;, &lt;code&gt;not_enabled&lt;/code&gt;, &lt;code&gt;block_reason_overflow&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full canonical list lives at &lt;a href="https://github.com/luckyPipewrench/pipelock/blob/main/docs/specs/block-reason-header.md" rel="noopener noreferrer"&gt;docs/specs/block-reason-header.md&lt;/a&gt; in the Pipelock repo and in &lt;code&gt;internal/blockreason/blockreason.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A block can have at most one reason code. The code is the primary signal. Severity and retry are advisory: severity tells the agent how loud to be when logging the block, retry tells the agent whether retrying with the same payload could ever succeed.&lt;/p&gt;

&lt;p&gt;The same reason vocabulary is used for WebSocket close frames. MCP stdio does not have an HTTP header surface, so stdio blocks flow through the JSON-RPC error envelope instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the schema is small
&lt;/h2&gt;

&lt;p&gt;Two design choices kept the vocabulary small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No free-form reason strings.&lt;/strong&gt; A free-form string would let the proxy tell the agent things like "request body contained &lt;code&gt;AKIA...EXAMPLE&lt;/code&gt; at offset 1024." That is too useful for an attacker who controls part of the request. The agent learns exactly what the scanner saw and can adjust the next attempt to avoid the match. Closed vocabularies do not leak that detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retry-after seconds.&lt;/strong&gt; A retry hint with timing would let the agent build a retry policy that matches whatever the proxy is rate-limiting. The hint is categorical: &lt;code&gt;transient&lt;/code&gt; says "retrying might work because the cause is not your request," &lt;code&gt;none&lt;/code&gt; says "this exact request will never work," and &lt;code&gt;policy&lt;/code&gt; says "this might work only after an operator changes policy."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both choices trade specificity for safety. The agent gets enough signal to react sensibly without learning how to evade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for operators
&lt;/h2&gt;

&lt;p&gt;The operator's experience changes from "decode the audit log against the agent's trace" to "read the block reason on the agent's HTTP response." Two examples:&lt;/p&gt;

&lt;p&gt;A coding agent's CI pipeline started failing on a fetch tool. The pipeline log shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch tool: HTTP 403, body empty
agent: retrying (1/3)
fetch tool: HTTP 403, body empty
agent: retrying (2/3)
fetch tool: HTTP 403, body empty
agent: failed after 3 retries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the header on, the pipeline log includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch tool: HTTP 403, X-Pipelock-Block-Reason: ssrf_private_ip
fetch tool: X-Pipelock-Block-Reason-Severity: critical
fetch tool: X-Pipelock-Block-Reason-Retry: none
agent: not retrying (non-retryable block)
agent: surfacing block reason to user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent stops retrying because retry is &lt;code&gt;none&lt;/code&gt;. The user sees a meaningful error instead of an opaque pipeline failure. The operator does not have to decode anything.&lt;/p&gt;

&lt;p&gt;Another example, MCP-shaped:&lt;/p&gt;

&lt;p&gt;A new MCP server gets added to the agent's config. The agent calls a tool from it. Pipelock's tool-policy denies the call.&lt;/p&gt;

&lt;p&gt;Without the header, the agent sees a JSON-RPC error with no actionable detail. With the header (or its JSON-RPC analog), the agent sees &lt;code&gt;tool_policy_deny&lt;/code&gt; and can route around it: ask the user, fall back to a different tool, or surface the block reason directly. The behavior change is small in code but big in the agent's ability to recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing with retry-budgeted agents
&lt;/h2&gt;

&lt;p&gt;Modern coding agents have explicit retry budgets. A budget of three on a tool call burns fast when every block looks the same. With block reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;retry=none&lt;/code&gt; consumes zero budget on retry. The agent should not retry. Surface the block.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry=transient&lt;/code&gt; consumes budget normally. The agent should retry with the same payload, possibly with backoff.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry=policy&lt;/code&gt; is the operator case. The block may clear after a policy change, but the agent should not keep guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a coding agent with a budget of three retries, this means an agent that runs into a &lt;code&gt;none&lt;/code&gt; block on attempt one fails in 100ms instead of three round trips. The retry budget is preserved for transient failures where retry actually helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema is open
&lt;/h2&gt;

&lt;p&gt;The vocabulary, header format, severity values, retry semantics, and the WebSocket and MCP variants are all documented in the open-source Pipelock repo. Anyone who wants to implement this header in their own proxy can do so without a Pipelock dependency. Any HTTP client that wants to parse the header can do so without a Pipelock-specific library. The header values are short identifiers and fixed allowlist values.&lt;/p&gt;

&lt;p&gt;If a competing agent firewall wants to adopt the same vocabulary, that is a feature, not a leak. A common reason vocabulary across vendors means agents can react sensibly regardless of which proxy is in front of them. The schema lives at &lt;a href="https://github.com/luckyPipewrench/pipelock/blob/main/docs/specs/block-reason-header.md" rel="noopener noreferrer"&gt;docs/specs/block-reason-header.md&lt;/a&gt;. Take it, use it, propose changes via issue or PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in the agent firewall stack
&lt;/h2&gt;

&lt;p&gt;Block reasons are the debugging surface of an enforcement layer. The enforcement is the load-bearing piece: the agent firewall scans the request, the response, the headers, the tool calls, and decides allow or deny. The block reason is what makes the deny actionable.&lt;/p&gt;

&lt;p&gt;A proxy without block reasons can still enforce, but every block is a black box. Operators get noisier audit logs and longer debugging cycles. Agents get worse retry behavior and more confused error messages. The header fixes both without weakening the enforcement.&lt;/p&gt;

&lt;p&gt;If you are running Pipelock on &lt;code&gt;main&lt;/code&gt; after 2026-05-02, the header is already on. If you are running an older release, &lt;a href="https://pipelab.org/blog/pipelock-v240-release/" rel="noopener noreferrer"&gt;v2.4 ships with the header on every block path&lt;/a&gt;, and the &lt;a href="https://pipelab.org/learn/block-reason-headers/" rel="noopener noreferrer"&gt;block reason headers guide&lt;/a&gt; walks the operator-facing changes. If you are running something else and your proxy gives you opaque 403s, this is the kind of feature worth asking for.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>subPath ConfigMap Mounts Don't Hot-Reload: Silent Drift in Kubernetes</title>
      <dc:creator>Josh Waldrep</dc:creator>
      <pubDate>Sat, 09 May 2026 21:17:08 +0000</pubDate>
      <link>https://dev.to/luckypipewrench/subpath-configmap-mounts-dont-hot-reload-silent-drift-in-kubernetes-52jn</link>
      <guid>https://dev.to/luckypipewrench/subpath-configmap-mounts-dont-hot-reload-silent-drift-in-kubernetes-52jn</guid>
      <description>&lt;p&gt;A Pipelock instance running in a Kubernetes cluster watched its config file for hours while four edits to the underlying ConfigMap landed in etcd. The dashboards showed updates. The pod showed an old config. The tests that exercised the new config kept failing for reasons that made no sense.&lt;/p&gt;

&lt;p&gt;The problem is not Pipelock. The problem is &lt;code&gt;subPath&lt;/code&gt;. Mount a ConfigMap key as a single file with &lt;code&gt;subPath&lt;/code&gt;, and kubelet stops propagating updates to that mount. The behavior is documented but easy to miss, and it is the load-bearing reason any service that runs hot-reload in Kubernetes needs to think about how its config volume is mounted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the bug
&lt;/h2&gt;

&lt;p&gt;A reasonable-looking ConfigMap mount in a Deployment spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/pipelock/pipelock.yaml&lt;/span&gt;
          &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock.yaml&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
      &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod gets &lt;code&gt;/etc/pipelock/pipelock.yaml&lt;/code&gt; populated from the &lt;code&gt;pipelock.yaml&lt;/code&gt; key of the &lt;code&gt;pipelock-config&lt;/code&gt; ConfigMap. Other files in &lt;code&gt;/etc/pipelock/&lt;/code&gt; are unaffected. This is what &lt;code&gt;subPath&lt;/code&gt; was designed for: pin one file to one path without taking over the whole directory.&lt;/p&gt;

&lt;p&gt;The drift surfaces when you &lt;code&gt;kubectl edit configmap pipelock-config&lt;/code&gt;, change the value, and watch the running pod for the change to propagate. It does not propagate. The running pod's view of &lt;code&gt;/etc/pipelock/pipelock.yaml&lt;/code&gt; is the same content it had at pod creation. The kubelet has updated the underlying ConfigMap volume, but the bind mount that &lt;code&gt;subPath&lt;/code&gt; created points at a different inode that is not part of the update path.&lt;/p&gt;

&lt;p&gt;Restart the pod and the new content shows up. fsnotify watchers configured to react to file changes never fire because the file the container sees is, from the container's perspective, the same file it has always been.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the directory mount works
&lt;/h2&gt;

&lt;p&gt;Drop the &lt;code&gt;subPath&lt;/code&gt; and mount the whole ConfigMap as a directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/pipelock&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
      &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelock-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;/etc/pipelock/&lt;/code&gt; contains every key from the ConfigMap. Kubelet syncs the directory periodically, subject to its sync period and ConfigMap cache. The atomic-update mechanism Kubernetes uses for ConfigMap volumes replaces a symlink that points at the current "version" of the data. Watchers need to watch the mounted directory or reopen the file path after an update, because the inode under an old file descriptor can change. With that watch shape, the service hot-reloads correctly.&lt;/p&gt;

&lt;p&gt;The cost is that &lt;code&gt;/etc/pipelock/&lt;/code&gt; now belongs to the ConfigMap. If you had other files in that directory (a CA certificate from a different volume, a generated state file written by an init container), the directory mount overwrites them. You have to mount each piece into a directory of its own and let the service compose them at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kubelet propagation lifecycle
&lt;/h2&gt;

&lt;p&gt;Kubelet runs a sync loop that watches the API server for ConfigMap and Secret updates. When an update lands, kubelet writes the new content to a versioned directory inside the volume's emptyDir on the node, then atomically swaps a symlink. The container, which had been reading through the symlink, now reads the new version. The whole swap is one syscall, so readers either see the old version or the new version, never a torn state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;subPath&lt;/code&gt; works by computing the source path at pod creation and creating a bind mount to that specific path. The bind mount captures the inode that backs the file at that moment. Kubelet's atomic swap operates on the symlink in the volume, not on the inode the bind mount points at. The bind survives the swap and continues to point at the original inode, which kubelet never updates.&lt;/p&gt;

&lt;p&gt;There is no documented kubelet behavior that re-evaluates a &lt;code&gt;subPath&lt;/code&gt; mount during a ConfigMap update. The upstream issue, &lt;a href="https://github.com/kubernetes/kubernetes/issues/50345" rel="noopener noreferrer"&gt;kubernetes/kubernetes#50345&lt;/a&gt;, has been open since 2017. The current state of the world is "subPath plus ConfigMap is static for running containers."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this hurts
&lt;/h2&gt;

&lt;p&gt;Anyone running a service that watches its config file for live updates. Pipelock has fsnotify-based hot-reload on its config (SIGHUP is also supported). Other services with the same shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Envoy and most service-mesh proxies, which use file-based dynamic configuration discovery.&lt;/li&gt;
&lt;li&gt;Prometheus, which reloads scrape configs on file change.&lt;/li&gt;
&lt;li&gt;Nginx with the &lt;code&gt;auto_reload&lt;/code&gt; patches.&lt;/li&gt;
&lt;li&gt;Any custom service that watches its config for runtime updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For all of these, &lt;code&gt;subPath&lt;/code&gt; is a silent foot-gun. The service starts up, reads its config, watches the file, and never sees the file change because the file is a frozen bind mount.&lt;/p&gt;

&lt;p&gt;The damage scales with how much you trust your config-update workflow. If you &lt;code&gt;kubectl apply&lt;/code&gt; a new ConfigMap and assume the running pod picks it up, every minute between the apply and the next pod restart is a minute the cluster is running stale config. For a security tool, that gap means the new policy is not enforced, the new pattern does not match, the new allowlist is not honored. The dashboards say one thing. The reality is another.&lt;/p&gt;

&lt;p&gt;This is the Kubernetes version of the same lesson in &lt;a href="https://pipelab.org/blog/politeness-vs-enforcement-https-proxy/" rel="noopener noreferrer"&gt;Politeness vs Enforcement&lt;/a&gt;: a dashboard that confirms the policy object changed is not the same thing as a kernel-enforced runtime boundary that changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two patterns that work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mount the directory, expose the file.&lt;/strong&gt; Mount the ConfigMap as a directory volume, and read the file by path from inside that directory. This is the simplest pattern for services that own their config directory. The cost is that the directory is now ConfigMap-shaped, so anything else that needs to live there has to come from a different mount point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidecar plus emptyDir.&lt;/strong&gt; A sidecar container mounts the ConfigMap as a directory, watches for updates, and writes the consolidated file to a shared emptyDir volume that the main container reads. This adds a moving piece, but it lets you compose multiple sources (ConfigMap, Secret, downward API, environment) into a single config file at a single path. The sidecar pattern is heavier than the direct mount but more flexible when the file's content is built from multiple inputs.&lt;/p&gt;

&lt;p&gt;If you see &lt;code&gt;subPath:&lt;/code&gt; and your service expects hot-reload, change the mount shape. The sidecar pattern is the fallback when a directory mount conflicts with other tenants of the target path.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to spot this in a fleet
&lt;/h2&gt;

&lt;p&gt;A Helm chart or Kustomize overlay that mounts a ConfigMap with &lt;code&gt;subPath:&lt;/code&gt; on a service that has a hot-reload code path is the warning. Two signals to look for in code review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A volume mount with &lt;code&gt;subPath:&lt;/code&gt; and a target path that matches a config file the service is known to watch.&lt;/li&gt;
&lt;li&gt;The service's startup logs include "watching config file" or fsnotify-style messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If both are true, the hot-reload is broken. The service will read the initial value at startup and freeze.&lt;/p&gt;

&lt;p&gt;The other signal is operational. If your platform has a story like "edit the ConfigMap, observe the pod pick up the new value," and that story sometimes does not work, &lt;code&gt;subPath&lt;/code&gt; is the first thing to check. The behavior is consistent: &lt;code&gt;subPath&lt;/code&gt; mounts always freeze, directory mounts always update. The trick is that the freeze is silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this surfaced
&lt;/h2&gt;

&lt;p&gt;A fleet of agent-firewall sidecars all mounted their Pipelock config with &lt;code&gt;subPath:&lt;/code&gt;. Operators added new entries to the redaction allowlist over a span of two weeks. The ConfigMaps in etcd reflected the changes. The running Pipelock pods continued to apply the older allowlist that had been in effect when the pods started. The drift surfaced when a new test fixture failed against the live agent because Pipelock had not picked up the allowlist entry that had been added to the ConfigMap five days earlier.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl delete pod&lt;/code&gt; fixed the symptom. Switching to a directory mount fixed the cause. The lesson generalizes: any service that hot-reloads needs a directory mount for its config, not a &lt;code&gt;subPath&lt;/code&gt; mount. If your platform team has standardized on &lt;code&gt;subPath&lt;/code&gt; for "single file" ConfigMap injection, audit which of those services have hot-reload code paths and migrate them.&lt;/p&gt;

&lt;p&gt;The Kubernetes docs warn about this behavior in the &lt;a href="https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically" rel="noopener noreferrer"&gt;ConfigMap reference&lt;/a&gt;, but only in passing, and the &lt;code&gt;subPath&lt;/code&gt; documentation does not link to the warning. The upstream issue is the canonical reference for the depth of the problem and the design discussion around fixing it. Until that discussion turns into a kubelet change, the workaround is structural: avoid &lt;code&gt;subPath&lt;/code&gt; for hot-reload-bearing files.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
