<?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: Ilya Ploskovitov</title>
    <description>The latest articles on DEV Community by Ilya Ploskovitov (@aragossa).</description>
    <link>https://dev.to/aragossa</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%2F3583415%2F71ea785a-1a2a-4eab-b5ca-3efa702702fe.png</url>
      <title>DEV Community: Ilya Ploskovitov</title>
      <link>https://dev.to/aragossa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aragossa"/>
    <language>en</language>
    <item>
      <title>Redact PII in bank logs under GDPR: hash it, don't mask it</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Thu, 24 Sep 2026 19:03:42 +0000</pubDate>
      <link>https://dev.to/aragossa/redact-pii-in-bank-logs-under-gdpr-hash-it-dont-mask-it-20d1</link>
      <guid>https://dev.to/aragossa/redact-pii-in-bank-logs-under-gdpr-hash-it-dont-mask-it-20d1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://pii-shield.com/guide-banking-logs-gdpr" rel="noopener noreferrer"&gt;pii-shield.com/guide-banking-logs-gdpr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; An account number in a log line is personal data, and the pipeline carrying it is processing. Redact it before the line leaves the pod, and put a keyed hash in its place rather than a constant, so the log can still answer "one customer or four hundred?" when the 72-hour clock is running. Below: where banks redact today and where each option breaks, a payments log before and after, how to check it worked, and what an entropy scanner will not find.&lt;/p&gt;

&lt;h2&gt;
  
  
  An account number in a log line is personal data
&lt;/h2&gt;

&lt;p&gt;Nobody argues about the customer table. The log line that says &lt;code&gt;payment.authorised account_number=GB29NWBK60161331926819&lt;/code&gt; is the same account number, attached to the same person, and Article 4(1) lists "an identification number" among the things that make someone identifiable. So the log is personal data, and everything that touches it on the way to the dashboard is processing: the file on the node, the shipper's queue, the vendor's index, the backup of that index.&lt;/p&gt;

&lt;p&gt;In a bank this usually comes up during a DPIA, and more often than not about a system that has been shipping logs to a SaaS for three years. The awkward part is that the lines have already left, and you cannot redact what is stored under somebody else's retention policy.&lt;/p&gt;

&lt;p&gt;The rest of this post is about the next line.&lt;/p&gt;

&lt;h2&gt;
  
  
  How banks redact today, and where each option breaks
&lt;/h2&gt;

&lt;p&gt;Where you redact decides how far the raw value has already travelled and who owns the rule. The four places, from earliest to latest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Where the raw value has already been&lt;/th&gt;
&lt;th&gt;Who maintains the rule&lt;/th&gt;
&lt;th&gt;Where it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In the application&lt;/td&gt;
&lt;td&gt;Nowhere. Best possible boundary&lt;/td&gt;
&lt;td&gt;Every service, every team&lt;/td&gt;
&lt;td&gt;The one service nobody touched since 2019, and every new field somebody logs next quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In the pod, before the shipper (sidecar)&lt;/td&gt;
&lt;td&gt;A file on a volume inside the pod&lt;/td&gt;
&lt;td&gt;Platform team, one config&lt;/td&gt;
&lt;td&gt;The app has to write to a file the sidecar can read; stdout-only apps need a small change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At the shipper (Logstash &lt;code&gt;gsub&lt;/code&gt;, Fluentd &lt;code&gt;record_transformer&lt;/code&gt;, Alloy &lt;code&gt;stage.replace&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Node disk, the shipper's queue, sometimes a buffer on another node&lt;/td&gt;
&lt;td&gt;Whoever owns the collector config&lt;/td&gt;
&lt;td&gt;Regex lists rot. A new field or a new format ships and the rule does not know about it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At the vendor (a sensitive-data scanner in the SaaS pipeline)&lt;/td&gt;
&lt;td&gt;Everything above, plus the vendor's ingestion infrastructure&lt;/td&gt;
&lt;td&gt;The vendor's rule engine, your rules in it&lt;/td&gt;
&lt;td&gt;The raw value reached a third party before anything masked it. That is the sentence the DPIA will quote&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a comparison of boundaries, not of detection quality. A well-kept Logstash filter catches what its regexes describe. The problem is the word "kept": the list was written for the fields that existed when it was written, and the line has been on disk for a while before the filter sees it.&lt;/p&gt;

&lt;p&gt;PII-Shield sits in the second row. It runs as a sidecar in the pod, reads the application's log file from a shared volume, and prints the cleaned line to its own stdout, where the existing collector picks it up with no configuration change. The raw line stays on the pod's volume and goes nowhere. The &lt;a href="https://pii-shield.com/guide-fluentd" rel="noopener noreferrer"&gt;Fluentd&lt;/a&gt;, &lt;a href="https://pii-shield.com/guide-datadog" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt; and &lt;a href="https://pii-shield.com/guide-alloy" rel="noopener noreferrer"&gt;Grafana Alloy&lt;/a&gt; guides show the wiring for each collector. This post is about what the redaction should do once it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in place of the value decides whether the log is still worth reading
&lt;/h2&gt;

&lt;p&gt;A constant like &lt;code&gt;REDACTED&lt;/code&gt; throws away the one thing that makes a redacted log useful. Forty lines carrying the same leaked account number turn into forty unrelated events. When the 72-hour notification clock under Article 33 is running, the question everyone asks first is whether it is one customer or four hundred, and a log full of &lt;code&gt;REDACTED&lt;/code&gt; cannot answer it.&lt;/p&gt;

&lt;p&gt;A keyed hash can. Same value, same salt, same tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login ok account_number=[HIDDEN:88797d]
export started account_number=[HIDDEN:88797d]
export started account_number=[HIDDEN:be171d]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of the three lines are about the same customer, and you can see that without knowing who the customer is. Distinct-count queries work. So do joins across services. Nothing readable is stored anywhere. The tag is an HMAC of the value under a salt you hold, so it is one-way for anyone without the salt and confirmable for anyone with it.&lt;/p&gt;

&lt;p&gt;Change the salt per environment and the tags stop lining up between production and the test cluster somebody handed a contractor. The same declined payment, under &lt;code&gt;PII_SALT=0123456789abcdef0123456789abcdef&lt;/code&gt; and then under &lt;code&gt;PII_SALT=fedcba9876543210fedcba9876543210&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-09-20T10:14:09Z WARN payment.declined account_number=[HIDDEN:88797d] reason=limit_exceeded request_id=7c1d9f
2026-09-20T10:14:09Z WARN payment.declined account_number=[HIDDEN:ba8164] reason=limit_exceeded request_id=7c1d9f
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A payments log, before and after
&lt;/h2&gt;

&lt;p&gt;Three lines a payments service might write. Nothing here was hand-edited: the "after" block is the output of the released CLI with a fixed test salt and default settings, so you can reproduce it.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-09-20T10:14:03Z INFO payment.authorised account_number=GB29NWBK60161331926819 card=4539148803436467 customer=j.smith@example.co.uk amount=120.00 session=AbC9xY2kQ8pLmN0rZq7 request_id=7c1d9e
2026-09-20T10:14:09Z WARN payment.declined account_number=GB29NWBK60161331926819 reason=limit_exceeded request_id=7c1d9f
2026-09-20T10:15:41Z WARN transfer.rejected from=31926819 to=44017265 sort_code=60-16-13 channel=mobile msg="beneficiary account closed"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After, &lt;code&gt;PII_SALT=0123456789abcdef0123456789abcdef pii-shield &amp;lt; payments.log&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-09-20T10:14:03Z INFO payment.authorised account_number=[HIDDEN:88797d] card=[HIDDEN:b9025c] customer=[HIDDEN:fc65b3] amount=120.00 session=[HIDDEN:7e1d32] request_id=7c1d9e
2026-09-20T10:14:09Z WARN payment.declined account_number=[HIDDEN:88797d] reason=limit_exceeded request_id=7c1d9f
2026-09-20T10:15:41Z WARN transfer.rejected from=31926819 to=44017265 sort_code=60-16-13 channel=mobile msg="beneficiary account closed"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happened, field by field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The IBAN was hidden on both lines with the same tag, &lt;code&gt;88797d&lt;/code&gt;. That is the counting property from the previous section, and it is what lets you see the declined payment and the authorised one belong to one customer.&lt;/li&gt;
&lt;li&gt;The card number passed a Luhn check. The email and the session token were hidden for a duller reason: they look random enough to the entropy scorer.&lt;/li&gt;
&lt;li&gt;The amount, the reason and the request id were left alone. &lt;code&gt;120.00&lt;/code&gt; is protected by a decimal rule, &lt;code&gt;limit_exceeded&lt;/code&gt; and &lt;code&gt;7c1d9e&lt;/code&gt; score below the threshold.&lt;/li&gt;
&lt;li&gt;The third line was not touched at all. Two 8-digit account numbers and a sort code went through in the clear. That is the limit of anything generic, and the next section is about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your own formats are yours to write
&lt;/h2&gt;

&lt;p&gt;A 22-character IBAN clears the entropy bar because it looks random. An 8-digit domestic account number does not, and neither does &lt;code&gt;60-16-13&lt;/code&gt;, and no generic detector can tell either of them from an order id. There is no IBAN detector and no account-number detector in PII-Shield, and I would be suspicious of one: your reference formats are your bank's, so the rule for them has to be yours too. It is one line of configuration:&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="nv"&gt;PII_CUSTOM_REGEX_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"name":"acct","pattern":"^\\d{8}$"},{"name":"sort-code","pattern":"^\\d{2}-\\d{2}-\\d{2}$"}]'&lt;/span&gt;
&lt;span class="nv"&gt;PII_ENTITY_TYPE_LABELS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third line again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-09-20T10:15:41Z WARN transfer.rejected from=[HIDDEN:acct:cca553] to=[HIDDEN:acct:6b4e22] sort_code=[HIDDEN:sort-code:e026ca] channel=mobile msg="beneficiary account closed"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marker now names the rule that fired. With the second variable on, the built-in detectors name themselves as well, so the first line becomes &lt;code&gt;account_number=[HIDDEN:entropy:88797d] card=[HIDDEN:card:b9025c]&lt;/code&gt; and a reviewer can tell a card from a session token without seeing either.&lt;/p&gt;

&lt;p&gt;That label is the part I got wrong for a while. For the first six months every match came out as the same bare marker with no type in it, so a redacted line told you something had been hidden and nothing about what kind of thing it was. An account number and a session id were indistinguishable. Type labels came later, and they are still off by default, because changing the shape of every marker breaks whatever people had already built on the old one. Turn them on in staging first and look at what they say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying it actually worked
&lt;/h2&gt;

&lt;p&gt;A redaction you have not checked is a belief. The first check is the one I would not skip: put a synthetic account number, a test card and a made-up email into a staging log line you control, then &lt;code&gt;grep -c&lt;/code&gt; for each of them on the collector side. The answer has to be 0. Reading the config tells you what you intended; this tells you what happened.&lt;/p&gt;

&lt;p&gt;Then take a few thousand lines of production-shaped logs with test data, run the CLI on them with a fixed salt, and read the diff. You are looking for two things at once, values that survived and values that should have been left alone.&lt;/p&gt;

&lt;p&gt;Run the same sample with &lt;code&gt;PII_ENTITY_TYPE_LABELS=true&lt;/code&gt;. A card that comes back as &lt;code&gt;entropy&lt;/code&gt; rather than &lt;code&gt;card&lt;/code&gt; is still hidden, but the Luhn path did not see it, and the next format change may not be so lucky.&lt;/p&gt;

&lt;p&gt;Two smaller checks. Log the same test identifier from two services under the same salt and confirm the tag is identical; if it is not, the salts differ, and your incident counts will be wrong on the day they matter. And with metrics enabled, watch &lt;code&gt;piishield_redaction_events_total&lt;/code&gt;, a Prometheus counter with a &lt;code&gt;type&lt;/code&gt; label: a drop in &lt;code&gt;card&lt;/code&gt; after a deploy is a format change somewhere upstream, a rise in &lt;code&gt;entropy&lt;/code&gt; is usually a new field logging something random.&lt;/p&gt;

&lt;p&gt;Last, every rule that protects a value from redaction can also hide a leak. After you add one to the safe list, run the planted-value check again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Values without a shape are not found. Card numbers, keys, emails and tokens have one. A customer's name in a free-text complaint field does not, and no threshold will find it. This line comes out of the scanner unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;complaint="Mr John Smith says the card was blocked twice"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finding names needs a model, which costs latency and money, and that is a different tool at a different point in the pipeline. The practical answer for logs is usually to decide that free text does not belong in them, rather than to try to scrub it after the fact.&lt;/p&gt;

&lt;p&gt;Some ordinary values look random to the scorer. An 18-character snake_case enum and a short hex-looking id can both cross the threshold. In the sample I ran for this post, the first false positive was a request id and the second was a reason code. Neither needed hiding. Here they are, then the safe list that fixes them, then the line again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-09-20T10:16:02Z WARN payment.declined reason=[HIDDEN:fd4d4d] request_id=[HIDDEN:c4c401]

&lt;/span&gt;&lt;span class="gp"&gt;PII_SAFE_REGEX_LIST='[{"name":"reason-enum","pattern":"^[a-z]+(_[a-z]+)+$&lt;/span&gt;&lt;span class="s2"&gt;"},{"&lt;/span&gt;name&lt;span class="s2"&gt;":"&lt;/span&gt;req-id&lt;span class="s2"&gt;","&lt;/span&gt;pattern&lt;span class="s2"&gt;":"&lt;/span&gt;^req_[0-9a-f]&lt;span class="o"&gt;{&lt;/span&gt;4&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;$"}]'
&lt;/span&gt;&lt;span class="go"&gt;
2026-09-20T10:16:02Z WARN payment.declined reason=insufficient_funds request_id=req_8f3a
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will find a handful of these in the first diff. Add them to the safe list by shape, not by value, and then re-run the planted-value check, because a safe-list rule that is too wide is a leak with a name.&lt;/p&gt;

&lt;p&gt;Hashing is pseudonymisation, not anonymisation. That is Article 4(5). The data stays personal data; you still owe retention, subject rights and a record of processing. What you get is a smaller blast radius when the wrong dashboard is open in front of the wrong person, and Article 32 does count pseudonymisation as a security measure. Your DPO gets the final word on the classification, not this post.&lt;/p&gt;

&lt;p&gt;And the old lines are gone. Nothing here reaches back into what a vendor already stored; that conversation is about the vendor's retention and deletion process.&lt;/p&gt;

&lt;p&gt;Two operational notes. Rotate the salt and correlation resets: tags before and after a rotation do not match, so plan rotations like key rotations, with a date and a note in the runbook. And the sidecar reads a file: an application that only writes to stdout needs to write to a shared volume instead, or use the in-process library, because transparent stdout capture is still at the design stage. The full list is in &lt;a href="https://github.com/pii-shield/pii-shield/blob/main/KNOWN_LIMITATIONS.md" rel="noopener noreferrer"&gt;KNOWN_LIMITATIONS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the CLI on a sample.&lt;/strong&gt; Grab the binary or the container from the &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, set a fixed test salt, pipe a few thousand lines through it and read the diff. Note what was missed and what was hidden by mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the rules for your formats.&lt;/strong&gt; Account numbers, sort codes, internal customer ids, reference numbers. One &lt;code&gt;PII_CUSTOM_REGEX_LIST&lt;/code&gt; entry each, named, so the marker says which one fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the collector guide and wire the sidecar.&lt;/strong&gt; &lt;a href="https://pii-shield.com/guide-fluentd" rel="noopener noreferrer"&gt;Fluentd and Fluent Bit&lt;/a&gt;, &lt;a href="https://pii-shield.com/guide-elk" rel="noopener noreferrer"&gt;ELK&lt;/a&gt;, &lt;a href="https://pii-shield.com/guide-datadog" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt;, &lt;a href="https://pii-shield.com/guide-loki" rel="noopener noreferrer"&gt;Loki&lt;/a&gt;, &lt;a href="https://pii-shield.com/guide-opentelemetry" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; or &lt;a href="https://pii-shield.com/guide-alloy" rel="noopener noreferrer"&gt;Grafana Alloy&lt;/a&gt;. Then run the planted-value check on the collector side, not on the pod.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next log line is the one you can still do something about. PII-Shield is Apache-2.0 on &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Run it on a sample before you run it on a cluster.&lt;/p&gt;

</description>
      <category>security</category>
      <category>gdpr</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mask PII in Grafana Alloy logs without a stage.replace pattern list</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Sat, 12 Sep 2026 18:35:04 +0000</pubDate>
      <link>https://dev.to/aragossa/mask-pii-in-grafana-alloy-logs-without-a-stagereplace-pattern-list-7bf</link>
      <guid>https://dev.to/aragossa/mask-pii-in-grafana-alloy-logs-without-a-stagereplace-pattern-list-7bf</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://pii-shield.com/guide-alloy" rel="noopener noreferrer"&gt;pii-shield.com/guide-alloy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Alloy can mask log lines with &lt;code&gt;loki.process&lt;/code&gt; and &lt;code&gt;stage.replace&lt;/code&gt;, and it even hashes with a salt if you ask it to. What it cannot do is mask a value before that value is written to the node's disk — by the time Alloy reads the line, the raw secret has already been sitting in &lt;code&gt;/var/log/pods&lt;/code&gt;. Redact inside the pod with a PII-Shield sidecar, and Alloy collects lines that were never sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyone is rewriting their log pipeline right now
&lt;/h2&gt;

&lt;p&gt;Promtail reached end of life on 2 March 2026. Its features were merged into Grafana Alloy, and every Promtail deployment is being converted — by hand or with &lt;code&gt;alloy convert&lt;/code&gt; — into Alloy components.&lt;/p&gt;

&lt;p&gt;Which makes this a good moment to ask an awkward question about the pipeline being rewritten: if it masked personal data before, where exactly did that masking happen?&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard approach: loki.process and stage.replace
&lt;/h2&gt;

&lt;p&gt;Alloy's answer to redaction is a processing stage between the source and the write component. A typical Kubernetes setup discovers pods, tails their log files, runs them through &lt;code&gt;loki.process&lt;/code&gt;, and writes to Loki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discovery.kubernetes "pod" {
  role = "pod"
  selectors {
    role  = "pod"
    field = "spec.nodeName=" + coalesce(sys.env("HOSTNAME"), constants.hostname)
  }
}

