Zero-code PII redaction for OpenTelemetry Collector logs in Kubernetes
TL;DR:
the Collector's redaction
processor governs structured attributes, but free-text log bodies still depend on OTTL regexes you
have to write and maintain. Run a PII-Shield sidecar so the filelog
receiver only ever reads masked identifiers like [HIDDEN:a1b2c3].
The OpenTelemetry
Collector is quickly becoming the default telemetry pipeline in Kubernetes: one DaemonSet
with a filelog
receiver tails /var/log/containers/*.log,
enriches every record with pod metadata, and exports to whichever backend you point it at —
Elasticsearch, Loki, Datadog, an OTLP endpoint, often several at once.
Which means that when an application logs a user's email or a bearer token, the Collector batches that value and exports it to every configured destination before anyone notices.
The Standard Approach: Redaction and Transform Processors
The Collector ships two contrib processors
for this job. The redaction
processor works on attributes: keys not on an allow-list are
dropped, and attribute values matching blocked patterns are masked:
processors:
redaction:
allow_all_keys: false
allowed_keys:
- http.method
- http.status_code
- k8s.pod.name
blocked_values:
- "[0-9]{3}-[0-9]{2}-[0-9]{4}" # SSN-shaped
- "4[0-9]{12}(?:[0-9]{3})?" # Visa-shaped
The harder part is the log
body, where most PII actually sits as free text. For that, the
standard answer is the transform
processor with an OTTL statement per secret shape:
processors:
transform/redact_body:
log_statements:
- context: log
statements:
- replace_pattern(body, "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+", "[REDACTED]")
- replace_pattern(body, "Bearer [A-Za-z0-9\\-_.]+", "Bearer [REDACTED]")
Why this approach breaks down at scale:
- The Regex Trap: the redaction processor's allow-list protects attributes, but every secret shape inside the body — API keys, session tokens, your internal ID formats — needs its own OTTL pattern. A shape you didn't anticipate ships in cleartext, and nothing fails or warns; the statement just doesn't match.
- Per-Record Pipeline Cost: every OTTL statement runs against every log record on the node's Collector. The cost scales with log volume and with the length of your pattern list — not with how much PII is actually present. Ten patterns means ten regex evaluations per line on every node.
- Loss of Context: a static
[REDACTED]destroys correlation. If ten log lines all become[REDACTED], you can no longer tell whether one user hit an error ten times or ten users hit it once each.
The Zero-Code Alternative: PII-Shield Sidecar
Instead of growing an OTTL pattern list inside every Collector config, move log sanitization to a dedicated, low-allocation Go sidecar that runs before the filelog receiver ever sees the line: PII-Shield. It detects secrets by entropy and key context rather than by enumerating shapes, so an unfamiliar token format is still caught.
The Result:
// What your app generated:
{"level":"info", "message":"User authenticated", "email":"john.doe@gmail.com", "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
// What the Collector's filelog receiver actually reads and exports:
{"level":"info", "message":"User authenticated", "email":"[HIDDEN:e9f1a2]", "token":"[HIDDEN:a1c7af]"}
The hash is a salted HMAC, deterministic
per value — so [HIDDEN:e9f1a2]
is the same user across all their log lines, and your queries and dashboards keep working.
How it works:
- The App
redirects its output to an ephemeral shared volume (an
emptyDir) instead of the mainstdout. - PII-Shield
(Sidecar) tails that file, scrubs it with entropy-based secret detection (no
pattern list to maintain for API keys and tokens), and prints the clean logs to its own
stdout. - The OTel Collector — already running as a
DaemonSet whose
filelogreceiver reads/var/log/containers/*.log— picks up the sidecar'sstdoutlike any other container's, no Collector config change required.
The transform/redact_body
block and its regex list can be deleted from the logs pipeline. The Collector goes back to doing
what it does best — enriching and routing already-clean records.
Kubernetes Implementation
The pattern is the same whether your Collector runs as a DaemonSet, a deployment, or via the OpenTelemetry Operator — the Collector never needs to know PII-Shield exists.
apiVersion: v1
kind: Secret
metadata:
name: pii-shield-secret
type: Opaque
stringData:
pii-salt: "replace-with-a-long-random-value"
---
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 2>&1"]
volumeMounts:
- name: log-volume
mountPath: /var/run/logs
- name: pii-shield-sidecar
image: thelisdeep/pii-shield:2.2.0
env:
- name: PII_SALT
valueFrom:
secretKeyRef:
name: pii-shield-secret
key: pii-salt
# Scratch image: run the binary directly (no shell/tail). Reads, scrubs,
# and outputs to stdout for the filelog receiver to pick up.
command: ["/pii-shield"]
args: ["--watch-file", "/var/run/logs/app.log"]
volumeMounts:
- name: log-volume
mountPath: /var/run/logs
volumes:
- name: log-volume
emptyDir: {}
*Pro
tip: The thelisdeep/pii-shield
image is multi-arch (amd64/arm64).*
Verify It's Actually Working
Before trusting the pipeline, confirm the sidecar's own output is already clean — don't just query your backend, since a misconfigured mount can leave the filelog receiver reading the app container's raw stream instead.
# Read the sidecar's own stdout directly — this is what the filelog receiver sees
kubectl logs billing-service -c pii-shield-sidecar --tail=50
# Confirm no raw emails/tokens survived
kubectl logs billing-service -c pii-shield-sidecar --tail=200 | grep -E "@|Bearer|sk-" | grep -v "\[HIDDEN:"
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.
Honest Limitations
The sidecar covers the logs path — what containers write to stdout and the filelog
receiver picks up. Traces and metrics that your application SDKs send straight to the Collector
over OTLP never pass through it, so if span attributes carry PII, keep the redaction
processor on those pipelines. The two mechanisms compose: sidecar for log bodies, processor
allow-list for attributes.
PII-Shield also 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 can't retroactively clean records the Collector already exported; it only sanitizes what passes through the sidecar from the moment it's deployed. And it's stream-based: always check the sidecar's own output during rollout instead of assuming the mount and file path line up on the first try.
Common questions
Stop
maintaining OTTL regex lists for secrets.
Check out the PII-Shield repository on
GitHub and drop a star if this simplifies your OpenTelemetry pipeline!