We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.Privacy Policy

EvilMailEvilMail
FeaturesEverything EvilMail can doTemp Mail for…Temp mail for Discord, ChatGPT & moreNetwork ToolsDNS, WHOIS, port & network toolsCompareEvilMail vs other temp mail services
APIREST API reference & guidesTemp Mail APICopy-paste code in every language
AppsPricingBlog
About UsWho we are & what we buildTrust & SecurityHow we protect your dataFAQAnswers to common questionsContactGet in touch with the team
LoginRegister
EvilMailEvilMail
AppsPricingBlog
LoginRegister
HomeBlogRedacting PII From Mail-Server Logs Without Going Blind
Privacy

Redacting PII From Mail-Server Logs Without Going Blind

Your mail.log is the largest un-audited PII store you run. Here is how to strip identity at the rsyslog layer, pseudonymize with a keyed HMAC, and still debug every delivery — because Postfix already hands you a PII-free correlation key.

EvilMail TeamAugust 3, 2026
12 min read
Redacting PII From Mail-Server Logs Without Going Blind
Contents
Anatomy of a leak: what one delivery writes to diskKeep the queue ID, drop the identityRedact at syslog, not in the appPseudonymize with an HMAC, don't just deleteThe quieter leaks: Dovecot auth and RspamdTime-box the verbosity you actually needRetention is redaction you get for freeWhy this is product-aligned, not just complianceOperator checklist

A single message hitting your MX writes something like this to disk:

Jul  4 09:12:44 mx1 postfix/smtpd[2214]: 4Xyz7Q1cKpz1: client=host.example.com[203.0.113.44]
Jul  4 09:12:44 mx1 postfix/cleanup[2219]: 4Xyz7Q1cKpz1: message-id=<[email protected]>
Jul  4 09:12:45 mx1 postfix/qmgr[1180]: 4Xyz7Q1cKpz1: from=<[email protected]>, size=5123, nrcpt=1
Jul  4 09:12:46 mx1 postfix/smtp[2231]: 4Xyz7Q1cKpz1: to=<[email protected]>, relay=..., delay=1.2, dsn=2.0.0, status=sent

Four lines, and you have just recorded a real person's IP, their reverse DNS, a precise timestamp, who they emailed, and who emailed them. Under GDPR that is textbook personal data — the CJEU settled in *Breyer* (C-582/14) that a dynamic IP plus a timestamp is identifiable, and an email address identifies directly. Multiply by a few million lines a day and your mail.log becomes the biggest PII archive you operate, with no retention policy, no access control worth the name, and no DPIA. Nobody decided to build a surveillance index. It accreted, one debug line at a time.

The reframe this article runs on: none of the red fields above are load-bearing for debugging. The queue ID 4Xyz7Q1cKpz1 is. Every troubleshooting workflow you actually run keys off the queue ID, not the recipient address. So you can redact identity at ingestion, keep the correlation spine intact, and lose nothing operational.

Anatomy of a leak: what one delivery writes to disk

Postfix logs a message across several daemons, and each field falls into exactly one of three buckets. Personal data: the client= hostname and IP, the from= sender, the to= recipient, the message-id, and — easy to forget — the SASL username on authenticated smtpd lines. Correlation-safe: the queue ID, the timestamp, size=, delay=, dsn=, status=. Noise: ephemeral client ports, most connect from chatter.

The queue ID is the join key. In modern Postfix — long queue IDs have been the default since 2.9 — it is a base-52 token encoding the message's arrival time plus an inode-derived sequence, which is the 4Xyz7Q1cKpz1 you see above; the legacy short form is uppercase hex derived from the queue-file inode. Either way it is unique per message per host and contains zero personal data. Every daemon that touches the message — smtpd, cleanup, qmgr, smtp/lmtp — stamps the same queue ID. That is the entire reason grep 4Xyz7Q1cKpz1 mail.log reconstructs a full session. You debug on the spine, not on the identity hanging off it.

Log line anatomy — identity vs. correlation key postfix/smtp: one delivery line, field by field RAW 4Xyz7Q1cKpz1 to=<bob@…> relay=203.0.113.5 delay=1.2 dsn=2.0.0 crimson = personal data amber = correlation-safe (keep) rsyslog mmanon + HMAC rewrite REDACTED 4Xyz7Q1cKpz1 to=<a1b2c3d4e5f6> relay=203.0.113.0 delay=1.2 dsn=2.0.0 queue ID unchanged — your entire debug workflow keys off this token

Keep the queue ID, drop the identity

Walk through what you lose. Take the redacted strip above and try to answer the questions you actually ask logs:

  • Did this message deliver? status=sent, dsn=2.0.0 — still there.
  • How long did it sit in the queue? delay=1.2 — still there.
  • What did the remote say when it bounced? The dsn and the enhanced status text — still there.
  • Can I follow the whole session? grep 4Xyz7Q1cKpz1 mail.log — the queue ID is untouched, so every line from smtpd accept to final smtp delivery still stitches together.

None of that needs [email protected] in cleartext. The recipient address is only genuinely required for one workflow: a *named* user complaint — "message from Alice never reached me" — where you have to correlate on a specific address. That is real, and it is exactly why the answer is pseudonymize, not delete. Hold that thought.

