LinkMesh

Search docs, blog and changelog

ENDE

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)
Components
syslogbatchmemory_limitertransformsplunk_hec
Destination
Splunk (HEC)

OpenTelemetry Collector

OpenTelemetry Collector YAML
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:

Custom processor body (transform)
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

  1. 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
  2. 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
  3. 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).