Security Auditing in Payara Server - Part 2

Photo of Fabio Turizo by Fabio Turizo

Following up from the first part of the Security Auditing  article, where we covered the audit logging,  in this part we will focus on creating a custom audit module.

 

Creating a Custom Audit Module

 

Imagine that you are responsible for monitoring the access of specific high risk users that use the applications deployed on a Payara Server instance. There could be a need to monitor the access of these in your applications, so detecting unauthenticated and unauthorized access attempts to some services and notifying the relevant company staff of these access attempts will be important to you. 

 

In the previous blog  we mentioned that implementing an audit module is easy. Here we will give instructions on how to create a sample custom module that follows these requirements we have set.

 

First, we start by creating a sample Java application using Maven. Keep in mind that to write our audit module, we need access to the AuditModule class which is an internal class to Payara Server. We can access it using the payara-embedded-all dependency. Here is the POM structure we will use:

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>fish.payara.support</groupId>
    <artifactId>CustomAuditModule</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-embedded-all</artifactId>
            <version>4.1.1.171.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>custom-audit-module</finalName>
    </build>
</project>

 

Now, we need to create our custom audit module. Since we are only interested in detecting failed authentication and authorization events, we will proceed to override the following methods:

  • init(Properties properties)
  • authentication(String user, String realm, boolean success)
  • webInvocation(String user, HttpServletRequest request, String type, boolean success)

  • ejbInvocation(String user, String ejb, String method, boolean success)

1 - Since we want to notify interested parties on invalid security events by email, it's best to reuse a JavaMail session object, and we need both an address that will be used as the sender of the message and a list of the interested parties email addresses that will work as recipients of the message. We will use these values of properties for our audit module and configure them in the init method:

 

init method

@Override
public void init(Properties properties) {
   this.mailSessionResourceName = properties.getProperty("mailSession");
   this.fromAddress = properties.getProperty("fromAddress");
   this.recipients = properties.getProperty("recipients", "");
   this.highRiskUsers = Arrays.asList(properties.getProperty("users", "").split(","))
                        .stream()
                        .map(String::trim)
                        .collect(Collectors.toSet());
}

 

The purpose of this audit module is to monitor invalid attempts made by a list of high risk users, so we also need to configure this list using a property. You can see that we are parsing this property as a comma-separated list of values and dropping them into a set for easy comparison.

 

Notice that we are also receiving a property that represents the JNDI reference name of a pre-configured JavaMail session object that will be used to send the email notifications.

 

2 - Since we want to notify on failed authentication events, we need to override the authentication method, and implement a simple check for the user and success of the event:

 

authentication method
@Override
public void authentication(String user, String realm, boolean success) {
    if (!success && highRiskUsers.contains(user)) {
        LOG.log(Level.WARNING, "Audit: Detected invalid authentication attempt by user {0}, notifying staff.", user);
        this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authentication attempt at %s", user, LocalDateTime.now()));
    }
}

 

Simple! We are also logging our decision to send the notification email, which has a simple summary of the invalid event and a timestamp for time tracking purposes.

 

3 - Also, we want to notify on failed authorization events, so we will override the webInvocation and ejbInvocation methods and implement similar checks to the one we did on the previous method (We are ignoring the webService methods to keep the example as simple as possible):

 

authorization methods
@Override
public void webInvocation(String user, HttpServletRequest request, String type, boolean success) {
    if (!success && highRiskUsers.contains(user)) {
        LOG.log(Level.WARNING, "Audit: Detected invalid authorization attempt by user {0}, notifying staff.", user);
        this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authorization attempt at %s, trying to access resource %s on method %s", user, LocalDateTime.now(), request.getRequestURI(), request.getMethod()));
    }
}
 
@Override
public void ejbInvocation(String user, String ejb, String method, boolean success) {
    if (!success && highRiskUsers.contains(user)) {
        LOG.log(Level.WARNING, "Audit: Detected invalid authorization attempt by user {0}, notifying staff.", user);
        this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authorization attempt at %s, trying to access EJB %s, method %s", user, LocalDateTime.now(), ejb, method));
    }
}

 

Pretty straightforward! As with the previous method, we are also logging our decisions to send notification emails, which contain simple summaries and timestamps as well.

Here is the complete code of the class, along with the methods used to send the email notification and getting the JavaMail Session object as well:

 

HighRiskUsersAuditModule.java
public class HighRiskUsersAuditModule extends AuditModule {
 
    private static final Logger LOG = Logger.getLogger(HighRiskUsersAuditModule .class.getName());
    private static final String EMAIL_SUBJECT = "Audit Notification from Payara Server";
 
    private String mailSessionResourceName;
    private String fromAddress;
    private String recipients;
    private Set<String> highRiskUsers;
 
    @Override
    public void init(Properties properties) {
        this.mailSessionResourceName = properties.getProperty("mailSession");
        this.fromAddress = properties.getProperty("fromAddress");
        this.recipients = properties.getProperty("recipients", "");
        this.highRiskUsers = Arrays.asList(properties.getProperty("users", "").split(","))
                .stream()
                .map(String::trim)
                .collect(Collectors.toSet());
    }
 
    @Override
    public void authentication(String user, String realm, boolean success) {
        if (!success && highRiskUsers.contains(user)) {
            LOG.log(Level.WARNING, "Audit: Detected invalid authentication attempt by user {0}, notifying staff.", user);
            this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authentication attempt at %s", user, LocalDateTime.now()));
        }
    }
 