Redact at syslog, not in the app

The instinct is to patch Postfix, or to run a nightly cron that scrubs yesterday's log with sed. Both are wrong. Patching source means you own a fork forever. Post-hoc scrubbing means raw PII lands on persistent storage first, gets backed up, and gets read by whoever tails the file in the gap — you have already lost. The goal is that cleartext identity never touches durable storage.

The choke point is rsyslog. Every daemon — Postfix, Dovecot, Rspamd — logs through it, so it is the one place to enforce policy once. Two mechanisms carry the load: mmanon for IPs, and an external helper for addresses.

# /etc/rsyslog.d/00-mail-redact.conf
module(load="mmanon")
module(load="omprog")

if ($programname startswith "postfix" or $programname startswith "dovecot"
    or $programname == "rspamd") then {
    # 1. Anonymize IPs in place: zero the last IPv4 octet, anonymize the IPv6 suffix
    action(type="mmanon" ipv4.mode="zero" ipv4.bits="8" ipv6.enable="on")

    # 2. Hand the IP-anonymized line to the HMAC helper, which pseudonymizes
    #    addresses and writes the redacted logfile. This action is the sink.
    action(type="omprog"
           binary="/usr/local/bin/mail-redact.sh"
           template="RSYSLOG_TraditionalFileFormat")
    stop   # don't let the default mail.* rule also persist the cleartext line
}

Order and module type matter here. mmanon is a *message-modification* module — it rewrites the IP inside the message in place, so 203.0.113.44 becomes 203.0.113.0 before anything else sees it. omprog, by contrast, is an *output* module: it pipes each line to your helper's stdin and the helper owns persistence. That is why IP anonymization runs first — by the time the line reaches the helper, the host identity is already gone and the helper only has to deal with addresses. The trailing stop is not optional: without it, rsyslog's default mail.* rule writes an untouched cleartext copy right next to your redacted one.

ipv4.mode="zero" ipv4.bits="8" keeps the /24 for abuse triage while dropping the host. If you need stable per-host pseudonyms within a run instead, use ipv4.mode="random-consistent", which maps each source IP to a fixed random value.

Journald-only hosts have no such hook. Either set Storage=volatile so nothing durable is written and forward a redacted copy to rsyslog, or add a forward hop and let rsyslog own the persistent stream. Do not try to redact inside journald — it has no field-rewrite stage.

Pseudonymize with an HMAC, don't just delete

This is where naive scrubbing fails. If you replace every to=<...> with to=<REDACTED>, you have destroyed the one legitimate correlation workflow: "show me every session for [email protected] during the incident window." You can no longer group by recipient, because they are all the same string.

A keyed HMAC fixes this. HMAC-SHA256 of the address gives a stable, deterministic token — same input always yields the same token, so you can still grep and group — but it is not reversible without the key. The helper is small:

bash
#!/bin/bash
# /usr/local/bin/mail-redact.sh — omprog sink: HMAC-pseudonymize addresses, then persist
KEY=$(cat /etc/mail-redact.key)
hmac() { printf '%s' "$1" | openssl dgst -sha256 -hmac "$KEY" -r | cut -c1-16; }
while IFS= read -r line; do
  while [[ "$line" =~ \<([^@<> ]+@[^<> ]+)\> ]]; do
    addr="${BASH_REMATCH[1]}"
    line="${line//<$addr>/<$(hmac "$addr")>}"
  done
  printf '%s\n' "$line" >> /var/log/mail-redacted.log
done

The core is printf '%s' "$addr" | openssl dgst -sha256 -hmac "$KEY" -r | cut -c1-16, producing to=<a1b2c3d4e5f6a7b8>. One caveat: a shell that forks openssl per address will not keep up with millions of lines a day — for production volume, reimplement this as a resident process (an mmexternal plugin or a small compiled filter) rather than a per-line fork.

Why HMAC and not plain SHA-256? The email address space is enumerable. If you know a target address, a plain unsalted hash is recovered by a single sha256 call — it is not a secret, it is an index. An HMAC with a secret key is a keyed MAC; without /etc/mail-redact.key an attacker cannot reproduce the token, so a leaked log stays pseudonymous.

Key hygiene:

  • chmod 0600 /etc/mail-redact.key, owner root:root. The helper runs as the rsyslog user, so read the key once at startup or grant that user read access only.
  • Rotate the key per retention epoch. Rotating deliberately severs correlation across epochs — last quarter's tokens no longer match this quarter's. That is a feature: it caps how far back any single incident can link.
  • Store the key outside your log backups, or you have handed the reverser both halves.

The quieter leaks: Dovecot auth and Rspamd

Postfix is not the only offender. Dovecot has two settings that will ruin your day if flipped. In 10-logging.conf / 10-auth.conf:

auth_verbose = yes              # fine: logs auth success/fail, no secrets
auth_verbose_passwords = no     # MUST stay no — "sha1" logs a truncated SHA1 of the password
auth_debug = no
auth_debug_passwords = no       # MUST stay no — logs near-cleartext credentials