discovery.relabel "pod_logs" {
  targets = discovery.kubernetes.pod.targets
  rule {
    source_labels = ["__meta_kubernetes_namespace"]
    target_label  = "namespace"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_name"]
    target_label  = "pod"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_container_name"]
    target_label  = "container"
  }
}

loki.source.kubernetes "pod_logs" {
  targets    = discovery.relabel.pod_logs.output
  forward_to = [loki.process.redact.receiver]
}

loki.process "redact" {
  forward_to = [loki.write.default.receiver]

  stage.replace {
    expression = "([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+)"
    replace    = "[REDACTED_EMAIL]"
  }

  stage.replace {
    expression = "(Bearer [A-Za-z0-9\\-_.]+)"
    replace    = "[REDACTED_TOKEN]"
  }

  // Hashing keeps correlation: the same value always yields the same digest.
  stage.replace {
    expression = "user_id=(\\d+)"
    replace    = "{{ Hash .Value \"a-long-random-salt\" }}"
  }
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credit where it is due: that last stage is better than most collectors offer. Alloy's &lt;code&gt;replace&lt;/code&gt; field takes a Go template, and the &lt;code&gt;Hash&lt;/code&gt; function takes a salt and returns a SHA3-256 digest — so a masked value stays consistent across lines and your dashboards keep grouping by user without exposing anyone. If you are going to mask in the collector, mask like that, not with a constant.&lt;/p&gt;

&lt;p&gt;Run that exact configuration, though, and watch what comes out the other end. Feed it one line carrying an email, an &lt;code&gt;Authorization&lt;/code&gt; header and a Stripe key, and the email becomes &lt;code&gt;[REDACTED_EMAIL]&lt;/code&gt;, the header becomes &lt;code&gt;[REDACTED_TOKEN]&lt;/code&gt;, the same &lt;code&gt;user_id&lt;/code&gt; hashes to the same digest on every line it appears in — and &lt;code&gt;sk_live_4eC39HqLyjWDarjt&lt;/code&gt; ships to Loki exactly as it was written, because nothing in the list above covers it. That is the whole failure mode in one line of output: the stages you wrote work, and the secret you did not think of leaves silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this stops being enough
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The value has already left the pod.&lt;/strong&gt; &lt;code&gt;loki.source.kubernetes&lt;/code&gt; reads what the kubelet already wrote to the node — the raw line exists in &lt;code&gt;/var/log/pods&lt;/code&gt; before any stage runs. Anything else on that node with read access to those files (a second collector, a debugging sidecar, a node agent, someone with &lt;code&gt;kubectl debug&lt;/code&gt;) sees the unmasked value, and your log retention on the node is now a copy you did not plan for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have to enumerate the shapes.&lt;/strong&gt; Every stage matches one RE2 expression. Emails, bearer tokens and your internal ID format are three stages; the API key format a vendor introduced last month is a fourth that nobody has written yet. A stage that does not match fails silently — there is no signal that a secret went past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost lands on the shared collector.&lt;/strong&gt; Alloy usually runs one instance per node for every pod on it. Each stage is evaluated against each line, so the work scales with total log volume multiplied by the length of your pattern list, on a process your whole node shares.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternative: redact inside the pod
&lt;/h2&gt;

&lt;p&gt;Move the redaction one step earlier — into the pod that produced the line — and the three problems above disappear together. PII-Shield is a small Go sidecar that reads the application's log file, scrubs it, and prints clean lines to its own stdout. It finds secrets by Shannon entropy and key context rather than by a list of shapes, so an unfamiliar token format is still caught.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;What&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wrote:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"payment accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"john.doe@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"88213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"sk_live_4eC39HqLyjWDarjt"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;What&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Alloy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;node:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"payment accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[HIDDEN:adc335]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"88213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[HIDDEN:4a692a]"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marker is a salted HMAC of the value, so &lt;code&gt;[HIDDEN:adc335]&lt;/code&gt; is the same person on every line and in every service that shares the salt — the same correlation the &lt;code&gt;Hash&lt;/code&gt; template gives you, except the raw value never reached the node's disk to begin with.&lt;/p&gt;

&lt;p&gt;Note what did &lt;strong&gt;not&lt;/strong&gt; change in that line: &lt;code&gt;user_id&lt;/code&gt; is still &lt;code&gt;88213&lt;/code&gt;. A bare number has no shape to recognise and &lt;code&gt;user_id&lt;/code&gt; is not a secret-bearing key name, so the scanner leaves it alone rather than guessing. If that identifier is personal data in your threat model, it goes in as a named rule through &lt;code&gt;PII_CUSTOM_REGEX_LIST&lt;/code&gt; — the same explicit decision the &lt;code&gt;stage.replace&lt;/code&gt; above was making, taken once and applied in every pod instead of in each collector config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes implementation
&lt;/h2&gt;

&lt;p&gt;The application writes to a shared &lt;code&gt;emptyDir&lt;/code&gt; instead of stdout; the sidecar owns stdout for the pod. Alloy needs no knowledge of any of this.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;pii-shield-secret&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pii-salt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace-with-a-long-random-value"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing&lt;/span&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;billing-app&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;billing-app:v2.1.0&lt;/span&gt;
      &lt;span class="c1"&gt;# The app writes its private output to a shared file&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;/bin/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;./billing-binary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;pii-shield-sidecar&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;thelisdeep/pii-shield:2.2.3&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PII_SALT&lt;/span&gt;
          &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretKeyRef&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;pii-shield-secret&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pii-salt&lt;/span&gt;
      &lt;span class="c1"&gt;# Scratch image: run the binary directly (no shell, no tail).&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;/pii-shield"&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;--watch-file"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;log-volume&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Alloy config then shrinks back to collection and labelling — the redaction stages come out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loki.source.kubernetes "pod_logs" {
  targets    = discovery.relabel.pod_logs.output
  forward_to = [loki.process.pod_logs.receiver]
}

