PasteSafe

How to mask sensitive data in logs

The reliable place to remove passwords, tokens and personal data from logs is the logger itself, before anything is written. Here is how to set that up in Node, Python, Java and Go, and what to do with logs that already contain secrets.

Mask your log in PasteSafe

Why redact at the logger

Once a line is written it gets copied: to log files, to a log shipper, to a search index that many people can query, to backups, and into exports for vendors and support. A secret that reaches the first file usually reaches all of them. Removing it where the log line is built is the one place that covers every copy.

There are two ways to do it, and most setups need both:

  • By field. With structured (JSON) logging, replace known keys such as password, authorization, cookie and email. This is exact and cheap.
  • By pattern. Run regular expressions over the message text for values that end up inside strings, such as token=... in a URL or a connection string in an exception. This catches more, costs CPU on every line, and still misses formats you did not think of.

Node.js with pino

pino has redaction built in. List the paths to replace:

const pino = require('pino');

const logger = pino({
  redact: {
    paths: ['req.headers.authorization', 'req.headers.cookie', 'user.email', 'body.password'],
    censor: '[REDACTED]',
  },
});

logger.info({ user: { id: 4821, email: 'jane.doe@example.com' } }, 'login ok');
// ... "user":{"id":4821,"email":"[REDACTED]"},"msg":"login ok"

Without censor the replacement is [Redacted]. Paths are exact, so a password nested under a key you did not list is still logged.

Python logging

A filter attached to a handler sees every record that handler writes, including records from other modules' loggers:

import logging
import re

SECRET = re.compile(r'(password|token|api_key)=[^&\s]+', re.IGNORECASE)

class RedactFilter(logging.Filter):
    def filter(self, record):
        record.msg = SECRET.sub(r'\1=[REDACTED]', record.getMessage())
        record.args = None
        return True

handler = logging.StreamHandler()
handler.addFilter(RedactFilter())
logging.basicConfig(level=logging.INFO, handlers=[handler])

logging.info('retrying %s', 'https://api.example.com/v1?token=abc123&page=2')
# INFO:root:retrying https://api.example.com/v1?token=[REDACTED]&page=2

The filter formats the message with getMessage() first, so values passed as arguments are covered too. Exception tracebacks are added later by the formatter and are not touched.

Java with Logback or Log4j 2

Both can rewrite the message in the pattern layout with a regular expression. Logback:

<encoder>
  <pattern>%d %-5level %logger{36} - %replace(%msg){'(password|token)=\S+', '$1=[REDACTED]'}%n</pattern>
</encoder>

Log4j 2:

<PatternLayout pattern="%d %-5level %logger{36} - %replace{%msg}{(password|token)=\S+}{$1=[REDACTED]}%n"/>

This only changes what that one appender prints. JSON encoders and other appenders need their own masking.

Go with log/slog

opts := &slog.HandlerOptions{
	ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
		switch a.Key {
		case "password", "token", "authorization", "email":
			return slog.String(a.Key, "[REDACTED]")
		}
		return a
	},
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
logger.Info("login ok", "user_id", 4821, "email", "jane.doe@example.com")
// {"time":"...","level":"INFO","msg":"login ok","user_id":4821,"email":"[REDACTED]"}

Test that it keeps working

Redaction breaks quietly when someone renames a field or adds a new log line. Add one test that logs a known fake secret and fails if it appears in the output. It is a few lines, and it catches the regression the day it happens.

Logs that already contain secrets

Logger redaction does nothing for files written before it existed, or for output from third party tools. When you need to share one of those, mask a copy. Before highlights what PasteSafe finds, After is its exact output:

Before
2026-09-15 09:41:07.312 ERROR SqlExceptionHelper - Connection refused: jdbc:postgresql://db.example.com:5432/billing?user=billing&password=Wint3r-Fake-Pass
2026-09-15 09:41:07.498 DEBUG RestTemplate - POST https://api.example.com/v1/charges headers=[Authorization:"Bearer FakeBearer7fK2mZpL9wR3nB8vT1yCq"]
2026-09-15 09:41:07.502 INFO  ChargeService - charge failed for jane.doe@example.com, retrying from 203.0.113.9
2026-09-15 09:41:07.610 WARN  LoginController - login failed for admin with password hunter2
After PasteSafe
2026-09-15 09:41:07.312 ERROR SqlExceptionHelper - Connection refused: jdbc:postgresql://db.example.com:5432/billing?user=billing&password=PASSWORD_1
2026-09-15 09:41:07.498 DEBUG RestTemplate - POST https://api.example.com/v1/charges headers=[Authorization:"Bearer BEARER_TOKEN_1"]
2026-09-15 09:41:07.502 INFO  ChargeService - charge failed for EMAIL_1, retrying from IP_1
2026-09-15 09:41:07.610 WARN  LoginController - login failed for admin with password hunter2
  1. Paste the log into PasteSafe or drop the file onto the editor.
  2. Known key formats (more than 200 rules from gitleaks), Bearer and Basic headers, passwords in connection strings, values under names like password, secret or token, emails, IP addresses, card numbers, IBANs and phone numbers become placeholders.
  3. Copy the cleaned text into the ticket, chat or AI assistant.

If a real key was in a log other people could read, rotate it. See what to do if you leaked an API key.

What PasteSafe does not catch

  • Passwords written as plain words in a sentence, like the last line of the example.
  • Secrets in formats it has no rule for, when they sit under a neutral name and do not look random enough to count as high entropy.
  • Names, street addresses and other personal data without a fixed format.

Questions

What is log masking?

Log masking replaces sensitive values in log output, such as passwords, tokens, card numbers and emails, with a fixed marker or a placeholder, so the log can be stored and read without exposing those values.

Should I mask in the application or in the log pipeline?

In the application when you can, because every later copy is then clean. Masking in a log shipper or log platform is a useful second layer, but by then the raw line has already been written somewhere.

Is hashing an email address enough?

Not to hide who the user is. Email addresses are easy to guess, so a plain hash can be matched by hashing a list of candidate addresses. A hash is useful for grouping lines by user, not for keeping the user private.

Can PasteSafe replace redaction in my logging code?

No. PasteSafe cleans a copy of a log before you share it. It does not change the logs your services write, so set up redaction in the logger as well.

Clean it before you paste it

PasteSafe masks API keys, passwords and personal data in your browser. Nothing is uploaded.

Mask your log in PasteSafe