auth_verbose = yes is safe and useful — it tells you *that* an auth failed without leaking *what*. The two *_passwords toggles are the trap; auth_debug_passwords = yes will happily write credential material to disk. Also audit the mail_log plugin — it stamps the username on every delete, expunge, and copy. Keep it only if you have an access-audit requirement.

Rspamd is the subtle one because it logs message *content* metadata. Its history and logs capture Subject, From, and extracted URLs. In options.inc:

options {
    log_urls = false;
}

Restrict the web UI and history to secure_ip ranges, keep it behind auth, and consider disabling detailed history retention entirely. Subjects leak into logs when extended_spam_headers or verbose logging is on — so don't run verbose in steady state.

And back in Postfix: SASL usernames appear on authenticated smtpd lines and are directly identifying — add them to your rewrite rule. Confirm smtpd_client_port_logging = no (the default) and never flip it on; the ephemeral client port is pure noise that only sharpens fingerprinting.

Time-box the verbosity you actually need

People keep noisy logs out of fear — "what if I need it later?" Kill that fear with targeted, temporary verbosity so your steady-state logs can be aggressively redacted. Postfix does per-peer debug without touching global logging. In main.cf:

debug_peer_list = smtp.problematic.example
debug_peer_level = 2

postfix reload, reproduce the issue, then revert and reload. That raises verbosity for exactly one remote, for exactly as long as you need it. Dovecot's auth_debug is the same pattern — flip during an incident, revert after. Deep debugging becomes opt-in and time-boxed, which is precisely what lets the default stream stay stripped.

Redaction pipeline — two streams, one choke point Postfix Dovecot Rspamd rsyslog mmanon (IP) · omprog HMAC stop — no cleartext copy 🔒 /etc/mail-redact.key 0600 Redacted stream Loki / journal · 90d Raw debug stream tmpfs · 48h · opt-in Redaction happens BEFORE persistence. Steady state = redacted 90d. Raw is volatile, short-lived, and only enabled via debug_peer_list during an incident.

Retention is redaction you get for free

The cheapest PII minimization is not writing the data. The second cheapest is deleting it fast. Split your streams: a redacted long-term stream (ship to Loki or keep in journal, 90 days) and a raw short-lived debug stream on tmpfs (48 hours, gone on reboot).

# /etc/logrotate.d/rsyslog
/var/log/mail.log {
    daily
    rotate 7
    maxage 7
    compress
    delaycompress
    missingok
    notifempty
}
# /etc/systemd/journald.conf
Storage=volatile
MaxRetentionSec=2day
SystemMaxUse=200M

Two traps. First, backups silently defeat retention — if your nightly job sweeps /var/log, your "7-day" mail log lives for years on tape. Either exclude raw logs from backups or match backup rotation to your retention. Second, legal holds override deletion; carve out a documented, scoped hold process rather than letting a lawsuit turn into "keep everything forever."

Why this is product-aligned, not just compliance

For a temp-email operator the recipient side is a high-cardinality firehose — thousands of burner addresses churning per hour. Storing raw to= next to sender IPs turns mail.log into a "who contacted which burner" index: exactly the linkage the product exists to prevent. Redacting here isn't a checkbox for the auditor, it's keeping the promise the service makes.

Operator checklist

  • mmanon loaded, zeroing the last IPv4 octet and anonymizing the IPv6 suffix.
  • Address-rewrite helper tested against a real sample line — confirm to=, from=, and the SASL username all get tokenized.
  • stop present so the default mail.* rule doesn't persist a cleartext copy.
  • HMAC key at /etc/mail-redact.key, 0600 root:root, excluded from backups.
  • Key rotation scheduled per retention epoch and documented.
  • Dovecot: auth_verbose_passwords = no, auth_debug_passwords = no — verified in effective config, not just the file.
  • Rspamd: log_urls = false

Run through it once, and the worst-case answer to "what's in your logs?" becomes queue IDs and status codes — a debugging tool, not a dossier.

Share
𝕏 TwitterLinkedInReddit
PreviousKVKK Compliance for Mail Systems Serving Turkish UsersNextEncrypting Dovecot Mailboxes at Rest with mail_crypt: Global vs Per-User Keys

Related Articles

Privacy

The DPA and Subprocessor List You Actually Need Before You Outsource Email

10 min read
Privacy

KVKK Compliance for Mail Systems Serving Turkish Users

12 min read
Privacy

Forward and Forget: Architecting a Zero-Retention Email Alias Relay

12 min read
EvilMailEvilMail

EvilMail — free temp mail and disposable email service. No registration required.

Product

FeaturesPricingAPITools

Resources

BlogFAQGlossaryRegisterAbout Us

Legal

Privacy PolicyTerms of ServiceTrust & SecurityContact
© 2026 EvilMail. All rights reserved.
, web UI behind
secure_ip
, history retention capped.
  • Postfix: smtpd_client_port_logging = no, debug_peer_list empty in steady state.
  • logrotate maxage set; journald Storage=volatile or MaxRetentionSec set.
  • Backups confirmed to honor retention (raw logs excluded).
  • One written "break glass" procedure for raw correlation during a named incident.