loki.process "pod_logs" {
  forward_to = [loki.write.default.receiver]

  stage.static_labels {
    values = {
      cluster = "production",
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify it is actually working
&lt;/h2&gt;

&lt;p&gt;Check the sidecar's own output first. Querying Loki only tells you what arrived; it does not tell you whether the raw stream is still being written somewhere on the node because a mount path was wrong.&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;# This is exactly what Alloy will collect&lt;/span&gt;
kubectl logs billing-service &lt;span class="nt"&gt;-c&lt;/span&gt; pii-shield-sidecar &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50

&lt;span class="c"&gt;# Nothing raw should survive&lt;/span&gt;
kubectl logs billing-service &lt;span class="nt"&gt;-c&lt;/span&gt; pii-shield-sidecar &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"@|Bearer|sk_live"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;HIDDEN:"&lt;/span&gt;

&lt;span class="c"&gt;# And the app container should now be quiet: its output goes to the file, not to stdout&lt;/span&gt;
kubectl logs billing-service &lt;span class="nt"&gt;-c&lt;/span&gt; billing-app &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command should print nothing. If it prints a line, that format is not being caught yet — report it as a scanner bug rather than assuming the pipeline is safe. The third should be empty too; if it is not, the application is still writing to stdout and Alloy is collecting the raw stream alongside the clean one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;This covers the logs path only. Metrics and traces that your instrumentation sends straight to Alloy over OTLP never pass through the sidecar, so if span attributes carry personal data, handle those in Alloy's own processors.&lt;/p&gt;

&lt;p&gt;Detection is not magic either. Entropy and key context catch values with a shape — tokens, keys, contact details, card numbers. A customer's name sitting in a free-text field has no shape, and no threshold finds it; that needs a model, which is a different cost. Your own internal identifier formats do have a shape, but it is yours, so it goes in as a rule (&lt;code&gt;PII_CUSTOM_REGEX_LIST&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Nothing here retroactively cleans what has already been shipped to Loki, and none of it is a substitute for not logging secrets in the first place — it is the last line of defence for what slips through anyway. And if you already have a &lt;code&gt;stage.replace&lt;/code&gt; list that works, there is no reason to delete it on day one: the sidecar in the pod and the stage in the collector compose fine, and running both while you build confidence costs you nothing but a few regex evaluations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not just add a stage.replace for every pattern and be done?&lt;/strong&gt;&lt;br&gt;
Because two of the three problems stay. A stage only masks the shapes you wrote down, and it runs after the kubelet has already written the raw line to the node's disk, so the value existed in the clear on that node regardless. Enumerating shapes is also the part that quietly rots: the vendor API key format introduced last month has no stage yet, and nothing tells you it went past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I am migrating from Promtail to Alloy — do my masking stages carry over?&lt;/strong&gt;&lt;br&gt;
Yes, and so do their limits. The &lt;code&gt;alloy convert&lt;/code&gt; command translates a Promtail &lt;code&gt;pipeline_stages&lt;/code&gt; block into the equivalent &lt;code&gt;stage.*&lt;/code&gt; blocks, replace stages included, so whatever your regex list caught before it catches now, and whatever it missed it still misses. The migration is a good moment to ask whether that list belongs in the collector at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I have to change my Alloy configuration to use the sidecar?&lt;/strong&gt;&lt;br&gt;
No. The sidecar writes clean lines to its own stdout, and the kubelet files them like any other container's output, so &lt;code&gt;discovery.kubernetes&lt;/code&gt; and &lt;code&gt;loki.source.kubernetes&lt;/code&gt; pick them up unchanged. That holds whether you run Alloy from the Grafana Helm chart, the k8s-monitoring chart, or your own DaemonSet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I still group by a user in LogQL after redaction?&lt;/strong&gt;&lt;br&gt;
Yes. Values are replaced with stable markers rather than deleted, so the line keeps its structure and LogQL parsers keep working. The marker is a salted HMAC, so the same user is the same marker on every line and across services sharing the salt — a distinct count is still a real distinct count, you just cannot read who it was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about the node and journald logs Alloy also collects?&lt;/strong&gt;&lt;br&gt;
The sidecar covers what your application containers write, nothing else. Systemd units, kubelet logs and anything you collect with &lt;code&gt;loki.source.journal&lt;/code&gt; never pass through it, so treat those with Alloy's own stages or keep them out of the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will it clean the logs already in Loki?&lt;/strong&gt;&lt;br&gt;
No. It sanitizes only what passes through it from the moment it is deployed. What is already ingested stays as it is, and the ways out are waiting for retention to expire or deleting the affected streams.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified on 9 September 2026: the Alloy configuration and the manifest above were run as printed — Grafana Alloy v1.19.2, Kubernetes v1.35 (kind), PII-Shield 2.2.3. Alloy collected the sidecar's output with no configuration change of its own, and every entry it received carried markers instead of values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop maintaining a pattern list inside your collector.&lt;/strong&gt; Check out the &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;PII-Shield repository on GitHub&lt;/a&gt; and drop a star if this simplifies your Alloy pipeline.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>observability</category>
    </item>
    <item>
      <title>Zero-code PII sanitization for Fluentd and Fluent Bit in Kubernetes</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:14:30 +0000</pubDate>
      <link>https://dev.to/aragossa/zero-code-pii-sanitization-for-fluentd-and-fluent-bit-in-kubernetes-1i1g</link>
      <guid>https://dev.to/aragossa/zero-code-pii-sanitization-for-fluentd-and-fluent-bit-in-kubernetes-1i1g</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; send app logs through a PII-Shield sidecar before Fluentd or Fluent Bit read them, then collect only masked identifiers like &lt;code&gt;[HIDDEN:a1b2c3]&lt;/code&gt; instead of maintaining regex filters that break the moment a new secret format shows up.&lt;/p&gt;

&lt;p&gt;Fluentd and its lighter C-based sibling &lt;strong&gt;Fluent Bit&lt;/strong&gt; are the most common unified logging layers in Kubernetes — CNCF graduated projects with a huge plugin ecosystem for routing logs to almost any backend (Elasticsearch, Loki, S3, Datadog, Splunk). That flexibility is exactly why they end up sitting directly in the path of every raw log line your applications write.&lt;/p&gt;

&lt;p&gt;If an application logs a user's email, an API key, or a card number, Fluentd/Fluent Bit will happily parse, buffer, and ship that value to wherever you've configured — permanently, and usually to more than one destination at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Standard Approach: Filter-Layer Masking
&lt;/h2&gt;

&lt;p&gt;Both projects expose a way to scrub fields in-flight. Fluentd uses &lt;code&gt;record_transformer&lt;/code&gt; with Ruby's &lt;code&gt;gsub&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;filter&lt;/span&gt; &lt;span class="err"&gt;app.**&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  @type record_transformer
  enable_ruby true
  &lt;span class="nt"&gt;&amp;lt;record&amp;gt;&lt;/span&gt;
    message ${record["message"].gsub(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/, '[REDACTED]')}
  &lt;span class="nt"&gt;&amp;lt;/record&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/filter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fluent Bit doesn't ship a built-in PII filter, so the common workaround is a &lt;code&gt;lua&lt;/code&gt; filter calling a hand-written script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[FILTER]&lt;/span&gt;
    &lt;span class="err"&gt;Name&lt;/span&gt;    &lt;span class="err"&gt;lua&lt;/span&gt;
    &lt;span class="err"&gt;Match&lt;/span&gt;   &lt;span class="err"&gt;*&lt;/span&gt;
    &lt;span class="err"&gt;script&lt;/span&gt;  &lt;span class="err"&gt;redact.lua&lt;/span&gt;
    &lt;span class="err"&gt;call&lt;/span&gt;    &lt;span class="err"&gt;redact_pii&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;redact_pii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;string.gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s2"&gt;"[%w.+-]+@[%w-]+%.[%w.-]+"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this approach breaks down at scale:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Regex Trap:&lt;/strong&gt; Every secret shape — API keys, session tokens, internal ID formats — needs its own pattern. Miss one and it ships in cleartext with no warning, because nothing fails; it just silently doesn't match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-Line Interpreter Overhead:&lt;/strong&gt; Fluent Bit's Lua filter runs an embedded Lua interpreter for every single record. On high-throughput pods this is a well-known source of added latency and CPU pressure on the node running the DaemonSet — worse than Fluentd's native Ruby path, but both add real per-line cost that scales with log volume, not with how much PII is actually present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss of Context:&lt;/strong&gt; A static &lt;code&gt;[REDACTED]&lt;/code&gt; destroys correlation. If ten log lines all become &lt;code&gt;[REDACTED]&lt;/code&gt;, you can no longer tell whether they're the same user hitting an error ten times or ten different users.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Zero-Code Alternative: PII-Shield Sidecar
&lt;/h2&gt;

&lt;p&gt;Instead of asking Fluentd's Ruby engine or Fluent Bit's Lua interpreter to parse and pattern-match every line, move sanitization to a dedicated, low-allocation Go sidecar that sits &lt;em&gt;before&lt;/em&gt; either one ever sees the log: &lt;strong&gt;PII-Shield&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What your app generated:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;level&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User authenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;john.doe@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// What Fluentd / Fluent Bit actually reads and ships:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;level&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User authenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[HIDDEN:e9f1a2]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[HIDDEN:a1c7af]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The App&lt;/strong&gt; redirects its output to an ephemeral shared volume (like an &lt;code&gt;emptyDir&lt;/code&gt;) instead of the main &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII-Shield (Sidecar)&lt;/strong&gt; tails that file, scrubs it using entropy-based secret detection (no regex list to maintain for API keys and tokens), and prints the &lt;em&gt;clean&lt;/em&gt; logs to its own &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fluentd or Fluent Bit&lt;/strong&gt; — already running as a DaemonSet reading &lt;code&gt;/var/log/containers/*.log&lt;/code&gt; — picks up the sidecar's &lt;code&gt;stdout&lt;/code&gt; the same way it picks up any other container's, no config change required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;record_transformer&lt;/code&gt; filter and the Lua script can both be deleted. Fluentd/Fluent Bit go back to doing what they do best — routing already-clean logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Implementation
&lt;/h2&gt;

&lt;p&gt;This is the same sidecar pattern regardless of whether your DaemonSet runs Fluentd or Fluent Bit — neither needs to know PII-Shield exists.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;pii-shield-secret&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pii-salt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace-with-a-long-random-value"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing&lt;/span&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;billing-app&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;billing-app:v2.1.0&lt;/span&gt;
      &lt;span class="c1"&gt;# The app writes its private output to a shared pipe/file&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;/bin/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;./billing-binary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;pii-shield-sidecar&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;thelisdeep/pii-shield:2.2.0&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PII_SALT&lt;/span&gt;
          &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretKeyRef&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;pii-shield-secret&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pii-salt&lt;/span&gt;
      &lt;span class="c1"&gt;# Scratch image: run the binary directly (no shell/tail). Reads, scrubs,&lt;/span&gt;
      &lt;span class="c1"&gt;# and outputs to stdout for Fluentd/Fluent Bit to pick up.&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;/pii-shield"&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;--watch-file"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;log-volume&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Pro tip: The &lt;code&gt;thelisdeep/pii-shield&lt;/code&gt; image is multi-arch (&lt;code&gt;amd64&lt;/code&gt;/&lt;code&gt;arm64&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify It's Actually Working
&lt;/h2&gt;

&lt;p&gt;Before trusting the pipeline, confirm the sidecar's own output is already clean — don't just check what lands in your log backend, since a misconfigured mount can make Fluentd/Fluent Bit silently fall back to the app container's raw stream instead.&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;# Read the sidecar's own stdout directly — this is what Fluentd/Fluent Bit sees&lt;/span&gt;
kubectl logs billing-service &lt;span class="nt"&gt;-c&lt;/span&gt; pii-shield-sidecar &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50

&lt;span class="c"&gt;# Confirm no raw emails/tokens survived&lt;/span&gt;
kubectl logs billing-service &lt;span class="nt"&gt;-c&lt;/span&gt; pii-shield-sidecar &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"@|Bearer|sk-"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;HIDDEN:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command should return nothing. If it does, that log line's format isn't being caught yet — file it as a scanner bug rather than assuming the pipeline is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;p&gt;PII-Shield doesn't replace input validation or make it safe to log secrets on purpose — it's a last line of defense for what slips through anyway. It also doesn't retroactively clean logs already sitting in Elasticsearch, Loki, or wherever Fluentd already shipped them; it only sanitizes what passes through the sidecar from the moment it's deployed. And it's stream-based: for the same reason described above, always check the sidecar's own output during rollout instead of assuming the mount and file path line up on the first try.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Stop maintaining regex filters for secrets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;PII-Shield repository on GitHub&lt;/a&gt; and drop a star if this simplifies your Fluentd or Fluent Bit pipeline!&lt;/p&gt;

</description>
      <category>fluentd</category>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>I fed a parody LinkedIn generator to three AI detectors. Gemini said it was human.</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/aragossa/i-fed-a-parody-linkedin-generator-to-three-ai-detectors-gemini-said-it-was-human-2l40</link>
      <guid>https://dev.to/aragossa/i-fed-a-parody-linkedin-generator-to-three-ai-detectors-gemini-said-it-was-human-2l40</guid>
      <description>&lt;p&gt;There's a site called &lt;a href="https://www.cringebot3000.com/" rel="noopener noreferrer"&gt;cringebot3000.com&lt;/a&gt; that generates deliberately terrible LinkedIn thought-leadership posts. You pick a topic and a cringe style ("The Vulnerability Post", "Proud to Announce"), it produces a story about a grandmother, a quirky German named Horst, or a cat that writes email subject lines. The footer says the posts are "guaranteed to make your followers shudder". It delivers.&lt;/p&gt;

&lt;p&gt;I maintain a small Claude skill called &lt;a href="https://github.com/aragossa/ai-tell-detector" rel="noopener noreferrer"&gt;ai-tell-detector&lt;/a&gt; that audits drafts for the patterns that make text read as AI-written. It exists because AI detectors once flagged a post I wrote myself, which was &lt;a href="https://dev.to/aragossa/i-deleted-my-own-linkedin-post-after-two-ai-detectors-flagged-it-1ilf"&gt;its own story&lt;/a&gt;. 24 checks, things like mirrored "not X, Y" contrasts, mic-drop closing lines, the story arc where every thread gets neatly resolved. CringeBot output should be the easiest possible target for it. So the interesting question wasn't whether my checklist would fire. It was what the big general models would say about the same texts, given that for once the ground truth is known: all three posts are 100% machine-written, by design, with a #cringebot3000 hashtag right in them (I stripped the hashtags before testing, to be fair).&lt;/p&gt;

&lt;p&gt;I generated three posts and asked ChatGPT, Grok and Gemini the same thing: does this read as AI-generated, give me a percentage and the top tells.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;ChatGPT&lt;/th&gt;
&lt;th&gt;Grok&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grandma flagged by an AI detector&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pierogi wisdom, shipping at 2am&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cat writes 847 email subject lines&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same texts. An 80-point spread on the first one.&lt;/p&gt;

&lt;p&gt;Gemini's verdict on the grandma post deserves a full quote: "Very high human storytelling cadence... natural comedic timing... the sharp takeaway at the end flows naturally from the specific story rather than sounding like a pre-baked moral." The takeaway it praised was produced by a template engine. ChatGPT looked at the exact same ending and called it "explicit moral... perfectly packaged thesis", 95% AI.&lt;/p&gt;

&lt;p&gt;ChatGPT also dropped one genuinely sharp observation: these posts don't sound like generic corporate AI writing, they sound like "AI instructed to imitate an unusually witty human storyteller". Which is exactly what CringeBot is.&lt;/p&gt;

&lt;p&gt;The checklist run was less dramatic but more useful. Post 1 tripped nine rules, posts 2 and 3 eight each. The part a percentage can never give you showed up across the posts rather than inside them: every single post has a side character with a carefully quirky name (Gerald, Horst, Biscuit). Grandmother wisdom appears in two of three. The number 94% appears in two of three, in completely unrelated contexts. The generator has favorite moves, and you only see them when you get a list of patterns instead of a confidence score. A score also can't tell you what to cut. A flagged line can.&lt;/p&gt;

&lt;p&gt;To be fair to all three models: this was target practice at point-blank range. CringeBot is built out of these patterns on purpose, so catching them proves little, and one funny mislabel doesn't make Gemini bad at everything. What the experiment does show is that an AI-likelihood percentage on a single text is a vibe with a number attached. Two of the three graders were confidently wrong in opposite directions about the same paragraph.&lt;/p&gt;

&lt;p&gt;The skill is MIT-licensed, English and Russian versions in the repo. I mostly use it on my own drafts before publishing, where the embarrassing discovery is usually that I write like a language model on my own, no generator needed.&lt;/p&gt;

&lt;p&gt;I still don't know what Gemini saw in Horst.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>showdev</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>I deleted my own LinkedIn post after two AI detectors flagged it</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:52:16 +0000</pubDate>
      <link>https://dev.to/aragossa/i-deleted-my-own-linkedin-post-after-two-ai-detectors-flagged-it-1ilf</link>
      <guid>https://dev.to/aragossa/i-deleted-my-own-linkedin-post-after-two-ai-detectors-flagged-it-1ilf</guid>
      <description>&lt;p&gt;Last month I published a LinkedIn post about a side project. It read fine to me. A browser&lt;br&gt;
detector put it around 60% AI. Gemini, asked to judge it the way Grammarly would, said 85-90%.&lt;/p&gt;

&lt;p&gt;I deleted it and rewrote the thing from scratch, keeping the same facts. The rewrite scored&lt;br&gt;
0-10% on Gemini and I felt pretty good for about an hour, until I fed the identical text to&lt;br&gt;
two other models. Grok landed somewhere around 65-75, and ChatGPT thought 70-85 was fair.&lt;/p&gt;

&lt;p&gt;Same words, a spread of roughly 70 points depending on who you ask.&lt;/p&gt;

&lt;p&gt;After that I mostly stopped caring what the detectors said, because human readers kept catching&lt;br&gt;
my drafts anyway, and they were more consistent than the tools: when I collected what people&lt;br&gt;
actually pointed at, it was the same short list of patterns every time. That list became a&lt;br&gt;
checklist I now run before publishing anything. It started at 7 rules. By the time I sat down&lt;br&gt;
to write this post it was at 21 (more on that number at the end).&lt;/p&gt;

&lt;p&gt;Some rules you already know: "it's not X, it's Y" contrasts, em-dash overload, "In today's&lt;br&gt;
world" openers. The ones that surprised me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gestured specificity.&lt;/strong&gt; Phrases like "a real number" or "an actual example" with no number&lt;br&gt;
and no example anywhere in the text. Reads as concrete on a skim, and is exactly as empty as&lt;br&gt;
"many companies struggle with this". My deleted post had two of these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The over-resolved arc&lt;/strong&gt; took me longest to see because no single sentence trips it. Label&lt;br&gt;
each paragraph with the one job it does: setup, numbers, cause, fix. If every paragraph does&lt;br&gt;
exactly one job, nothing overlaps and nothing dangles, that cleanliness is itself the tell.&lt;br&gt;
Off-the-cuff writing leaves threads hanging somewhere.&lt;/p&gt;

&lt;p&gt;Then there's the way numbers get reported. "A said 8%. B said 71%. C said 82%." Three data&lt;br&gt;
points in identical syntax read as manufactured evidence even when every number is real,&lt;br&gt;
which is why the detector scores at the top of this post don't line up in a neat row.&lt;/p&gt;

&lt;p&gt;And the closing question. Every draft I wrote wanted to end with "What's your experience?"&lt;br&gt;
Readers learned to see that as template long before I did. A post can also just stop.&lt;/p&gt;

&lt;p&gt;The worst thing the checklist ever caught wasn't stylistic though. I found two published&lt;br&gt;
comments from my own account, under other people's posts, that said some version of "this&lt;br&gt;
happened to me too." It hadn't. The drafts passed every style rule I had at the time, sounded&lt;br&gt;
perfectly human, and claimed a personal experience that didn't exist. That's not a tell,&lt;br&gt;
that's a lie, and a reader who spots it is right to write off everything else you say. It&lt;br&gt;
became its own rule, the only one where the fix is "ask the author whether this actually&lt;br&gt;
happened", and it's the rule I'd keep if I had to drop the rest.&lt;/p&gt;

&lt;p&gt;I packaged the checklist as a Claude skill so I stop having to remember it: drop it into&lt;br&gt;
&lt;code&gt;~/.claude/skills/&lt;/code&gt;, feed it a finished draft, get back a table of line → pattern → fix. Free&lt;br&gt;
on GitHub, English and Russian: &lt;a href="https://github.com/aragossa/ai-tell-detector" rel="noopener noreferrer"&gt;https://github.com/aragossa/ai-tell-detector&lt;/a&gt;. It's the one&lt;br&gt;
skill I give away from a paid pack of nine, because it's the one everybody seems to need. Fair&lt;br&gt;
warning from the skill's own hard rules: passing the checklist doesn't mean a detector will&lt;br&gt;
score you 0%, since detectors can't reliably tell "human draft, AI-assisted edit" from "fully&lt;br&gt;
generated" anyway. The goal is that a reader can't name the template you wrote from.&lt;/p&gt;

&lt;p&gt;About that "21". A draft of this post scored 44% on Grammarly, so I showed it to ChatGPT and&lt;br&gt;
Gemini and asked where a classifier would push back. They independently flagged the run of&lt;br&gt;
bolded term-definition blocks in the middle ("looks like a generated taxonomy"), a punchy&lt;br&gt;
"So X is noise. What isn't noise:" pivot I was honestly proud of, and the tidy caveat&lt;br&gt;
paragraph I'd slotted right before the ending. All three went into the checklist, which is&lt;br&gt;
now at 24, and I rewrote this post against the new rules before publishing. I doubt it's&lt;br&gt;
done. Readers, and apparently other models, keep finding new ways to catch me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>Zero-code PII sanitization for Grafana Loki and Promtail in Kubernetes</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:56:43 +0000</pubDate>
      <link>https://dev.to/aragossa/zero-code-pii-sanitization-for-grafana-loki-and-promtail-in-kubernetes-739</link>
      <guid>https://dev.to/aragossa/zero-code-pii-sanitization-for-grafana-loki-and-promtail-in-kubernetes-739</guid>
      <description>&lt;p&gt;If you use Grafana Loki for log aggregation, you probably rely on &lt;strong&gt;Promtail&lt;/strong&gt; (or the Grafana Agent) to scrape your Kubernetes pods and ship the logs. Loki is famously cost-effective because it only indexes metadata (labels), keeping the actual log text raw.&lt;/p&gt;

&lt;p&gt;However, this raw text architecture makes PII (Personally Identifiable Information) and secret leakage a critical issue. If your applications log user emails, credit cards, or API keys, that data is permanently stored in Loki chunk files (often in deeply integrated S3/GCS buckets) making it incredibly hard to comply with GDPR "Right to be Forgotten" requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Standard Promtail Approach: &lt;code&gt;pipeline_stages&lt;/code&gt; Regex
&lt;/h2&gt;

&lt;p&gt;The official Grafana way to handle sensitive data is using Promtail's &lt;code&gt;pipeline_stages&lt;/code&gt; to scrub data before it reaches Loki. A typical Promtail config looks like this:&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;pipeline_stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{app="my-service"}'&lt;/span&gt;
      &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(?P&amp;lt;email&amp;gt;[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
            &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(.*)'&lt;/span&gt;
            &lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[REDACTED]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this approach breaks down at scale:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Regex Trap:&lt;/strong&gt; You have to write and maintain complex regular expressions for every type of sensitive data (API keys, passwords, custom tokens). If a new format appears, your Promtail config is instantly outdated and secrets leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU and Latency:&lt;/strong&gt; Running heavily nested regex &lt;code&gt;pipeline_stages&lt;/code&gt; on high-throughput log streams puts massive CPU pressure on the node running Promtail. Promtail can quickly throttle or consume too much memory when evaluating hundreds of regex rules against every log line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss of Context:&lt;/strong&gt; When you replace a secret with &lt;code&gt;[REDACTED]&lt;/code&gt;, you lose the ability to track an entity. If you need to trace why a specific (but anonymous) user experienced 50 errors, you can't, because all users now just look like &lt;code&gt;[REDACTED]&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Zero-Code Alternative: PII-Shield Sidecar
&lt;/h2&gt;

&lt;p&gt;Instead of forcing Promtail to do the heavy lifting of parsing and regex matching, you can shift the responsibility to a dedicated, high-performance sidecar next to your application: &lt;strong&gt;PII-Shield&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PII-Shield intercepts the log stream &lt;em&gt;before&lt;/em&gt; Promtail even sees it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The App&lt;/strong&gt; writes logs to a shared volume file instead of &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII-Shield (Sidecar)&lt;/strong&gt; tails that file, scrubs it using zero-allocation Go routines and mathematical entropy detection (no regex needed for API keys!), and prints the &lt;em&gt;clean&lt;/em&gt; logs to its own &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promtail&lt;/strong&gt; natively scrapes the sidecar's &lt;code&gt;stdout&lt;/code&gt; just like any other container.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Promtail needs &lt;strong&gt;zero configuration changes&lt;/strong&gt;. It just blindly ships the logs to Loki, completely unaware that the heavy sanitization has already occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes Implementation
&lt;/h3&gt;

&lt;p&gt;Here is how you configure the pod natively. Note that Promtail, by default, reads from &lt;code&gt;/var/log/containers/*.log&lt;/code&gt; via Kubernetes Service Discovery. It will automatically find the &lt;code&gt;pii-shield-sidecar&lt;/code&gt; output.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing&lt;/span&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;billing-app&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;billing-app:v2.1.0&lt;/span&gt;
      &lt;span class="c1"&gt;# The app writes its private output to a shared pipe/file&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;/bin/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;./billing-binary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;pii-shield-sidecar&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;thelisdeep/pii-shield:2.2.0&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PII_SALT&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grafana-secure-salt"&lt;/span&gt;
      &lt;span class="c1"&gt;# PII-Shield reads, scrubs, and outputs to stdout for Promtail to pick up&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;/bin/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;tail&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-n&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/var/run/logs/app.log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pii-shield"&lt;/span&gt;&lt;span class="pi"&gt;]&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;log-volume&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;/var/run/logs&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;log-volume&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Pro tip: The &lt;code&gt;thelisdeep/pii-shield&lt;/code&gt; image is multi-arch (&lt;code&gt;amd64&lt;/code&gt;/&lt;code&gt;arm64&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is the ultimate Loki stack upgrade:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Promtail Pipelines:&lt;/strong&gt; You can delete hundreds of lines of brittle &lt;code&gt;pipeline_stages&lt;/code&gt; from your Promtail DaemonSet. Promtail goes back to doing what it does best: shipping logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Hashing:&lt;/strong&gt; PII-Shield replaces secrets with a stable HMAC using the &lt;code&gt;PII_SALT&lt;/code&gt;, (e.g., &lt;code&gt;[HIDDEN:e9f1a2]&lt;/code&gt;). In Loki's LogQL, you can now trace an exact user workflow &lt;code&gt;|= "[HIDDEN:e9f1a2]"&lt;/code&gt; across microservices without actually knowing their email or token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Entropy Detection:&lt;/strong&gt; Unlike Promtail regexes, PII-Shield mathematically calculates Shannon Entropy. It will automatically detect a newly issued 64-character API key it has never seen before and mask it, preventing 0-day log leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Micro-footprint:&lt;/strong&gt; the default Helm chart caps it at a 30Mi memory limit, making it cheap to run as a sidecar.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Protect your Grafana Loki data retention today.&lt;/strong&gt; &lt;br&gt;
Check out the &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;PII-Shield repository on GitHub&lt;/a&gt; and drop a star if this simplifies your Kubernetes logging!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>observability</category>
    </item>
    <item>
      <title>PII-Shield: Cleaning PII From Logs Before It Reaches ELK</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Wed, 10 Jun 2026 04:52:48 +0000</pubDate>
      <link>https://dev.to/aragossa/pii-shield-cleaning-pii-from-logs-before-it-reaches-elk-534j</link>
      <guid>https://dev.to/aragossa/pii-shield-cleaning-pii-from-logs-before-it-reaches-elk-534j</guid>
      <description>&lt;p&gt;The first idea was simple.&lt;/p&gt;

&lt;p&gt;Take a log line. Look at suspicious parts. Count entropy. Hide anything that looks like a random secret.&lt;/p&gt;

&lt;p&gt;PII means personally identifiable information. It includes emails, phone numbers, addresses, passport numbers, card numbers, access tokens, and other values that should not move freely through logs.&lt;/p&gt;

&lt;p&gt;At first, entropy looked like a good signal. Many tokens, keys, and session values really look like noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x9VdQp2Mz_La77kPq0
sk_live_51Nx...
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But entropy alone was not enough.&lt;/p&gt;

&lt;p&gt;Some values have low entropy and still must be hidden. For example: &lt;code&gt;password=123&lt;/code&gt;, &lt;code&gt;token=dev&lt;/code&gt;, &lt;code&gt;cvv=000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Other values look random, but they are not secrets. Trace IDs, UUIDs, short commit hashes, request IDs, and path fragments can all look suspicious.&lt;/p&gt;

&lt;p&gt;If the entropy threshold is too low, the filter breaks useful logs. If the threshold is too high, it misses weak secrets.&lt;/p&gt;

&lt;p&gt;That is why PII-Shield grew beyond entropy. It added regex rules, sensitive keys, allow lists, and separate validators like the Luhn algorithm for payment card numbers.&lt;/p&gt;

&lt;p&gt;I also did not like where PII is often cleaned today.&lt;/p&gt;

&lt;p&gt;Many teams clean logs at the Fluentd, Logstash, SIEM, or log pipeline level. This helps. But it is late. The raw data has already left the application. It may have passed through buffers, retries, temporary files, alerts, and dashboards.&lt;/p&gt;

&lt;p&gt;PII-Shield tries to clean the data earlier. It is an open-source tool that removes PII and secrets from logs before they leave the pod.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pii-shield/pii-shield" rel="noopener noreferrer"&gt;https://github.com/pii-shield/pii-shield&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic Idea
&lt;/h2&gt;

&lt;p&gt;The short version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application writes a log
        |
        v
PII-Shield reads the raw log near the app
        |
        v
only the cleaned line goes out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not "clean it later". The goal is "do not let the raw value leave".&lt;/p&gt;

&lt;p&gt;PII-Shield can be used in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a CLI tool or container that filters standard input and output;&lt;/li&gt;
&lt;li&gt;a sidecar container in Kubernetes;&lt;/li&gt;
&lt;li&gt;a Kubernetes operator that injects the sidecar when a pod is created;&lt;/li&gt;
&lt;li&gt;Helm charts for installation;&lt;/li&gt;
&lt;li&gt;WASM SDKs for Node.js and Python, if you want to run the scanner inside the process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main Kubernetes path works like this. The application writes logs to a file on a shared volume. The sidecar reads that file. It scans each line. Then it writes the cleaned stream to standard output. A normal log collector can read it from there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────── pod ────────────────────┐
│                                             │
│  app container                              │
│      │                                      │
│      │ /var/log/app/output.log             │
│      v                                      │
│  shared emptyDir volume                     │
│      │                                      │
│      v                                      │
│  pii-shield sidecar -&amp;gt; sanitized stdout     │
│                                             │
└─────────────────────────────────────────────┘
                      │
                      v
              Loki / ELK / S3 / SIEM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not invisible interception of everything. The application must write to a known file. But this path is easy to test. It does not need a shell inside the sidecar image. It also does not change the application runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Counts As Sensitive
&lt;/h2&gt;

&lt;p&gt;The scanner does not have one magic button for "find all private data". It uses several layers.&lt;/p&gt;

&lt;p&gt;The first layer is sensitive keys. If a line has &lt;code&gt;password=...&lt;/code&gt;, &lt;code&gt;token=...&lt;/code&gt;, &lt;code&gt;secret=...&lt;/code&gt;, or &lt;code&gt;api_key=...&lt;/code&gt;, the value near that key should be hidden.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:  payment failed token=sk_live_51Nx...
output: payment failed token=[HIDDEN:9b22c1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second layer is custom regex rules.&lt;/p&gt;

&lt;p&gt;Many companies have their own internal IDs. These can be ticket numbers, policy IDs, customer IDs, medical record numbers, legal case numbers, or contract numbers.&lt;/p&gt;

&lt;p&gt;These values are hard to guess from general signals. It is better to say it directly:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PII_CUSTOM_REGEX_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[
  {"pattern": "^MRN-[0-9]{8}$", "name": "MedicalRecord"},
  {"pattern": "^CASE-[0-9]{4}-[0-9]{6}$", "name": "CaseNumber"}
]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a performance detail here.&lt;/p&gt;

&lt;p&gt;If a user adds ten rules, and the scanner runs ten separate regex checks on each token, line processing can become heavier. So the rules are first checked one by one when the config is loaded. Then they are joined into one larger regexp with &lt;code&gt;|&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each rule is wrapped in a group. This lets the scanner know which name to put into the redaction marker.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(^MRN-[0-9]{8}$)|(^CASE-[0-9]{4}-[0-9]{6}$)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code this is stored as &lt;code&gt;CombinedCustomRegex&lt;/code&gt;. The separate compiled rules are still kept in the config. But the main path uses the combined regexp.&lt;/p&gt;

&lt;p&gt;This does not promise a speed win for every possible rule set. Regex performance depends on the rules. But it removes the need to try each custom regex one after another on every token.&lt;/p&gt;

&lt;p&gt;The third layer is entropy.&lt;/p&gt;

&lt;p&gt;Many secrets look like random text. API keys, session tokens, and random passwords are common examples. PII-Shield uses Shannon entropy for this. If a token is long enough and random enough, the scanner treats it as suspicious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:  Authorization failed: x9VdQp2Mz_La77kPq0
output: Authorization failed: [HIDDEN:3e12aa]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But entropy can fight with real logs. Commit hashes, UUIDs, trace IDs, request IDs, and file paths can also look suspicious. So there is an allow list:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PII_SAFE_REGEX_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[
  {"pattern": "^[a-f0-9]{7}$", "name": "GitShortSHA"}
]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allow list runs before other checks. If the rule is too broad, it can allow a value that should be hidden. This is not a bug in the idea. It is the cost of manual tuning.&lt;/p&gt;

&lt;p&gt;The fourth layer is validation for specific data types.&lt;/p&gt;

&lt;p&gt;Payment card numbers are checked with the Luhn algorithm. This is not just a regex for 16 digits. The scanner looks for digit sequences from 13 to 19 digits. It checks boundaries. It drops weak digit sets. Then it checks the Luhn checksum.&lt;/p&gt;

&lt;p&gt;There is also a context check to reduce false positives.&lt;/p&gt;

&lt;p&gt;A number can pass Luhn and still be just a trace value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TraceId=4556737586899855
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should not always be treated as a card.&lt;/p&gt;

&lt;p&gt;But this line has card context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;visa card 4556737586899855 provided
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one should be hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use A Hash Instead Of &lt;code&gt;[REDACTED]&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If every secret becomes &lt;code&gt;[REDACTED]&lt;/code&gt;, debugging gets harder.&lt;/p&gt;

&lt;p&gt;Sometimes you need to know that ten errors all had the same token or the same user ID. You still should not see the raw value.&lt;/p&gt;

&lt;p&gt;So PII-Shield replaces a sensitive value with a short marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HIDDEN:a1b2c3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marker is based on a salt. If &lt;code&gt;PII_SALT&lt;/code&gt; is stable, the same raw value gets the same marker. QA and SRE teams can compare events across logs.&lt;/p&gt;

&lt;p&gt;If the salt is random on every start, this link is lost after a restart.&lt;/p&gt;

&lt;p&gt;For production, the salt should come from a secret:&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;env&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;PII_SALT&lt;/span&gt;
    &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;secretKeyRef&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;pii-shield-secrets&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;salt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also better to require a strong salt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PII_REQUIRE_STRONG_SALT=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Test Without Kubernetes
&lt;/h2&gt;

&lt;p&gt;The fastest way to try the scanner is through the container:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'login failed email=ivan@example.com password=MySecretPass123!'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | docker run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/pii-shield/pii-shield:2.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login failed email=[HIDDEN:...] password=[HIDDEN:...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact hash suffix depends on the salt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes: Operator And Policy
&lt;/h2&gt;

&lt;p&gt;For Kubernetes, there is an operator. It can be installed with Helm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add pii-shield https://pii-shield.github.io/pii-shield/
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;pii-shield-operator pii-shield/pii-shield-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; operator-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you create a &lt;code&gt;PiiPolicy&lt;/code&gt;:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;core.pii-shield.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PiiPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;strict-policy&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;injectionMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
  &lt;span class="na"&gt;logPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/app/output.log&lt;/span&gt;
  &lt;span class="na"&gt;failPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you mark the deployment:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;billing-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pii-shield.io/inject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pii-shield.io/policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict-policy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook adds the sidecar, the volume, and the settings. The application writes to &lt;code&gt;/var/log/app/output.log&lt;/code&gt;. The sidecar reads this file and prints the cleaned stream.&lt;/p&gt;

&lt;p&gt;The Helm chart has small resource limits: 30Mi of memory and 50m CPU for the sidecar. You still need to measure this on your own logs. This matters when logs contain a lot of JSON or very long lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why The Scanner Does Not Use A Heavy JSON Parser
&lt;/h2&gt;

&lt;p&gt;Logs often come as JSON. But using &lt;code&gt;encoding/json&lt;/code&gt; for every line would add overhead during constant stream processing.&lt;/p&gt;

&lt;p&gt;The scanner only needs to find values and keep the structure valid enough for redaction. So it uses a narrower parser. It can handle JSON-like text for this task, but it does not turn every line into a full object tree.&lt;/p&gt;

&lt;p&gt;This does not make the code prettier. It does reduce allocations and surprises under load.&lt;/p&gt;

&lt;p&gt;There are tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nested JSON;&lt;/li&gt;
&lt;li&gt;broken lines;&lt;/li&gt;
&lt;li&gt;binary garbage;&lt;/li&gt;
&lt;li&gt;multilingual logs;&lt;/li&gt;
&lt;li&gt;false positives;&lt;/li&gt;
&lt;li&gt;custom regex rules;&lt;/li&gt;
&lt;li&gt;fuzz regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For scanner-only benchmarks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-benchmem&lt;/span&gt; ./pkg/scanner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For end-to-end CLI throughput:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./benchmark/run_benchmarks.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the current project notes, the normal Go scanner was in the microsecond range per line on a synthetic corpus.&lt;/p&gt;

&lt;p&gt;A neural PII detector on CPU was about three orders of magnitude slower in my test. So a model layer should not run on every line by default. If it is added, it should be an explicit mode. It may make sense for medical logs, legal logs, support chats, and similar domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail Open Or Fail Closed
&lt;/h2&gt;

&lt;p&gt;A log filter has an unpleasant choice. What should happen if the scanner fails?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fail open&lt;/code&gt; means the line is allowed to pass. Logs keep flowing. The application and monitoring do not go blind. But there is a risk that a raw value escapes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fail closed&lt;/code&gt; means the raw line is not released. The filter returns a drop marker instead. This is safer for privacy. It is worse for debugging.&lt;/p&gt;

&lt;p&gt;PII-Shield makes this configurable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PII_FAIL_POLICY=open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PII_FAIL_POLICY=closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is &lt;code&gt;open&lt;/code&gt;. Losing logs in production can become a separate incident. For strict compliance workloads, the right choice may be different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Limits
&lt;/h2&gt;

&lt;p&gt;I do not want to hide this part at the end of the README.&lt;/p&gt;

&lt;p&gt;PII-Shield can be run and tested now. But not all modes have the same maturity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file-based sidecar mode is the main practical path;&lt;/li&gt;
&lt;li&gt;fully transparent protection for normal Kubernetes &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; logs is not production-ready yet;&lt;/li&gt;
&lt;li&gt;pipe mode changes the target container command and needs testing per workload;&lt;/li&gt;
&lt;li&gt;the Kubernetes operator is still being stabilized;&lt;/li&gt;
&lt;li&gt;eBPF mode is research work, not a production compliance control;&lt;/li&gt;
&lt;li&gt;Proxy-Wasm gateway integration and a visual control plane are still planned work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This part is boring, but it matters. A privacy tool without clear limits quickly becomes a decorative shield.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Hard
&lt;/h2&gt;

&lt;p&gt;Not regex. Not Helm.&lt;/p&gt;

&lt;p&gt;The hard part was not breaking normal logs.&lt;/p&gt;

&lt;p&gt;If the scanner is too aggressive, it hides trace IDs, commit hashes, harmless UUIDs, and path fragments. If it is too relaxed, it misses weak passwords and handmade tokens.&lt;/p&gt;

&lt;p&gt;One global threshold does not fit every language and every domain. And an allow list can create its own problems if it is too broad.&lt;/p&gt;

&lt;p&gt;So the project is not moving toward one smart detector. It is moving toward several clear layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sensitive keys;&lt;/li&gt;
&lt;li&gt;strict custom rules;&lt;/li&gt;
&lt;li&gt;entropy with tuning;&lt;/li&gt;
&lt;li&gt;allow lists;&lt;/li&gt;
&lt;li&gt;profiles for different domains;&lt;/li&gt;
&lt;li&gt;maybe an optional model check where the extra latency is worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less magic. More testable rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Helps
&lt;/h2&gt;

&lt;p&gt;PII-Shield does not replace logging discipline.&lt;/p&gt;

&lt;p&gt;Developers still should not write passwords into &lt;code&gt;log.info(...)&lt;/code&gt;. APIs still need to validate input. Security teams still need to check retention, access, and log export.&lt;/p&gt;

&lt;p&gt;But a filter near the application helps with one common mistake: a sensitive value enters the shared log stream by accident.&lt;/p&gt;

&lt;p&gt;This matters when logs move into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;central log storage;&lt;/li&gt;
&lt;li&gt;a data lake;&lt;/li&gt;
&lt;li&gt;incident analysis tools;&lt;/li&gt;
&lt;li&gt;analytics and reporting;&lt;/li&gt;
&lt;li&gt;LLM/RAG pipelines;&lt;/li&gt;
&lt;li&gt;AI agents that read traces and evidence chains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point was important for me. If PII gets into a training or evaluation dataset, removing one line from Loki is no longer enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;The next work areas are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production support for Kubernetes stdout and stderr;&lt;/li&gt;
&lt;li&gt;a more stable operator lifecycle;&lt;/li&gt;
&lt;li&gt;stronger release checks: checksums, image digests, and artifact provenance;&lt;/li&gt;
&lt;li&gt;domain-specific profiles;&lt;/li&gt;
&lt;li&gt;selective model checks without a large latency hit;&lt;/li&gt;
&lt;li&gt;Proxy-Wasm and eBPF work only where they give a real benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PII-Shield is not a universal cure. It is a practical layer of protection in a place that is often not controlled well: between the application and the log system.&lt;/p&gt;

&lt;p&gt;If there is one idea to take from this article, it is this: clean private data as close as possible to the place where it appears. Everything after that is harder to check and harder to undo.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>go</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Masking PII in Kubernetes: How we solved 3 annoying sidecar edge cases (v2.0.0)</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Wed, 29 Apr 2026 18:15:20 +0000</pubDate>
      <link>https://dev.to/aragossa/masking-pii-in-kubernetes-how-we-solved-3-annoying-sidecar-edge-cases-v200-3lal</link>
      <guid>https://dev.to/aragossa/masking-pii-in-kubernetes-how-we-solved-3-annoying-sidecar-edge-cases-v200-3lal</guid>
      <description>&lt;p&gt;Building a mutating webhook for Kubernetes is easy in tutorials, but brutal in production. You immediately hit the reality of volume permissions, security contexts, and zombie sidecars.&lt;/p&gt;

&lt;p&gt;I recently released v2.0.0 of the &lt;strong&gt;PII-Shield Operator&lt;/strong&gt;. It’s a Go-based tool that injects a sidecar into your pods to mask sensitive data (PII) before the logs hit Datadog or ELK.&lt;/p&gt;

&lt;p&gt;Getting the core Shannon entropy logic to work was step one. Making it bulletproof for strict SOC2 environments was step two. Here are the three K8s edge cases we solved for this release:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dropping the Shell (Moving to Distroless)&lt;/strong&gt;&lt;br&gt;
Security teams hate sidecars with shell access. In earlier versions, we used Alpine. Now, the agent is compiled with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; and deployed on &lt;code&gt;gcr.io/distroless/static:nonroot&lt;/code&gt;. There is no &lt;code&gt;/bin/sh&lt;/code&gt; and zero attack surface. It just tails the log files directly using native Go.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "Immortal Sidecar" Problem&lt;/strong&gt;&lt;br&gt;
If you have ever injected sidecars into a K8s &lt;code&gt;Job&lt;/code&gt;, you know it breaks the lifecycle. The main container finishes its work, but the sidecar keeps tailing logs forever, so the Job never reaches the &lt;code&gt;Completed&lt;/code&gt; state.&lt;br&gt;
To fix this, we moved to the new &lt;strong&gt;Native Sidecars&lt;/strong&gt; feature (K8s 1.28+). The webhook now puts the agent inside the &lt;code&gt;initContainers&lt;/code&gt; array with &lt;code&gt;RestartPolicy: Always&lt;/code&gt;. Kubernetes finally understands how to kill the sidecar gracefully when the main app exits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The emptyDir Permission Trap&lt;/strong&gt;&lt;br&gt;
When the webhook mounts an &lt;code&gt;emptyDir&lt;/code&gt; volume to share logs, you often get &lt;code&gt;Permission Denied&lt;/code&gt;. If the user's main app runs as &lt;code&gt;root&lt;/code&gt; with a strict &lt;code&gt;umask 0077&lt;/code&gt;, the &lt;code&gt;nonroot&lt;/code&gt; sidecar can't read the file.&lt;br&gt;
Instead of forcing users to rewrite their manifests, the Mutating Webhook now handles it silently. It checks the pod's SecurityContext and automatically injects &lt;code&gt;fsGroup: 65532&lt;/code&gt;. The volume permissions match up, and the logs flow without errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I packaged the whole thing into a Helm chart so it takes about two minutes to test. You just label your pod with &lt;code&gt;pii-shield.io/inject: "true"&lt;/code&gt; and the webhook handles the rest.&lt;/p&gt;

</description>
      <category>go</category>
      <category>kubernetes</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to mask PII in Kubernetes before sending logs to Datadog</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Sun, 15 Mar 2026 23:05:58 +0000</pubDate>
      <link>https://dev.to/aragossa/how-to-mask-pii-in-kubernetes-before-sending-logs-to-datadog-3b4l</link>
      <guid>https://dev.to/aragossa/how-to-mask-pii-in-kubernetes-before-sending-logs-to-datadog-3b4l</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Datadog Bills and GDPR Nightmares
&lt;/h2&gt;

&lt;p&gt;If you are running applications in Kubernetes and shipping your logs to Datadog, you have probably faced two major headaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; Datadog charges by the volume of logs ingested and indexed. Every megabyte counts. And while Datadog offers a built-in Sensitive Data Scanner, it is a premium feature billed separately on top of your base log costs. A free, open-source sidecar lets you bypass the vendor-side premium scrubber entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance:&lt;/strong&gt; Sending Personally Identifiable Information (PII) like emails, credit card numbers, or API keys to a third-party logging service often violates GDPR and other privacy laws.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more comprehensive your logs are for debugging, the higher your Datadog bill gets, and the bigger your risk of a privacy breach becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Standard Approach (And Why It Hurts)
&lt;/h2&gt;

&lt;p&gt;The usual fix is to configure the Datadog Agent to mask or scrub PII before it leaves your cluster. That approach has real drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complexity:&lt;/strong&gt; Custom parsing rules, regexes, and pipelines in the Agent config are tedious to set up and maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU cost at the collector:&lt;/strong&gt; Running heavy regex over large log volumes inside your shipper burns CPU on every node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whack-a-Mole:&lt;/strong&gt; You keep updating rules as your application output changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: PII-Shield as a Lightweight Sidecar
&lt;/h2&gt;

&lt;p&gt;Instead of loading your cluster-wide log shipper with heavy processing, mask PII before it even leaves the pod.&lt;/p&gt;

&lt;p&gt;PII-Shield is a small, dependency-free tool written in Go. It runs as a sidecar container next to your application, reads the app's log stream, redacts secrets and PII using entropy-based detection plus deterministic hashing, and writes the clean stream to its own stdout. By the time the Datadog Agent picks the logs up from the node, they are already sanitized — in-pod, before egress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready-to-Use Pod Configuration
&lt;/h2&gt;

&lt;p&gt;The app writes its logs to a shared volume; PII-Shield watches that file and emits a sanitized stream on its own stdout. The PII-Shield image is a scratch-based static binary, so it is started directly with &lt;code&gt;--watch-file&lt;/code&gt; — no shell, no &lt;code&gt;tail&lt;/code&gt;.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;my-app-with-pii-shield&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Drop the raw app container's logs to prevent duplicates&lt;/span&gt;
    &lt;span class="na"&gt;ad.datadoghq.com/my-app.logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[{"source":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"my-app",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"service":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"billing",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"log_processing_rules":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[{"type":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"exclude_at_match",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"name":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"exclude_all",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"pattern":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;".*"}]}]'&lt;/span&gt;
    &lt;span class="c1"&gt;# Collect only the clean stream from the sidecar&lt;/span&gt;
    &lt;span class="na"&gt;ad.datadoghq.com/pii-shield-sidecar.logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[{"source":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"pii-shield",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"service":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"billing"}]'&lt;/span&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;my-app&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;my-app-image:v1.0.0&lt;/span&gt;
      &lt;span class="c1"&gt;# The app writes to a shared file instead of stdout&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;/bin/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;./my-app-binary&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/shared-logs/app.log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="pi"&gt;]&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;shared-logs&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;/shared-logs&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;pii-shield-sidecar&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;thelisdeep/pii-shield:2.1.1&lt;/span&gt;
      &lt;span class="c1"&gt;# Scratch image: run the binary directly, no shell/tail needed&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;/pii-shield"&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;--watch-file"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/shared-logs/app.log"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PII_SALT&lt;/span&gt;
          &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretKeyRef&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;pii-shield-secret&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pii-salt&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;shared-logs&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;/shared-logs&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;shared-logs&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; PII-Shield publishes multi-arch images (amd64 and arm64), so this works as-is on ARM nodes like AWS Graviton.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How does Datadog know what to read?
&lt;/h3&gt;

&lt;p&gt;The main application redirects its output to a file, so its own stdout is empty. The Datadog Agent, which listens to container stdout via Autodiscovery, then picks up only the clean stream from the &lt;code&gt;pii-shield-sidecar&lt;/code&gt;. No conflicts, no duplicate logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is better
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero configuration for log shippers:&lt;/strong&gt; Datadog just receives clean logs — no pipeline rules to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bypass premium vendor fees:&lt;/strong&gt; Datadog's Sensitive Data Scanner is billed on top of your log volume. An open-source in-pod sidecar removes the need for vendor-side scrubbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light footprint on a hot path:&lt;/strong&gt; the scan loop is a low-allocation Go hot path on a scratch-based image, and the chart ships it under a 30Mi memory / 50m CPU limit — so a sidecar in every pod stays cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggable redaction:&lt;/strong&gt; with deterministic hashing, &lt;code&gt;user@email.com&lt;/code&gt; becomes something like &lt;code&gt;[HIDDEN:a1b2c3]&lt;/code&gt;. The same input always maps to the same token for a given salt, so you can still correlate a user across logs without ever seeing the raw value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By putting the shield where the data is generated, you protect your users' privacy and keep your observability bill in check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to secure your Kubernetes logs?
&lt;/h2&gt;

&lt;p&gt;PII-Shield is open source (Apache-2.0). Check out the repository on GitHub, try the Helm chart, and if it helps, consider dropping a star.&lt;/p&gt;

</description>
      <category>datalog</category>
      <category>kubernetes</category>
      <category>log</category>
    </item>
    <item>
      <title>Integrating PII-Shield into GuardSpine (WASM vs Native execution)</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Wed, 25 Feb 2026 05:39:27 +0000</pubDate>
      <link>https://dev.to/aragossa/integrating-pii-shield-into-guardspine-wasm-vs-native-execution-1m4e</link>
      <guid>https://dev.to/aragossa/integrating-pii-shield-into-guardspine-wasm-vs-native-execution-1m4e</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/DNYoussef/codeguard-action" rel="noopener noreferrer"&gt;GuardSpine's&lt;/a&gt; main job is AI-aware code governance—using AI models to automatically review pull requests and create cryptographic proof of those reviews. However, because GuardSpine sends code to third-party AI models (like Claude or GPT) to be reviewed, there is a massive risk. Sensitive information—like passwords and personal data (PII)—must be stopped from leaking to these AI providers. You cannot let personal data poison third-party AI models.&lt;/p&gt;

&lt;p&gt;To solve this, the GuardSpine team turned to &lt;a href="https://github.com/aragossa/pii-shield" rel="noopener noreferrer"&gt;PII-Shield&lt;/a&gt;: a lightning-fast, open-source Go engine designed to find and redact sensitive data. The big engineering challenge was figuring out how to integrate this powerful external Go dependency into GuardSpine's strictly Python-based ecosystem. We had to make sure the integration was 100% secure (no data could accidentally leak to the internet) and extremely fast.&lt;/p&gt;

&lt;p&gt;We had to make a choice. Should we run the Go tool as a normal program using Python's &lt;code&gt;subprocess.run&lt;/code&gt;? Should we deploy a local server? Or, should we try something newer and run the Go code straight inside Python using WebAssembly (WASM)? This article explains how we tested normal programs against WebAssembly to adapt PII-Shield for this AI pipeline, the problems we found with slow startup times, and when you should use each method.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. WebAssembly (WASI) Integration: The Sandboxed Approach
&lt;/h2&gt;

&lt;p&gt;WebAssembly (WASM) is great for adding tools into Python. Its biggest benefit is that it works anywhere. Instead of building different versions of the tool for Linux, Mac, and Windows, we only need to build one &lt;code&gt;.wasm&lt;/code&gt; file. Any computer running &lt;code&gt;wasmtime&lt;/code&gt; can use it. Also, WASM is very secure because it runs in a "sandbox." This means the Go tool is locked inside and cannot access your network or your hard drive.&lt;/p&gt;

&lt;p&gt;To make this work, we built a Python script (&lt;code&gt;pii_wasm_client.py&lt;/code&gt;). When we need to hide data, this script starts the WASM engine. It passes important settings (like the secret &lt;code&gt;PII_SALT&lt;/code&gt; password) safely using environment variables. Then, it uses temporary files to send the data into the WASM tool and get the clean text back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjgwbjbstaa9xbhjaaejb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjgwbjbstaa9xbhjaaejb.png" alt=" " width="637" height="474"&gt;&lt;/a&gt;&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;wasmtime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Setup the Engine (Turn on the computer)
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Load the Code (Load the program)
&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pii-shield-wasi.wasm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Setup the Rules (Configure WASI)
&lt;/span&gt;&lt;span class="n"&gt;linker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;linker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define_wasi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Create a Safe Workspace (Create a Store)
&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;wasi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WasiConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;wasi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inherit_stdout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_wasi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wasi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Run the Program
&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;wasmtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExitTrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt; &lt;span class="c1"&gt;# Expected exit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we explain this to business leaders, they usually ask these important security questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why not just use a paid API on the internet?"&lt;/strong&gt; Because the WASM tool runs locally on your machine, your code never leaves your private network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do you keep the secret salt (PII_SALT) safe?"&lt;/strong&gt; The secret is passed directly into the locked WASM sandbox. Because the sandbox is blocked off, nothing outside of it can steal the secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why not just use simple search patterns (Regex)?"&lt;/strong&gt; Simple search patterns are fast but easily break on weird data, often flagging safe text as secrets by mistake (false positives). PII-Shield uses smart tokenization and entropy scoring to find real secrets, which requires a strong Go engine, not just simple text matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Real-World Benchmarks &amp;amp; The Bottlenecks
&lt;/h2&gt;

&lt;p&gt;When we started, we thought running the WASM tool directly inside Python would be faster than asking the computer (OS) to start a whole new external program (&lt;code&gt;subprocess.run&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;To test this, we built a script to run the tool 10,000 times (you can find the open-source &lt;code&gt;benchmark.py&lt;/code&gt; script in this public &lt;a href="https://gist.github.com/aragossa/68f19a73a982a49db0596857d30a6055" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;). The text we tested was very small: just a 100-character JSON string. We compared a normal Linux program against the WASM engine.&lt;/p&gt;

&lt;p&gt;The results were surprising. In our test with the tiny text, the normal program was about 4.7x faster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normal Program (&lt;code&gt;subprocess.run&lt;/code&gt;): ~0.86 ms per run.&lt;/li&gt;
&lt;li&gt;Starting and running the WASM Engine: ~4.03 ms per run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when scaled to 100,000 iterations, the per-execution latency remained rock-solid (0.85ms vs 4.05ms). The execution time scaled perfectly linearly, proving that while WASM carries a strict 4ms "cold start" tax, it introduces absolutely no memory leaks or degraded performance over time—a crucial metric for GuardSpine's stability.&lt;/p&gt;

&lt;p&gt;But saying "WASM is slower" is not fair. WebAssembly code is actually very fast. The problem was the "Cold Start"—how long it takes to turn the tool on. We found three big slowdowns:&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;Starting Go is Slow&lt;/strong&gt;. Every time we ran the WASM tool, the Go language had to start up its memory manager and background tasks inside the sandbox. When you run a normal program, your computer’s Operating System does this almost instantly. In WASM, we had to wait for it every single time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpl6xmpxn0a3iboumdq2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpl6xmpxn0a3iboumdq2n.png" alt=" " width="637" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;The File Speed Problem (I/O)&lt;/strong&gt;. To send text into the WASM tool, our Python script created temporary files on the hard drive. Creating, writing, and deleting files takes a lot of time and caused about 30-50% of the delay.&lt;/p&gt;

&lt;p&gt;Finally, &lt;strong&gt;The Text Was Too Small&lt;/strong&gt;. Because our test text was only 100 characters, 99% of the time was spent just turning the tool on and managing files. The actual work of finding the secrets took almost zero time. If we tested a massive 10MB file instead, the 4x speed difference would likely disappear because the real work (finding secrets) would take much longer than the startup time.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Unlocking WASM's True Potential
&lt;/h2&gt;

&lt;p&gt;Even though the "Cold Start" is slow, we shouldn't give up on WASM's amazing security and portability. To make WASM lightning-fast, we need to change how we run it:&lt;/p&gt;

&lt;p&gt;The easiest fix is &lt;strong&gt;Keeping It Running&lt;/strong&gt;. Our test turned the WASM tool on and off 10,000 times. If we turn it on just once when the server starts, we never have to wait for the Go startup delay again. WASM would be just as fast as a normal program.&lt;/p&gt;

&lt;p&gt;Next, we must fix the slow files by using &lt;strong&gt;In-Memory Pipes&lt;/strong&gt;. Instead of writing data to real files on the hard drive, we can push the data directly through standard in-memory streams (pipes). This fixes the file speed problem easily.&lt;/p&gt;

&lt;p&gt;The perfect, final goal is &lt;strong&gt;Shared Memory&lt;/strong&gt;. Instead of copying text back and forth, Python and WASM can look at the exact same spot in the computer's memory. This is the fastest possible way to share data, though it introduces complex engineering challenges, such as safely managing Go's Garbage Collector across the Python-WASM boundary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkucqd0r2mtc7ib703u3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkucqd0r2mtc7ib703u3.png" alt=" " width="189" height="637"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we should look at &lt;strong&gt;TinyGo&lt;/strong&gt;. The normal Go compiler adds a lot of heavy, extra background tasks. TinyGo is a smaller compiler made just for WASM. Using it would make the tool much smaller and make it start up 10 to 20 times faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Verdict: When to use which?
&lt;/h2&gt;

&lt;p&gt;Even though normal programs start faster, speed isn't the only thing that matters—security and stability matter too. The hidden superpower of WASM is that Python is in total control. If Python crashes, the WASM tool dies cleanly with it. If you use &lt;code&gt;subprocess.run&lt;/code&gt;, a crashed Python app might leave behind "zombie" programs that eat up your server's resources.&lt;/p&gt;

&lt;p&gt;So, when should you use a &lt;strong&gt;Normal Program&lt;/strong&gt;? They are best for things you run rarely, like a command-line developer tool or a script that runs once an hour. The computer's Operating System is great at starting them quickly. While proper process management in Python can mitigate risks, WASM remains inherently safer for memory lifecycles.&lt;/p&gt;

&lt;p&gt;When should you use &lt;strong&gt;WASM&lt;/strong&gt;? WASM is the undisputable winner for modern, always-on Cloud servers. When you need to run untrusted code safely, or you need your tool to work on any operating system without building 10 different versions, WASM is unbeatable. It guarantees that your code stays locked in a safe box.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating PII-Shield into GuardSpine taught us a lot about the trade-offs between raw speed and safe, modern design. While normal programs won the simple speed test, adapting WebAssembly gave the platform unmatched security and the ability to run anywhere without complex cross-compilation.&lt;/p&gt;

&lt;p&gt;As the platform's processing volume scales, the roadmap for this integration is clear. We need to stop turning the WASM tool on and off for every log line. Instead, the plan is to keep the engine running persistently, compile it with TinyGo, and share memory directly across the Python-WASM boundary. The ultimate goal isn't just to match the speed of normal programs, but to beat them completely, creating a lightning-fast, unbreakable redaction layer for AI development.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>go</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>Stop Leaking API Keys in your AI Agent Logs: A Go Sidecar Approach</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Thu, 05 Feb 2026 21:41:24 +0000</pubDate>
      <link>https://dev.to/aragossa/stop-leaking-api-keys-in-your-ai-agent-logs-a-go-sidecar-approach-d3</link>
      <guid>https://dev.to/aragossa/stop-leaking-api-keys-in-your-ai-agent-logs-a-go-sidecar-approach-d3</guid>
      <description>&lt;h1&gt;
  
  
  Stop Leaking API Keys in your AI Agent Logs: A Go Sidecar Approach
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; The Hidden Privacy Leak in your AI Agents (and why your LLM "Audit Logs" are a GDPR Nightmare)&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Problem
&lt;/h2&gt;

&lt;p&gt;Everyone is building AI agents right now. Whether you're using LangChain, AutoGPT, or custom loops, you are almost certainly logging their work. You keep traces of every step to debug reasoning loops or monitor costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here lies the pain:&lt;/strong&gt; Everything goes into these logs. User prompts, model responses, and raw JSON payloads from APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Visualization
&lt;/h3&gt;

&lt;p&gt;Imagine this scenario:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input Log:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aragossa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My key is sk-live-123456"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What your Logging System Saved:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aragossa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My key is sk-live-123456"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user pastes their password, API key, or PII into the chat, or if an API returns a sensitive internal token, that data is now permanently etched into your Elasticsearch, Datadog, or S3 bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Consequence:&lt;/strong&gt; Your fine-tuning dataset is now "poisoned" with real user data. This is a massive GDPR violation and a ticking security time bomb. You can't just "delete" it if you don't know where it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Gap: Why usual methods fail
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Regex?&lt;/strong&gt; Good luck maintaining a regex list for every possible API key format, session token, and PII variation in existence. It’s a game of whack-a-mole you will lose.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;ML-based protection?&lt;/strong&gt; Too slow. If your agent operates in real-time, you cannot afford a 500ms roundtrip to a BERT model just to sanitize specific log lines. PII scans often become the bottleneck.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python-native logic?&lt;/strong&gt; Processing massive text streams in an interpreted language (like Python) adds significant CPU overhead per log line compared to a compiled Go binary. In high-throughput pipes, this latency adds up fast.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Existing Observability Tools?&lt;/strong&gt;&lt;br&gt;
Systems like Fluent Bit, Datadog, or OpenTelemetry already offer redaction and PII masking, usually via pattern rules and regex. For many workloads that’s perfectly fine. The trade‑off is that these pipelines are not optimized for AI‑agent traces: they either run late in the pipeline (after logs have already left the pod) or rely on configuration‑heavy pattern catalogs that are hard to keep up‑to‑date in a world of ever‑changing API keys and internal tokens.&lt;/p&gt;

&lt;p&gt;Static scanners like &lt;strong&gt;TruffleHog&lt;/strong&gt; shine for repositories and CI, where you scan code at rest. They’re not meant to sit inline on a hot log stream and make sub‑millisecond decisions on every line.&lt;/p&gt;

&lt;p&gt;Where &lt;strong&gt;PII‑Shield&lt;/strong&gt; is different is not in “inventing redaction”, but in the combination of techniques tailored for AI agents: entropy + bigram signals for unknown secrets, &lt;strong&gt;deterministic HMAC&lt;/strong&gt; instead of &lt;code&gt;***&lt;/code&gt; for referential integrity, and deep JSON traversal to keep your log schemas intact while still scrubbing sensitive values.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The Implementation: Enter PII-Shield
&lt;/h2&gt;

&lt;p&gt;Meet &lt;strong&gt;PII-Shield&lt;/strong&gt;. It’s a lightweight sidecar written in Go that sits right next to your agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Killer Feature #1: Entropy-based Detection
&lt;/h3&gt;

&lt;p&gt;We don't just search for "password=". We look for &lt;strong&gt;chaos&lt;/strong&gt;.&lt;br&gt;
API keys and authentication tokens naturally have high "entropy" (randomness/complexity). Normal human speech has low entropy. By calculating the mathematical complexity of strings, we can flag 64-character hex strings or base64 blobs without knowing their specific format.&lt;/p&gt;
&lt;h3&gt;
  
  
  Killer Feature #2: Deterministic HMAC
&lt;/h3&gt;

&lt;p&gt;This is the feature that caught attention on Hacker News.&lt;br&gt;
We don't just replace secrets with &lt;code&gt;***&lt;/code&gt;. We turn &lt;code&gt;secret123&lt;/code&gt; into &lt;code&gt;[HIDDEN:a1b2c3]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input Log:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aragossa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My key is sk-live-123456"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PII-Shield Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aragossa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My key is [HIDDEN:8f2a1b]"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;br&gt;
This is a deterministic HMAC (Hash-based Message Authentication Code).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It allows you to &lt;strong&gt;trace&lt;/strong&gt; a specific user or session across multiple log lines without knowing who they are.&lt;/li&gt;
&lt;li&gt;  It preserves &lt;strong&gt;Referential Integrity&lt;/strong&gt; for debugging. You can see that "Session A" failed 5 times, but you validly cannot see the Session ID itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Killer Feature #3: Statistical Adaptive Threshold
&lt;/h3&gt;

&lt;p&gt;PII-Shield doesn't just use a hardcoded number. It learns the "baseline noise" of your logs. By calculating the mean and standard deviation ($2\sigma$), it automatically adjusts the sensitivity to your specific environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. The Logic: Sidecar Architecture
&lt;/h2&gt;

&lt;p&gt;The architecture is dead simple, leveraging the power of Kubernetes sidecars or UNIX pipes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2a293mf3qpmfv2r502b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2a293mf3qpmfv2r502b.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your agent simply writes logs to stdout. PII-Shield intercepts the stream, scans it in real-time with near-zero overhead, sanitizes it, and passes it forward.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The Technical Meat
&lt;/h2&gt;

&lt;p&gt;Why Go? Because we need raw speed and no dependencies.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Entropy &amp;amp; Bigrams (The Math)
&lt;/h3&gt;

&lt;p&gt;Here is how we calculate the "Chaos" (Entropy) of a token in &lt;a href="https://github.com/aragossa/pii-shield/blob/main/pkg/scanner/scanner.go" rel="noopener noreferrer"&gt;scanner.go&lt;/a&gt;. We use a combination of Shannon Entropy, Character Class Bonuses, and English Bigram analysis.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Bigram Check&lt;/strong&gt; is crucial: it penalizes strings that look like valid English (common letter pairs) and boosts score for "unnatural" strings. (Note: This is optimized for English but can be disabled or tuned via &lt;code&gt;PII_DISABLE_BIGRAM_CHECK&lt;/code&gt; for other languages).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// From scanner.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CalculateComplexity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Shannon Entropy&lt;/span&gt;
    &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;calculateShannon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

    &lt;span class="c"&gt;// 2. Class Bonus (Upper, Lower, Digit, Symbols)&lt;/span&gt;
    &lt;span class="c"&gt;// bonus := float64(classes-1) * 0.5&lt;/span&gt;
    &lt;span class="n"&gt;bonus&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;calculateClassBonus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

    &lt;span class="c"&gt;// 3. Bigram Check (English Likelihood)&lt;/span&gt;
    &lt;span class="c"&gt;// Penalizes common English, boosts random noise&lt;/span&gt;
    &lt;span class="n"&gt;bigramScore&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;calculateBigramAdjustment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bonus&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bigramScore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. False Positives? (Whitelists)
&lt;/h3&gt;

&lt;p&gt;"Entropy is great, but won't it eat my Git Hashes or UUIDs?"&lt;br&gt;
PII-Shield includes built-in &lt;strong&gt;Whitelists&lt;/strong&gt; (&lt;a href="https://github.com/aragossa/pii-shield/blob/main/pkg/scanner/scanner.go#670-760" rel="noopener noreferrer"&gt;isSafe&lt;/a&gt; function) for standard identifiers like UUIDs, IPv6 addresses, Git Commit hashes (SHA-1), and MongoDB ObjectIDs. This ensures your debugging data stays intact while secrets get redacted.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Credit Card Detection (Luhn Algorithm)
&lt;/h3&gt;

&lt;p&gt;Entropy isn't enough for credit cards, as numbers often have low randomness. PII-Shield includes a high-performance implementation of the &lt;strong&gt;Luhn Algorithm&lt;/strong&gt; to scan for valid card checksums in the stream (&lt;a href="https://github.com/aragossa/pii-shield/blob/main/pkg/scanner/scanner.go#813-883" rel="noopener noreferrer"&gt;FindLuhnSequences&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Deep JSON Inspection
&lt;/h3&gt;

&lt;p&gt;For JSON logs, PII-Shield performs deep inspection (&lt;a href="https://github.com/aragossa/pii-shield/blob/main/pkg/scanner/scanner.go#962-981" rel="noopener noreferrer"&gt;processJSONLine&lt;/a&gt;), preserving the schema while redacting values. &lt;br&gt;
&lt;em&gt;Note: PII-Shield re-serializes JSON (using &lt;code&gt;json.Marshal&lt;/code&gt;), which may change key order/sorting. It guarantees semantic integrity but is best used early in your pipeline, before any byte-sensitive steps (like signing or exact-diff comparisons).&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Deterministic Redaction
&lt;/h3&gt;

&lt;p&gt;Here is the HMAC logic using a secure Salt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;redactWithHMAC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensitiveData&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// CurrentConfig.Salt is loaded securely from env vars&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Salt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensitiveData&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c"&gt;// We only keep a short prefix for tracing identity&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[HIDDEN:%s]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&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 main loop (&lt;a href="https://github.com/aragossa/pii-shield/blob/main/cmd/cleaner/main.go" rel="noopener noreferrer"&gt;cmd/cleaner/main.go&lt;/a&gt;) is a highly efficient buffered reader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c"&gt;// Core logic: Low allocation overhead&lt;/span&gt;
        &lt;span class="c"&gt;// Imports github.com/aragossa/pii-shield/pkg/scanner&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScanAndRedact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Note on Scope: High-Entropy vs. Natural Language
&lt;/h3&gt;

&lt;p&gt;Let's be clear: PII-Shield is laser-focused on &lt;strong&gt;High-Entropy secrets&lt;/strong&gt; (API keys, tokens, auth headers) and structural patterns (Credit Cards). It is &lt;strong&gt;not&lt;/strong&gt; a magic NLP bullet for detecting names like "John Smith" or free-text addresses. For that, you should treat PII-Shield as a low-latency "first line of defense" for your infrastructure, potentially complemented by heavier offline NLP tools for semantic analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. How to try it
&lt;/h2&gt;

&lt;p&gt;I am looking for edge cases. If you are building AI agents, try running your trace logs through this and see what it catches (or misses).&lt;/p&gt;

&lt;p&gt;You can run it locally with Docker:&lt;br&gt;
&lt;code&gt;docker run -i -e PII_SALT="mysalt" pii-shield &amp;lt; logs.txt&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/aragossa/pii-shield" rel="noopener noreferrer"&gt;https://github.com/aragossa/pii-shield&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;br&gt;
Security often trails behind innovation. With the explosion of AI Agents, we are generating massive amounts of sensitive data in logs. PII-Shield is a "drop-in" safety net to ensure your innovation doesn't become a liability.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>go</category>
    </item>
    <item>
      <title>Playwright &amp; Chaos Engineering: 3 Ways to Break Your UI in 10 Lines of Code 🧨</title>
      <dc:creator>Ilya Ploskovitov</dc:creator>
      <pubDate>Tue, 03 Feb 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aragossa/playwright-chaos-engineering-3-ways-to-break-your-ui-in-10-lines-of-code-2bkj</link>
      <guid>https://dev.to/aragossa/playwright-chaos-engineering-3-ways-to-break-your-ui-in-10-lines-of-code-2bkj</guid>
      <description>&lt;p&gt;"The tests are green, but production is down."&lt;/p&gt;

&lt;p&gt;We’ve all been there. Your CI/CD pipeline looks like a Christmas tree (all green), yet 5 minutes after deployment, the support tickets start rolling in. Why? Because we tend to test only the &lt;strong&gt;"Happy Path."&lt;/strong&gt; In the real world, users enter elevators (network loss), backends have database deadlocks (500 errors), and low-end devices struggle with heavy JS (CPU race conditions).&lt;/p&gt;

&lt;h2&gt;
  
  
  Here are 3 simple ways to inject chaos into your Playwright tests using &lt;strong&gt;Python&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt; without any external dependencies.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. The "Kill the Backend" Scenario (500 Error Injection)
&lt;/h2&gt;

&lt;p&gt;What happens if your billing API fails? Does your UI show a "Retry" button, or does it hang forever?&lt;/p&gt;

&lt;p&gt;Scenario: Intercept a critical API call and return a 500 Internal Server Error.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Python Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_billing_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Intercepting the payment endpoint
&lt;/span&gt;    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/api/v1/billing/pay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal Database Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pay Now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Assert that the UI handles the crash gracefully
&lt;/span&gt;    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.error-message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to_be_visible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;b&gt;TypeScript Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handle billing failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/api/v1/billing/pay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal Database Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pay Now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.error-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  2. The "Elevator Effect" (Sudden Offline Mode)
&lt;/h2&gt;

&lt;p&gt;Users move. Networks drop. If your app is an SPA, losing connection mid-session can lead to corrupted local states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;: Start a file upload and cut the internet connection.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Python Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_upload_interruption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set_input_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heavy_video.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Chaos: Go offline instantly
&lt;/span&gt;    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_offline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Expect a "Resume" button or "Connection lost" banner
&lt;/span&gt;    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to_be_visible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_offline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Restore network
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;b&gt;TypeScript Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovery on network loss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setInputFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavy_video.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOffline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Resume&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOffline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  3. The "Old Phone" Race Condition (CPU Throttling)
&lt;/h2&gt;

&lt;p&gt;Async bugs often hide behind the speed of your developer laptop. By slowing down the CPU, you change the execution order of scripts and catch elusive race conditions.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Python Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_race_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Slow down CPU by 6x using Chrome DevTools Protocol (CDP)
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_cdp_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Emulation.setCPUThrottlingRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/heavy-dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Load Stats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Assert that the status eventually becomes 'Ready'
&lt;/span&gt;    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to_contain_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;b&gt;TypeScript Code&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;catch race conditions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;newCDPSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Emulation.setCPUThrottlingRate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/heavy-dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Load Stats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 Pro Tip: When to Run These?
&lt;/h2&gt;

&lt;p&gt;Don't run chaos tests on every PR. They are inherently more complex and can be "flaky" if your timeouts aren't tuned.&lt;/p&gt;

&lt;p&gt;Best Practice: Add them to a nightly or pre-release suite.&lt;/p&gt;

&lt;p&gt;Limit: Remember that CDP (CPU Throttling) only works on Chromium-based browsers.&lt;/p&gt;

&lt;p&gt;Wrapping Up&lt;br&gt;
Resilience is a feature. If you only test for success, you're only doing half of your job as a QA Engineer. Break your UI before your users do.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I’ve written a more detailed deep-dive on Resilience Strategy &amp;amp; CI/CD integration on my new blog. Check it out at &lt;a href="http://chaosqa.com" rel="noopener noreferrer"&gt;ChaosQA.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>playwright</category>
      <category>python</category>
    </item>
  </channel>
</rss>
