Security

How to Keep Sensitive Data Out of Your Logs (PII, API Keys, Passwords)

Logging PII, API keys, or passwords creates compliance violations and security risks. Here's how to build automatic redaction into your logging pipeline.

LogFlow TeamAugust 18, 20268 min read

Every team that logs request bodies eventually logs a password. Every team that logs headers eventually logs an API key. Every team that logs user data eventually has a GDPR conversation they weren't prepared for.

It's not malicious — it's structural. The same practice that makes debugging effective (log everything with context) creates security and compliance risks when "everything" includes sensitive data.

This guide covers what to redact, how to automate it, and how to build a logging pipeline that's useful for debugging without being a liability.

What Counts as Sensitive Data

Always redact (no exceptions)

Data type Common log sources Risk
Passwords Login endpoints, auth middleware Account takeover
API keys / tokens Authorization headers, webhook payloads Service impersonation
Credit card numbers Payment endpoints, billing webhooks PCI DSS violation
Social security / ID numbers User registration, KYC flows Identity theft
Session tokens / JWTs Auth middleware, cookie logging Session hijacking

Redact by default (unless compliance allows it)

Data type Regulation Notes
Email addresses GDPR, CCPA Often needed for debugging — hash or mask instead
Phone numbers GDPR, CCPA Mask: +1 *** *** 4821
IP addresses GDPR (EU considers IPs personal data) Hash or truncate: 203.0.113.xxx
Full names GDPR, CCPA Usually unnecessary in logs
Physical addresses GDPR, CCPA Never useful for debugging

Context-dependent

Data type When to log When to redact
User IDs Always fine — internal identifiers
Order IDs Always fine — business references
Request paths / methods Always fine Except when paths contain tokens (/reset?token=...)
Error messages Usually fine When they echo user input (SQL errors with query params)

Approach 1 — Redact at the Logger Level

Build redaction into the logging library so it applies everywhere automatically:

Node.js with Pino

import pino from 'pino'

const sensitiveKeys = ['password', 'token', 'apiKey', 'secret', 'authorization',
  'creditCard', 'ssn', 'cardNumber', 'cvv', 'sessionToken']

const logger = pino({
  redact: {
    paths: sensitiveKeys.map(k => `*.${k}`),
    censor: '[REDACTED]'
  }
})

// Automatically redacted:
logger.info({ user: 'alex', password: 's3cret123' }, 'Login attempt')
// Output: {"user":"alex","password":"[REDACTED]","msg":"Login attempt"}

Pino's redact option uses fast path matching — no regex overhead on every log call.

Python with structlog

import structlog
import re

SENSITIVE_PATTERNS = {
    'password': re.compile(r'.*'),
    'token': re.compile(r'.*'),
    'api_key': re.compile(r'.*'),
    'authorization': re.compile(r'.*'),
    'credit_card': re.compile(r'\d{13,19}'),
    'ssn': re.compile(r'\d{3}-?\d{2}-?\d{4}'),
}

def redact_sensitive(logger, method_name, event_dict):
    for key in list(event_dict.keys()):
        key_lower = key.lower()
        if any(s in key_lower for s in SENSITIVE_PATTERNS):
            event_dict[key] = '[REDACTED]'
    return event_dict

structlog.configure(
    processors=[
        redact_sensitive,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer(),
    ]
)

Approach 2 — Redact at the Middleware Level

Sanitize request/response objects before they reach logging:

function sanitizeHeaders(headers) {
  const sanitized = { ...headers }
  const redactKeys = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']

  for (const key of redactKeys) {
    if (sanitized[key]) {
      sanitized[key] = '[REDACTED]'
    }
  }
  return sanitized
}

function sanitizeBody(body) {
  if (!body || typeof body !== 'object') return body

  const sanitized = { ...body }
  const redactKeys = ['password', 'confirmPassword', 'currentPassword',
    'token', 'secret', 'cardNumber', 'cvv', 'ssn']

  for (const key of redactKeys) {
    if (sanitized[key]) sanitized[key] = '[REDACTED]'
  }
  return sanitized
}

