Secure Spring Boot logs: Automatic masking of passwords, API keys, and tokens
Posted By
Rushabh Bansod
Passwords, API keys, and authentication tokens are constantly at risk in Spring Boot applications. They leak into logs through HTTP clients, framework internals, exception messages, and custom debug code. Once in logs, they're shipped to centralized platforms like Elasticsearch, Datadog, and Splunk where multiple teams, analysts, and support staff can find them by searching. Secrets stay indexed and archived for months or years. People without production access see credentials they shouldn't. That's how compliance violations happen.
This guide shows you how to automatically detect and mask sensitive data in Spring Boot logs using a custom Logback filter. No code changes needed. No modifications to existing logger calls. The masking works transparently in the background, protecting secrets before logs reach your system.
Why log security matters for Spring Boot applications
Real applications leak secrets constantly. Not because developers are careless, but because secrets hide in unexpected places. HTTP client libraries log request bodies. Framework internals expose credentials. Exception messages contain tokens. Custom debug code prints things it shouldn't.
Here's what typical Spring Boot logs look like before masking:
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Sending request: { "username":"admin", "password":"mySecret123" }
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Received token: access_token=eyJhbGciOirtctgf24c...
The password and API key are there in plain text. Visible to anyone with access to your logs.
How to secure Spring Boot logs with automatic masking
Logging is essential for debugging and monitoring, but sensitive data in logs becomes a security liability. Passwords, tokens, and API keys must never be printed in plain text.
This guide presents a practical approach using a custom Logback filter. The solution:
- Detects sensitive fields inside log messages
- Masks their values with asterisks
- Preserves log entries without skipping them
- Supports JSON, query parameters, and HTTP headers
- Applies globally without modifying existing logger calls
- Intercepts all log events before they reach appenders
The centralized logging problem
Even with careful coding, logs generated by HTTP clients, frameworks, libraries, custom debug prints, and exception messages can unintentionally expose secrets.
In modern applications, logs do far more than sit inside a server's /var/logs directory. Almost every organization uses centralized logging and observability platforms:
- Elasticsearch + Kibana (ELK Stack)
- Grafana Loki
- Splunk
- Datadog
- CloudWatch Logs
When logs reach these platforms, secrets become searchable across your infrastructure. Support teams, analysts, and developers can find credentials by typing a keyword. Secrets stay stored for months or years. People without production access suddenly see sensitive information.
This is why implementing log masking for Spring Boot applications moved from nice-to-have to mandatory. For teams using cloud infrastructure, learn more about cloud security and monitoring strategies.
What you gain with automatic log masking
- Stronger application security across your infrastructure
- Compliance with regulations like GDPR, HIPAA, and PCI DSS
- Protection against insider threats and accidental exposure
- Ability to use logs for debugging without hiding important information
- Automatic background processing with no manual intervention
Steps to building a log masking solution for Spring Boot
The approach uses two core pieces.
- A startup hook that attaches a masking filter to all Logback appenders
- A custom Logback filter that detects and masks secrets inside log messages
Step 1: registering the log masking filter at application startup
This component runs when Spring Boot starts. It accesses the Logback context, creates your masking filter, and attaches it to every appender.
Securing your application involves multiple layers. Beyond logging security, implement proper authentication. Check out our guide on securing Spring Boot applications with SAML authentication to add enterprise-grade authentication.
@Component
public class LogSanitizerInitializer {
@EventListener(ApplicationStartedEvent.class)
public void registerSanitizerFilter() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger baseLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
SensitiveDataSanitizer sanitizerFilter = new SensitiveDataSanitizer();
sanitizerFilter.start();
Iterator<Appender<ILoggingEvent>> appenderIterator = baseLogger.iteratorForAppenders();
while (appenderIterator.hasNext()) {
Appender<ILoggingEvent> appender = appenderIterator.next();
appender.addFilter(sanitizerFilter);
}
}
}
Step 2: building the sensitive data masking filter
Your filter class uses predefined constants to identify what needs masking. It precompiles regex patterns so matching is fast. It handles JSON, query parameters, and header-style logs.
public class SensitiveDataSanitizer extends Filter<ILoggingEvent> {
private static final String MASKED_VALUE = "********";
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList(
"password", "apikey", "secret", "token", "credential", "key",
"accesskeyid", "secretaccesskey", "sessiontoken",
"access_token", "refresh_token", "authorization"
));
private static final Set<String> MASKABLE_PACKAGES = new HashSet<>(Arrays.asList(
"org.apache.http.wire",
"org.apache.hc.client5.http.wire"
));
}
We precompiled regex patterns for JSON, URL params, and header-style logs:
private static final Map<String, Pattern> JSON_PATTERNS = new HashMap<>();
private static final Map<String, Pattern> URL_PATTERNS = new HashMap<>();
private static final Map<String, Pattern> HEADER_PATTERNS = new HashMap<>();
private static final Map<String, Pattern> FIELD_DETECTION_PATTERNS = new HashMap<>();
static { for (String field : SENSITIVE_FIELDS) { JSON_PATTERNS.put(field, Pattern.compile("("" + field + ""\s*:\s")(.?)(")", Pattern.CASE_INSENSITIVE));
URL_PATTERNS.put(field,
Pattern.compile("(" + field + "=)([^&\\s]*)", Pattern.CASE_INSENSITIVE));
HEADER_PATTERNS.put(field,
Pattern.compile("(" + field + ":\\s*)(.*?)(\\r?\\n|$)", Pattern.CASE_INSENSITIVE));
FIELD_DETECTION_PATTERNS.put(field,
Pattern.compile("\\b" + field + "\\b", Pattern.CASE_INSENSITIVE));
}
}
Step 3: intercepting and scrubbing log messages
When a log event happens, this filter intercepts it before reaching appenders or external systems. It checks for protected field names and applies masking if found. If not, it passes the log through unchanged.
Effective log filtering requires understanding how authentication data flows through your system. Review our guide on Java Spring security with custom authentications to see how authentication data leaks into logs.
@Override
public FilterReply decide(ILoggingEvent event) {
String originalMsg = event.getFormattedMessage();
if (originalMsg == null) {
return FilterReply.NEUTRAL;
}
Set<String> keysFound = PROTECTED_KEYS.stream()
.filter(k -> triggerPatterns.get(k).matcher(originalMsg).find())
.collect(Collectors.toSet());
if (keysFound.isEmpty()) {
return FilterReply.NEUTRAL;
}
String sanitizedMessage = applyMasking(originalMsg, keysFound);
try {
Field msgField = event.getClass().getDeclaredField("formattedMessage");
msgField.setAccessible(true);
msgField.set(event, sanitizedMessage);
} catch (Exception e) {
return FilterReply.NEUTRAL; // graceful fallback
}
return FilterReply.NEUTRAL;
}
Key behavior built into this approach:
- Only messages containing protected field names are scanned
- Nothing is dropped — logs still print
- Only values are replaced
Step 4: implementing the masking logic (Sanitizer Engine)
The actual masking engine handles all three data formats. It uses the patterns you defined earlier to find values and replace them with asterisks.
private String applyMasking(String message, Set<String> keys) {
String updated = message;
for (String key : keys) {
updated = jsonPatterns.get(key)
.matcher(updated)
.replaceAll("$1" + REDACTION + "$3");
updated = queryPatterns.get(key)
.matcher(updated)
.replaceAll("$1" + REDACTION);
updated = headerPatterns.get(key)
.matcher(updated)
.replaceAll("$1" + REDACTION + "$3");
}
return updated;
}
How log masking works across different data formats
Your masking filter handles JSON bodies, query string parameters, and HTTP headers. Each requires a different regex pattern, but the result is the same.
For larger systems with event-driven patterns, consistent security practices across different flows are critical. Learn more in our guide on Spring Boot event-driven architecture for backend automation to ensure secure event handling.
Masking results by format type
| Format Type | Example | Masking Result |
|---|---|---|
| JSON | "password":"mypass" | "password":"********" |
| Query Params | apiKey=123456 | apiKey=******** |
| Headers | Authorization: Bearer gns13 | Authorization: ******** |
Before and after comparison
Your logs remain completely functional while secrets stay completely hidden.
Before masking
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Sending request: { "username":"admin", "password":"mySecret123" }
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Received token: access_token=eyJhbGciOirtctgf24c...\
After masking
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Sending request: { "username":"admin", "password":"********" }
2026-02-06T14:39:09.034+05:30 INFO --- [<service_name>] [main] <package_path> : Received token: access_token=********
Logs remain fully usable while secrets stay fully protected.
Moving forward with secure Spring Boot logging
This log masking approach keeps logs fully usable for debugging while protecting secrets from the moment they're written. Developers need no code changes. The filter runs on every event with minimal performance impact, scanning only messages containing protected field names.
When you add this layer to Spring Boot, you build confidence that centralized logging platforms, compliance audits, and incident investigations won't expose your infrastructure secrets. That's the foundation of production-ready applications.
If you need expert help implementing secure logging practices, securing your entire Spring Boot infrastructure, or building a complete security strategy for your applications, contact Opcito. Our team specializes in application security, DevOps security, and helping organizations build production-ready systems that protect sensitive data at every layer.













