sorenpoulsen.com header

Sandboxing Tomcat with a Security Manager

With Java's Security Manager we can sandbox Apache Tomcat to protect the host operating system and to some degree also segregate and protect the various code bases running inside Tomcat.

How the security manager works

Java code running in a JVM, with the security manager enabled, has no permission to access sensitive resources unless explicitly granted, with the exception of reading its own code base whether that might be a jar file, a location in the files system or an address on the network.

A collection of permissions assigned to a code base is called a protection domain. Protection domains are configured in a policy file.

As an example the following clause from a policy file grants the code base postgresql.jar permission to connect to a database running on host 127.0.0.1 port 5432.

grant codeBase "jar:file:${catalina.home}/webapps/myapp/WEB-INF/lib/postgresql.jar!/-" {
      permission java.net.SocketPermission "127.0.0.1:5432", "connect,resolve";
};

When the postgresql.jar code base exercises its permission to connect to the database, the security manager checks that all callers on the stack have been granted the permission to connect to the database. This protects the Postgresql client from being tricked into doing something on behalf of another code base that is not trusted to use the Postresql client.

If the Postgresql client is going to be called legitimately by Hibernate which is called from Spring-mvc, then we must grant permission to connect to the database to both Hibernate and Spring-mvc as well.

Enable the security manager

Continuing from the previous post on installing Tomcat I'm going to assume that Tomcat is installed under /usr/local/apache-tomcat-8.0.32 and is accessed through a symbolic link /usr/local/tomcat.

Now create a file named tomcat/bin/setenv.sh

$ cd /usr/local
$ vi tomcat/bin/setenv.sh

Add the following content to the file and save it:

export CATALINA_HOME="/usr/local/tomcat"
export CATALINA_OPTS="-Djava.security.manager -Djava.security.policy==/usr/local/tomcat/conf/catalina.policy -Djava.security.debug=all"

Tomcat's startup script tomcat/bin/startup.sh will source the setenv.sh file if it exists before starting the server.

Here follows an explanation of the parameters added through CATALINA_OPTS to Java's command line:

CATALINA_OPTS 
-Djava.security.managerEnables the Java security manager.
-Djava.security.policy==/usr/local/tomcat/conf/catalina.policySets the security manager policy file.
-Djava.security.debug=allLog all failed permissions to console. For other debug options look inside sun.security.util.Debug.java

The catalina.policy file already has the necessary permissions for Tomcat itself to run under the security manager, but we need to extend it with permissions tailored for our own web application code. More on this in the next chapter.

As for CATALINA_HOME we need to set this path explicitly in setenv.sh when using the symbolic link /usr/local/tomcat/bin/startup.sh to start Tomcat. If we don't then CATALINA_HOME is resolved automatically by Tomcat's startup script to the physical path /usr/local/apache-tomcat-8.0.32. If the symbolic link used to start Tomcat differs from the path in ${catalina.home} used in the policy file, then the security manager will fail to apply the policy file correctly.

With setenv.sh in place we are now ready to work on the policy file.

Extending the policy file

Start Tomcat.

$ tomcat/bin/startup.sh

Open any web page in your app http://localhost:8080/myapp. The page will fail to load, we just need to trigger the security managers output in the log. Then stop Tomcat:

$ tomcat/bin/shutdown.sh

The security managers output to console is redirected to tomcat/logs/catalina.out. Open the file with the less command:

$ less tomcat/logs/catalina.out

Search for the sentence "access denied" by typing slash "/" followed by the sentence.

/access denied

access: access denied ("java.net.SocketPermission" "127.0.0.1:5432" "connect,resolve")
java.lang.Exception: Stack trace
        at java.lang.Thread.dumpStack(Thread.java:1329)
        at java.security.AccessControlContext.checkPermission(AccessControlContext.java:462)
        at java.security.AccessController.checkPermission(AccessController.java:884)
        at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
        at java.lang.SecurityManager.checkConnect(SecurityManager.java:1051)
        at java.net.Socket.connect(Socket.java:584)
        at java.net.Socket.connect(Socket.java:538)
        at java.net.Socket.(Socket.java:434)
        at java.net.Socket.(Socket.java:211)
        at org.postgresql.core.PGStream.(PGStream.java:62)
        at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:76)
        at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:66)
        at org.postgresql.jdbc2.AbstractJdbc2Connection.(AbstractJdbc2Connection.java:125)
        at org.postgresql.jdbc3.AbstractJdbc3Connection.(AbstractJdbc3Connection.java:30)
        at org.postgresql.jdbc3g.AbstractJdbc3gConnection.(AbstractJdbc3gConnection.java:22)
        at org.postgresql.jdbc4.AbstractJdbc4Connection.(AbstractJdbc4Connection.java:32)
        at org.postgresql.jdbc4.Jdbc4Connection.(Jdbc4Connection.java:24)
        at org.postgresql.Driver.makeConnection(Driver.java:393)
        at org.postgresql.Driver.connect(Driver.java:267)

Decoding the output above we conclude, that some code base was trying to call the Socket API to connect to host 127.0.0.1 on port 5432 but didn't have the necessary permission. The exact record of the required permission is:
("java.net.SocketPermission" "127.0.0.1:5432" "connect,resolve").

Java's own runtime is allowed to access any resources it wants to, but going further down the logged call stack we find the Postgresql client. We now have a pretty good hunch where we need to grant the permission, but let's get confirmation by searching for the sentence "domain that failed":

/domain that failed

access: domain that failed ProtectionDomain  (file:/usr/local/apache-tomcat-8.0.32/webapps/myapp/WEB-INF/lib/postgresql.jar )

The protection domain that failed is indeed the one assigned to the postgresql.jar code base.

Open the policy file:

$ sudo vi tomcat/conf/catalina.policy

Add the following protection domain to the end of the file. Replace the context name "myapp" with whatever your app is called.

grant codeBase "jar:file:${catalina.home}/webapps/myapp/WEB-INF/lib/postgresql.jar!/-" {
        permission java.net.SocketPermission "127.0.0.1:5432", "connect,resolve";
};

The permission line in catalina.policy is almost a copy of the line we found in catalina.out, we just need to add that extra comma and a semicolon showed here.

Notice the exclamation mark at the end of the jar file name. Java's URL resources use this to reference stuff inside a Jar file.

A good resource for further information on the format of the codeBase and permission arguments is http://docs.oracle.com/javase/8/docs/technotes/guides/security/spec/security-spec.doc3.html.

Now clean out the catalina.out file which can easily grow to a gigabyte of data in just one run.

$ rm tomcat/logs/catalina.out

And repeat the process to find and fix the next "access denied".

{{model.usr.name}}
{{cmt.user.name}}
{{cmt.user.name}}
{{childcmt.user.name}}
{{childcmt.user.name}}