Snippets · Pipelines
Firewall syslog to a SIEM, with PII masked
Firewalls and VPN gateways send syslog that the SIEM needs, including user names and e-mail addresses the SIEM team should not see in clear. This pipeline receives RFC 3164 syslog over UDP, replaces each user name with a salted SHA-256 (so one user's events still correlate), masks e-mail addresses, keeps source and destination IPs, and sends the result to Splunk HEC. We sent real syslog over UDP to this config; the before/after below is the collector's output for the same lines.
- Signals
- Logs
- Category
- CollectionCompliance
- Source
- Firewall syslog (UDP)
- Destination
- Splunk (HEC)
Included steps
OpenTelemetry Collector
receivers:
syslog:
udp:
listen_address: 0.0.0.0:5514
protocol: rfc3164
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 25
# Masks e-mail addresses and pseudonymises user names (in the message and
# the raw body) before the events leave for the SIEM; source and destination IPs stay, they are
# what the SIEM correlates on.
transform/pii:
error_mode: ignore
log_statements:
- context: log
statements:
# Hash first: once the next statement masks user=, there is nothing left to hash.
- set(attributes["user.hash"], SHA256(Concat(["${env:USER_HASH_SALT}", ExtractPatterns(attributes["message"], "user=(?P<u>[^ ]+)")["u"]], ""))) where IsMatch(attributes["message"], "user=")
- replace_pattern(attributes["message"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
- replace_pattern(attributes["message"], "user=([^ ]+)", "user=<user>")
# The syslog receiver keeps the raw line in the body too: mask it there as well.
- replace_pattern(body, "[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
- replace_pattern(body, "user=([^ ]+)", "user=<user>")
batch:
timeout: 5s
exporters:
splunk_hec:
endpoint: https://siem.example.internal:8088/services/collector
token: ${env:SPLUNK_HEC_TOKEN}
index: firewall
sourcetype: syslog
sending_queue:
enabled: true
queue_size: 5000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
pipelines:
logs:
receivers: [syslog]
processors: [memory_limiter, transform/pii, batch]
exporters: [splunk_hec]
LinkMesh
LinkMesh has no built-in template for this job. Add it as a custom processor in raw mode, with transform as the component type and this body - the same lines as under transform/pii in the collector config:
error_mode: ignore
log_statements:
- context: log
statements:
- set(attributes["user.hash"], SHA256(Concat(["${env:USER_HASH_SALT}", ExtractPatterns(attributes["message"], "user=(?P<u>[^ ]+)")["u"]], ""))) where IsMatch(attributes["message"], "user=")
- replace_pattern(attributes["message"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
- replace_pattern(attributes["message"], "user=([^ ]+)", "user=<user>")
- replace_pattern(body, "[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
- replace_pattern(body, "user=([^ ]+)", "user=<user>")
How custom components are validated and rolled out: create a custom processor.
Download config.yamlValidated with otelcol-contrib v0.161.0 on 2026-10-26
Complete config files, each checked with the collector's own validate command (otelcol-contrib v0.161.0).
Before and after
Example telemetry in, and what this exact config sent out when we ran it in otelcol-contrib v0.161.0. CI re-runs it on every change and fails if the output differs. Changed values are highlighted.
3 log records in, 3 log records out
Before
- body
- <134>Oct 23 08:15:02 fw01 vpn: user=anna.meier login from 203.0.113.7 ok
- attributes.appname
- vpn
- attributes.hostname
- fw01
- attributes.message
- user=anna.meier login from 203.0.113.7 ok
- attributes.priority
- 134
After
- body
- <134>Oct 23 08:15:02 fw01 vpn: user=<user> login from 203.0.113.7 ok
- attributes.appname
- vpn
- attributes.hostname
- fw01
- attributes.message
- user=<user> login from 203.0.113.7 ok
- attributes.priority
- 134
- attributes.user.hash
- 6c9adc5977965db8c49839df5a4680e29b0f81e8a37a6668a9ce268dc33903af
Before
- body
- <132>Oct 23 08:15:09 fw01 vpn: password reset mail sent to anna.meier@example.ch
- attributes.appname
- vpn
- attributes.hostname
- fw01
- attributes.message
- password reset mail sent to anna.meier@example.ch
- attributes.priority
- 132
After
- body
- <132>Oct 23 08:15:09 fw01 vpn: password reset mail sent to <email>
- attributes.appname
- vpn
- attributes.hostname
- fw01
- attributes.message
- password reset mail sent to <email>
- attributes.priority
- 132
Before
- body
- <134>Oct 23 08:15:11 fw01 filter: block tcp 198.51.100.23:51522 -> 10.0.4.12:22
- attributes.appname
- filter
- attributes.hostname
- fw01
- attributes.message
- block tcp 198.51.100.23:51522 -> 10.0.4.12:22
- attributes.priority
- 134
After
- body
- <134>Oct 23 08:15:11 fw01 filter: block tcp 198.51.100.23:51522 -> 10.0.4.12:22
- attributes.appname
- filter
- attributes.hostname
- fw01
- attributes.message
- block tcp 198.51.100.23:51522 -> 10.0.4.12:22
- attributes.priority
- 134
The gotcha
Two orderings matter, and both failed in our first draft. Statements run top to bottom: when the user name was masked before it was hashed, every user got the same hash - the hash of the placeholder "<user>". And the syslog receiver keeps the raw line in the body as well as in the message attribute: masking only the attribute left the e-mail address in clear in the body the SIEM receives. This config hashes first and masks both.
Manage this across a fleet
LinkMesh manages collector configuration across a fleet from one self-hosted control plane, so a change like this is made once rather than host by host. The first 25 Collectors are free after a no-card registration in the OpenSight Customer Portal (5 without one).
Related snippets
- Drop sensitive attributes before exportDelete named attributes, and every captured HTTP request header, from logs, spans and metrics before they are exported.
- Pseudonymise user IDs with a salted hashReplace user.id with a salted SHA-256 on logs and spans - same user, same value, so per-user queries still join.
- Kubernetes pod logs to Loki, enriched with deployment and clusterEvery pod's logs into Loki over OTLP, labelled with namespace, pod, container, deployment, node and cluster.
- Receive syslog (RFC 5424 and RFC 3164)Accept modern and legacy syslog on one collector, from network gear and hosts that speak nothing else.