Tuesday, May 12, 2015

Securing Tomcat

Securing Tomcat


Tomcat is a great web server. Though for production there is room for improvement.
There are some basic things that need to be done to make your tomcat secure. Some of them are simple like changing the default shutdown port to a different one. Others are simple but can be easily overlooked, like removing the default demo application and removing the management interface. For a list see: http://www.tomcatexpert.com/blog/2011/11/02/best-practices-securing-apache-tomcat-7.

Tomcat Users

If you want to use the tomcat manager application but what to use users, tomcat supports defining roles and users in the tomcat-users.xml file (see https://tomcat.apache.org/tomcat-7.0-doc/manager-howto.html). An example of this file is:
<tomcat-users>
                <user password="test" roles="manager-gui,manager-status,manager-script,manager-jmx" username="tomcat"/>
                <role rolename="admin"/>
</tomcat-users>
For the most part this is good enough, but what if you do not want your password in clear text? Tomcat supports the option to use MD5 instead of the clear text. To do this in the server.xml file, in the Realm node, you need to add the following:
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" digest="md5" resourceName="UserDatabase"/>

JMX Login

Most applications need a JMX MBean for debugging and monitoring. The JVM supports user name and password in the following way.
To your application you add the following JVM args:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8007
-Dcom.sun.management.jmxremote.local.only=false
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.password.file={file_location}\jmxremote.password
-Dcom.sun.management.jmxremote.access.file={file_location}\jmxremote.access
You will need to change the ACL of the file for the jmxremote.password else the solution will not work (http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html).

The problem is that the file with the password is still open and not encrypted. To fix this we need to implement our own Login Module using JASS (have a look at https://blogs.oracle.com/lmalventosa/entry/jmx_authentication_authorization).
The implement our own login module we need to change the JVM args to:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8007
-Dcom.sun.management.jmxremote.local.only=false
-Dcom.sun.management.jmxremote.ssl=false
-Djava.ext.dirs={dir_location}
-Dcom.sun.management.jmxremote.login.config=AMFConfig
-Djava.security.auth.login.config={file_location}\AMF.config
-Dcom.sun.management.jmxremote.access.file={file_location}\jmxremote.access

For the login to work, the user still needs to be in the jmxremote.access file with the proper access level (readonly, readwrite).
We have added two new entries:
java.security.auth.login.config – this entirety will tell the JVM to load the JASS configuration file, for example:
TestConfig {
   plugin.com.tomcat.login.MyLoginModule Requisite passwordFile="\user\abc\jmxremote.access";
};
This will tell the JVM to use the class LoginModule for the JASS login. Since the configuration file can have more than one definition, you need to specific which module to use with: com.sun.management.jmxremote.login.config.
What they don’t write is that you need to create a separate jar that includes your login module (plugin.com.tomcat.login.LoginModule). Once you have this jar you need to put it in the java external folder. Adding it to the classpath is not enough. No we usually don’t have access to the java external folder, so since java 6 you can add another path for the external dir: java.ext.dirs. This will load all default external jars and your login.
An example code for the login module is:
public class MyLoginModule implements LoginModule {

       private Subject subject;
       private CallbackHandler callbackHandler;
       private Map<String, ?> options;
       private JMXPrincipal user;

       private boolean succeeded = false;

       public MyLoginModule() {
       }


       @Override
       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
              this.subject = subject;
              this.callbackHandler = callbackHandler;
              this.options = options;
              succeeded = false;
       }

       //function for map
       private Map<String,String> getUserPassword(String passwordFile){
              Map<String,String> map = new HashMap<String, String>();
              File file = new File(passwordFile);
              if (file.exists()) {
                     try (BufferedReader buffer = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
                           while (true) {
                                  String line = buffer.readLine();
                                  if (line == null)
                                         break;
                                  String[] fields = line.split("\\s+");
                                  String fileUser = fields[0];
                                  String filePassword = fields[1];
                                  map.put(fileUser, filePassword);
                           }
                     } catch (IOException e) {
                           return map;
                     }
              }
              else{
                     System.out.println("password file not found: " + passwordFile);
              }
              return map;
       }
      
       public boolean validateUser(String passwordFile, String userName, String password) {
              String md5 = EncryptionHelper.md5(password);
              Map<String, String> userPasswords = getUserPassword(passwordFile);
              String filePassword = userPasswords.get(userName);
              if (md5.equals(filePassword)) {
                     return true;
              }
              return false;
       }

       @Override
       public boolean login() throws LoginException {
              String passwordFile = (String) options.get("passwordFile");

              if (callbackHandler == null) {
                     throw new LoginException("Oops, callbackHandler is null");
              }

              Callback[] callbacks = new Callback[2];
              callbacks[0] = new NameCallback("name:");
              callbacks[1] = new PasswordCallback("password:", false);

              try {
                     callbackHandler.handle(callbacks);
              } catch (IOException e) {
                     throw new LoginException("Oops, IOException calling handle on callbackHandler");
              } catch (UnsupportedCallbackException e) {
                     throw new LoginException("Oops, UnsupportedCallbackException calling handle on callbackHandler");
              }

              NameCallback nameCallback = (NameCallback) callbacks[0];
              PasswordCallback passwordCallback = (PasswordCallback) callbacks[1];

              String name = nameCallback.getName();
              String password = new String(passwordCallback.getPassword());
              user = new JMXPrincipal(name);

              if (validateUser(passwordFile, name, password)) {
                     succeeded = true;
                     return succeeded;
              } else {
                     succeeded = false;
                     throw new FailedLoginException("Wrong Login! Incorrect User or Password");
              }
       }

       @Override
       public boolean commit() throws LoginException {
              subject.getPrincipals().add(user);

              return succeeded;
       }

       @Override
       public boolean logout() throws LoginException {
              subject.getPrincipals().remove(user);
              return false;
       }

       @Override
       public boolean abort() throws LoginException {
              return false;
       }
      
}

Debugging

To debug the login you have a few options. The regular attach process will work nicely and you can debug your login. In addition on the server side you can add the following system parameter: -Djava.security.debug=all.
Also on the client side you can run the jconsole from the command line with the following parameter:
Jconsole –debug. This will open a debug window and print the errors from the server side.