DEV Community

Ilya Ploskovitov
Ilya Ploskovitov

Posted on Edited on Originally published at pii-shield.com

Zero-code PII sanitization for Grafana Loki and Promtail in Kubernetes

If you use Grafana Loki for log aggregation, you probably rely on Promtail (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.

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.

The Standard Promtail Approach: pipeline_stages Regex

The official Grafana way to handle sensitive data is using Promtail's pipeline_stages to scrub data before it reaches Loki. A typical Promtail config looks like this:

pipeline_stages:
  - match:
      selector: '{app="my-service"}'
      stages:
        - regex:
            expression: '(?P<email>[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'
        - replace:
            source: email
            expression: '(.*)'
            replace: '[REDACTED]'
Enter fullscreen mode Exit fullscreen mode

Why this approach breaks down at scale:

  1. The Regex Trap: 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.
  2. CPU and Latency: Running heavily nested regex pipeline_stages 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.
  3. Loss of Context: When you replace a secret with [REDACTED], 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 [REDACTED].

The Zero-Code Alternative: PII-Shield Sidecar

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: PII-Shield.

PII-Shield intercepts the log stream before Promtail even sees it.

How it works:

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

Promtail needs zero configuration changes. It just blindly ships the logs to Loki, completely unaware that the heavy sanitization has already occurred.

Kubernetes Implementation

Here is how you configure the pod natively. Note that Promtail, by default, reads from /var/log/containers/*.log via Kubernetes Service Discovery. It will automatically find the pii-shield-sidecar output.

apiVersion: v1
kind: Pod
metadata:
  name: billing-service
  labels:
    app: billing
spec:
  containers:
    - name: billing-app
      image: billing-app:v2.1.0
      # The app writes its private output to a shared pipe/file
      command: ["/bin/sh", "-c"]
      args: ["./billing-binary > /var/run/logs/app.log"]
      volumeMounts:
        - name: log-volume
          mountPath: /var/run/logs

    - name: pii-shield-sidecar
      image: thelisdeep/pii-shield:2.2.0
      env:
        - name: PII_SALT
          value: "grafana-secure-salt"
      # PII-Shield reads, scrubs, and outputs to stdout for Promtail to pick up
      command: ["/bin/sh", "-c"]
      args: ["tail -n +1 -f /var/run/logs/app.log | pii-shield"]
      volumeMounts:
        - name: log-volume
          mountPath: /var/run/logs

  volumes:
    - name: log-volume
      emptyDir: {}
Enter fullscreen mode Exit fullscreen mode

Pro tip: The thelisdeep/pii-shield image is multi-arch (amd64/arm64).

Why this is the ultimate Loki stack upgrade:

  • Zero Promtail Pipelines: You can delete hundreds of lines of brittle pipeline_stages from your Promtail DaemonSet. Promtail goes back to doing what it does best: shipping logs.
  • Deterministic Hashing: PII-Shield replaces secrets with a stable HMAC using the PII_SALT, (e.g., [HIDDEN:e9f1a2]). In Loki's LogQL, you can now trace an exact user workflow |= "[HIDDEN:e9f1a2]" across microservices without actually knowing their email or token.
  • Smart Entropy Detection: 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.
  • Micro-footprint: the default Helm chart caps it at a 30Mi memory limit, making it cheap to run as a sidecar.

Protect your Grafana Loki data retention today.
Check out the PII-Shield repository on GitHub and drop a star if this simplifies your Kubernetes logging!

Top comments (1)

Collapse
 
scsoi profile image
Info Comment hidden by post author - thread only accessible via permalink
疏影 •

PII sanitization at ingest is the right call — sanitizing after the fact on Grafana queries is fragile and gets you out of compliance the first time someone exports a LogQL query result.

A few operational notes that we hit running Loki at scale:

  1. Labels are the price of admission. Loki uses labels as the indexing key — every unique label combination is a stream. If you put high-cardinality labels (request_id, user_id, trace_id) on every log line, the indexer dies within hours. Keep labels low-cardinality (app, env, pod_template) and put the high-cardinality values in the log line itself.

  2. Chunk size and flush interval trade off memory vs query latency. Default chunk_idle_period=5m means a log stream has to be quiet for 5 minutes before its chunk is finalized. During an incident, you want smaller chunks (1-2m) so you can query partial data while the stream is still active.

  3. Compactor is the part people forget. Without it, the index grows monotonically and query latency creeps up weekly. Run the compactor on a single replica (it uses a single-lease lock); default retention is unlimited, which is fine until your first storage bill arrives.

  4. Storage backend choice matters more than people admit. S3 / GCS work fine for cold chunks; the Boltdb-shipper index needs fast random-access, which is fine for object storage but bad on a slow NFS mount. We have seen shops standardise on a single mount layer (ScsDriver mounts WebDAV / Aliyun OSS / S3 / NFS as a real local disk so the Boltdb-shipper index sees a stable path; cross-region failover just re-mounts at the new node). Loki does not care that the underlying blob store is object storage, as long as fsync on the index writes is honest.

  5. Promtail is the easy part. AllergicFly's design (pipeline stages for parsing, drop, label, template) handles most of the heavy lifting without custom code. The mistake is dropping everything to one giant pipeline; per-app pipelines let you keep the regex simple.

  6. PII redaction at ingest is exactly what the article covers, and we agree — regex-driven redaction in Promtail's pipeline_stages catches email, phone, and credit-card patterns. For tokenized data (Bearer / JWT), a regex stage with (?i)(bearer\s+)[a-z0-9._-]+ and replace with ${1}REDACTED is the pattern. Do this on the Promtail side, not Loki queries.

Anyone running Loki with the new tsdb index (post v2.9) — anyone moved off Boltdb-shipper yet? Curious about the migration path in production.

Some comments have been hidden by the post's author - find out more