// In your request logging middleware:
logger.info({
  method: req.method,
  path: req.url,
  headers: sanitizeHeaders(req.headers),
  body: sanitizeBody(req.body)
}, 'Incoming request')

Approach 3 — Pattern-Based Redaction

Catch sensitive data by format, regardless of field name. This catches cases where sensitive data ends up in unexpected fields (error messages, stack traces, free-text descriptions):

function redactPatterns(text) {
  if (typeof text !== 'string') return text

  return text
    // Credit card numbers (13-19 digits, with or without spaces/dashes)
    .replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/g, '[CARD_REDACTED]')
    // Email addresses
    .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL_REDACTED]')
    // JWT tokens
    .replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[JWT_REDACTED]')
    // API keys (common formats)
    .replace(/\b(sk|pk|api|key|token)[-_][a-zA-Z0-9]{20,}\b/gi, '[KEY_REDACTED]')
    // SSN (US)
    .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN_REDACTED]')
}

Pattern-based redaction is a safety net, not a primary strategy. It catches things that key-based redaction misses, but it also has false positives. Use it as a second layer.

Approach 4 — Masking Instead of Full Redaction

Sometimes partial data is needed for debugging. Masking preserves enough context to identify records without exposing the full value:

function maskEmail(email) {
  const [local, domain] = email.split('@')
  return `${local[0]}***@${domain}`
  // "alex.b@gmail.com" → "a***@gmail.com"
}

function maskCard(number) {
  return `****${number.slice(-4)}`
  // "4242424242424242" → "****4242"
}

function maskIp(ip) {
  return ip.replace(/\.\d+$/, '.xxx')
  // "203.0.113.42" → "203.0.113.xxx"
}

This lets the team identify "which user had the issue?" without exposing the full email. A masked card number helps confirm "was it the Visa ending in 4242?" without logging the full number.

Testing Your Redaction

Redaction that isn't tested is redaction that doesn't work. Write tests that deliberately log sensitive data and verify it's removed:

describe('log redaction', () => {
  it('redacts passwords from log output', () => {
    const output = captureLogOutput(() => {
      logger.info({ username: 'alex', password: 'secret123' }, 'Login')
    })

    expect(output).toContain('alex')
    expect(output).not.toContain('secret123')
    expect(output).toContain('[REDACTED]')
  })

  it('redacts credit card patterns from message strings', () => {
    const output = captureLogOutput(() => {
      logger.info('Payment with card 4242 4242 4242 4242 failed')
    })

    expect(output).not.toContain('4242 4242 4242 4242')
    expect(output).toContain('[CARD_REDACTED]')
  })
})

Run these tests in CI. A code change that breaks redaction should fail the build, not show up in a compliance audit.

Ingestion-Level Redaction

Some log management platforms support server-side redaction — sensitive data is stripped before it's stored, even if the application sends it.

LogFlow, for example, has a per-project PII masking toggle that automatically redacts common patterns (emails, IPs, card numbers) at ingestion time. This acts as a safety net when application-level redaction misses something.

Server-side redaction doesn't replace application-level redaction — it supplements it. The best approach is defense in depth: redact at the logger, redact at the middleware, and redact at the platform.

The Checklist

  • Sensitive field names are redacted in the logger configuration (password, token, apiKey, secret, authorization)
  • Request headers are sanitized before logging (Authorization, Cookie, X-API-Key)
  • Request/response bodies are sanitized (passwords, card numbers, SSN)
  • Pattern-based redaction catches sensitive data in unexpected fields
  • Redaction is tested in CI — failing tests break the build
  • Log retention policies comply with GDPR/CCPA deletion requirements
  • The team has reviewed what data actually appears in production logs (not just what's supposed to appear)

The last point is often overlooked. Run a query against actual production logs for patterns like password, secret, @gmail.com, or credit card regex. The results are usually surprising.


The goal isn't to log less — it's to log smart. Structured fields with automatic redaction give debugging context without security risk. Every field that reaches the log store should be there intentionally, not accidentally.

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free