Log4j2 offers a number of ways to accomplish this:
- Filters
- Rewriting Log Events
Filters
Log4j2 allows you to configure filters on specific loggers, or on specific appenders, or globally (so the filter applies to all log events). What filters give you is the ability to force-ACCEPT a log event, or force-DENY a log event, or to be "NEUTRAL". In your case you may want to DENY log events that contain sensitive data.
You can create a custom Filter implementation (see the plugin docs for how to install your custom filter), or you can use some of the built-in filters. Either the built-in RegexFilter or a ScriptFilter should be sufficient for your purposes.
Regex filter example
Let's say that this is a class that contains sensitive data:
public class Customer {
public String name;
public String password;
@Override
public String toString() {
return "Customer[name=" + name + ", password=" + password + "]";
}
}
And your application logging looks something like this:
public class CustomerLoggingApp {
public static void main(String[] args) {
Logger log = LogManager.getLogger();
Customer customer = new Customer();
customer.name = "Jesse Zhuang";
customer.password = "secret123";
log.info("This is sensitive and should not be logged: {}", customer);
log.info("But this message should be logged.");
}
}
You can configure a regex filter that looks at the formatted (or unformatted) message and DENY any log messages that have the word "Customer
" followed by ", password=
" in them:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<RegexFilter regex=".*Customer.*, password=.*" onMatch="DENY" onMismatch="NEUTRAL"/>
<PatternLayout>
<pattern>%d %level %c %m%n</pattern>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
Script filter example
Another very flexible filter is ScriptFilter. The below example uses Groovy, but you can also use JavaScript or whatever other scripting language is available on your Java installation.
Given the above application classes, the following log4j2.xml
configuration would filter out any log events containing any parameters whose fully qualified class name is Customer
:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<ScriptFilter onMatch="DENY" onMisMatch="NEUTRAL">
<Script name="DropSensitiveObjects" language="groovy"><![CDATA[
parameters.any { p ->
// DENY log messages with Customer parameters
p.class.name == "Customer"
}
]]>
</Script>
</ScriptFilter>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout>
<pattern>%d %level %c %m%n</pattern>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
Rewriting Log Events
Another interesting option is to rewrite the log event so that the message does not get completely filtered out, but instead you just mask the sensitive data. For example you replace the password string with "***" in the log.
To do this, you create a RewriteAppender. From the manual:
The RewriteAppender allows the LogEvent to manipulated before it is
processed by another Appender. This can be used to mask sensitive
information such as passwords or to inject information into each
event.
Example rewrite policy:
package com.jesse.zhuang;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.ObjectMessage;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.message.ReusableMessage;
@Plugin(name = "MaskSensitiveDataPolicy", category = Core.CATEGORY_NAME,
elementType = "rewritePolicy", printObject = true)
public class MaskSensitiveDataPolicy implements RewritePolicy {
private String[] sensitiveClasses;
@PluginFactory
public static MaskSensitiveDataPolicy createPolicy(
@PluginElement("sensitive") final String[] sensitiveClasses) {
return new MaskSensitiveDataPolicy(sensitiveClasses);
}
private MaskSensitiveDataPolicy(String[] sensitiveClasses) {
super();
this.sensitiveClasses = sensitiveClasses;
}
@Override
public LogEvent rewrite(LogEvent event) {
Message rewritten = rewriteIfSensitive(event.getMessage());
if (rewritten != event.getMessage()) {
return new Log4jLogEvent.Builder(event).setMessage(rewritten).build();
}
return event;
}
private Message rewriteIfSensitive(Message message) {
// Make sure to switch off garbage-free logging
// by setting system property `log4j2.enable.threadlocals` to `false`.
// Otherwise you may get ReusableObjectMessage, ReusableParameterizedMessage
// or MutableLogEvent messages here which may not be rewritable...
if (message instanceof ObjectMessage) {
return rewriteObjectMessage((ObjectMessage) message);
}
if (message instanceof ParameterizedMessage) {
return rewriteParameterizedMessage((ParameterizedMessage) message);
}
return message;
}
private Message rewriteObjectMessage(ObjectMessage message) {
if (isSensitive(message.getParameter())) {
return new ObjectMessage(maskSensitive(message.getParameter()));
}
return message;
}
private Message rewriteParameterizedMessage(ParameterizedMessage message) {
Object[] params = message.getParameters();
boolean changed = rewriteSensitiveParameters(params);
return changed ? new ParameterizedMessage(message.getFormat(), params) : message;
}
private boolean rewriteSensitiveParameters(Object[] params) {
boolean changed = false;
for (int i = 0; i < params.length; i++) {
if (isSensitive(params[i])) {
params[i] = maskSensitive(params[i]);
changed = true;
}
}
return changed;
}
private boolean isSensitive(Object parameter) {
return parameter instanceof Customer;
}
private Object maskSensitive(Object parameter) {
Customer result = new Customer();
result.name = ((Customer) parameter).name;
result.password = "***";
return result;
}
}
CAUTION: When running in garbage-free mode (the default), Log4j2 uses reusable objects for messages and log events. These are not
suitable for rewriting. (This is not documented well in the user
manual.) If you want to use the rewrite appender, you need to
partially switch off garbage-free logging by setting system property
log4j2.enable.threadlocals
to false
.
Configure your rewrite appender with your custom MaskSensitiveDataPolicy
rewrite policy. To let Log4j2 know about your plugin, specify the name of the package of your plugin in the packages
attribute of the Configuration
:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" packages="com.jesse.zhuang">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout>
<pattern>%d %level %c %m%n</pattern>
</PatternLayout>
</Console>
<Rewrite name="obfuscateSensitiveData">
<AppenderRef ref="Console"/>
<MaskSensitiveDataPolicy />
</Rewrite>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="obfuscateSensitiveData"/>
</Root>
</Loggers>
</Configuration>
This will make the above example program produce the following output. Note that the password is masked but other attributes of the sensitive object are preserved:
2018-01-09 22:18:30,561 INFO CustomerLoggingApp This is sensitive and should not be logged: Customer[name=Jesse Zhuang, password=***]
2018-01-09 22:18:30,569 INFO CustomerLoggingApp But this message should be logged.