    @Override
    public void webInvocation(String user, HttpServletRequest request, String type, boolean success) {
        if (!success && highRiskUsers.contains(user)) {
            LOG.log(Level.WARNING, "Audit: Detected invalid authorization attempt by user {0}, notifying staff.", user);
            this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authorization attempt at %s, trying to access resource %s on method %s", user, LocalDateTime.now(), request.getRequestURI(), request.getMethod()));
        }
    }
 
    @Override
    public void ejbInvocation(String user, String ejb, String method, boolean success) {
        if (!success && highRiskUsers.contains(user)) {
            LOG.log(Level.WARNING, "Audit: Detected invalid authorization attempt by user {0}, notifying staff.", user);
            this.sendEmail(EMAIL_SUBJECT, String.format("The user %s has made an invalid authorization attempt at %s, trying to access EJB %s, method %s", user, LocalDateTime.now(), ejb, method));
        }
    }
 
    private void sendEmail(String subject, String text) {
        Session mailSession = getEmailSession();
        if (mailSession != null) {
            try {
                Message message = new MimeMessage(mailSession);
                message.setFrom(new InternetAddress(fromAddress));
                message.setRecipients(RecipientType.TO, InternetAddress.parse(recipients));
                message.setSubject(subject);
                message.setText(text);
 
                Transport.send(message);
            } catch (MessagingException ex) {
                LOG.log(Level.SEVERE, "Audit: Error sending email notification", ex);
            }
        }
    }
 
    private Session getEmailSession() {
        try {
            InitialContext context = new InitialContext();
            return (Session) context.lookup(this.mailSessionResourceName);
        } catch (NamingException ex) {
            LOG.log(Level.SEVERE, "Audit: No mail session resource with name " + this.mailSessionResourceName + " found in the server", ex);
            return null;
        }
    }
}

 

Now that we have written the code for our audit module, run a simple mvn install command to generate the JAR file that contains our class. Put this JAR either into the ${PAYARA_INSTALL_DIR}/lib directory (so it applies to all domains) or the ${DOMAIN_DIR}/lib directory (so it applies to a specific domain only). This is to make sure that the audit module is on the server classpath for registration.

Before proceeding to register the module on the server, we need a JavaMail session resource that can be used to send notification emails. For testing purposes, we will run the following asadmin command to create a session that will connect to an Office 365 mail account:

 

JavaMail session resource
create-javamail-resource --debug=false --storeProtocol=imap --transportProtocol=smtp --host=m.outlook.com --storeProtocolClass=com.sun.mail.imap.IMAPStore --from=fabio.turizo@payara.fish --transportProtocolClass=com.sun.mail.smtp.SMTPTransport --user=fabio.turizo@payara.fish --enabled=true --target=domain --property "mail.smtp.port=587:mail.smtp.auth=true:mail.smtp.starttls.enable=true:mail.smtp.ssl.trust=m.outlook.com:mail.smtp.password=XXXXXXX" mail/Office365Session
 
create-resource-ref --enabled=true --target=server mail/Office365Session

 

Finally, we need to register the audit module on Payara Server. We can do this by going to the server-config → Security → Audit Modules section in the administration console and pressing the New button:

 

New Audit Module Image

 

You can also achieve the same result with the following asadmin command:

 
JavaMail session resource
asadmin create-audit-module --classname fish.payara.support.customaudit.HighRiskUsersAuditModule --property mailSession=mail/Office365Session:fromAddress=security-audit@payara.fish:recipients=sample.user@payara.fish:users=bob high-risk-users

 

With our audit module correctly configured, we will use the test application we developed to test the default module. Since user bob is on the high users list and he doesn't have access to the /execute endpoint, let's issue a request using cURL:

 
Second endpoint test
curl -u bob:bob -X POST http://localhost:8080/audit-module-test/resources/execute --data "This is a test message from bob, which will be denied from JAAC authorization"

 

This will generate the following log entries on the server and the following email notification will be delivered to the configured address, sample.user@payara.fish:

 
Audit Trail #3
Warning:   Audit: Detected invalid authorization attempt by user bob, notifying staff

 

security auditing 5.jpg 

And that's it! Our custom audit module is working correctly and auditing invalid attempts made by the users in the configured list, as intended.

 

Asynchronous notification warning

Keep in mind that that the code in the overridden methods of the audit module will be called in a synchronous manner, which can slow down and affect the performance of secured applications. If you want to implement this code on a production environment, we would recommend wrapping the email notification code in an asynchronous block (you can even get a ManagedExecutorService via JNDI as well), to avoid impacting application performance in a negative way.

 

Summary

Security audit modules are an obscure feature inherited from GlassFish, but a powerful feature nonetheless. With the default audit module shipped into Payara Server you already can create a simple audit trail of all the authentication and authorization decisions made by all the application you have deployed on the server, which is a good starting point for many organizations considering implementing security auditing into their applications.

 

If you need to go beyond those capabilities and create specific audit reactions based on the security requirements set by your organization, developing your own modules which can use the server's resources as in this example will be very helpful. The pluggable nature of these modules makes them easy to integrate into the server's workings, and their customization capabilities allows for greater fine tuning; is the default logging slow for critical applications? Then implement a module that uses faster logging mechanism, for example!

 

If you are considering using this feature but think it could be improved by including more entry points for authorization requests or include more information to the audit modules themselves, then let us know! We are open to any suggestions that help us improve and expand the uses cases for audit modules.

 

 

 

Comments