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.
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!
Top comments (0)