sorenpoulsen.com header

Install Tomcat 8 on Ubuntu

In this post we go through the installation of Tomcat 8 on Ubuntu 15.10 for production use. When the installation is done, Tomcat will be running as a service that is managed with Systemd.

Installing Tomcat from Ubuntu's package repository would be very convenient, but the Tomcat package lives in the Universe repository which means that it is community maintained. The security team at Canonical is not responsible for sync'ing these packages with security patches from their upstream Debian maintainers. So instead we are going to install Tomcat's binaries directly from Apache Software Foundation.

I'm going to assume that Oracle's Hotspot JVM is already installed and that the JAVA_HOME environment variable is set up.

Install the binaries

Head over to https://tomcat.apache.org/ and download the latest Tomcat 8 tar.gz. At the time of writing it's apache-tomcat-8.0.32.tar.gz.

Set the umask, then unpack Tomcat and move it to /usr/local. Setting umask affects the permissions of newly created files when we unpack the Tar file. A umask of 022 strips write permission from group and other users. It is only a first step to lock down permissions in the installation folder, there will be more work on this later.

$ cd ~/Downloads
$ umask 022
$ tar xvzf apache-tomcat-8.0.32.tar.gz 
$ sudo mv apache-tomcat-8.0.32 /usr/local

Create a symbolic link /usr/local/tomcat pointing to the installation folder. We can switch this link to another version of tomcat without having to change Systemd's unit file (that we create in a later section) and environment variables that reference the folder.

$ sudo ln -s /usr/local/apache-tomcat-8.0.32/ /usr/local/tomcat

Create a tomcat user

Create a system user and group named tomcat. This user will be running the Tomcat process. It cannot log in and has no home folder.

$ sudo useradd -r -s /usr/sbin/nologin tomcat

Prepare the log folder

Administrators expect services to log to a dedicated folder under /var/log, so let's create one for Tomcat.

$ sudo mkdir /var/log/tomcat
$ sudo chown tomcat:adm /var/log/tomcat
$ sudo chmod 750 /var/log/tomcat
$ sudo chmod g+s /var/log/tomcat

adm is a group for local system monitoring tasks. Our regular user should already be member of this group and can thus read log files. The last line above sets the SGID bit on the folder. This indicates that newly created log files in the folder will have the same group as the folder itself, in other words the adm group.

Now replace the folder named tomcat/logs with a symbolic link to /var/log/tomcat. Logging frameworks such as Log4J that reference the log folder in their configuration with ${catalina.home}/logs can still do so, but effectively the logs are redirected to /var/log/tomcat.

$ cd /usr/local
$ rm -rf tomcat/logs
$ sudo ln -s /var/log/tomcat /usr/local/tomcat/logs

Configure file permissions

Popular web frameworks that are often hosted in Tomcat, such as Struts, have had documented vulnerabilities that allow a remote attacker to execute arbitrary code in Tomcat. Ready made tools that exploit these vulnerabilities are available and are being used actively.

Because the tomcat user is running the Tomcat process, which could be running hostile code, we have to limit the tomcat user's permissions to an absolute minimum. In particular we are going to make the webapps folder read-only to the tomcat user, to prevent modification of the website by the Tomcat process itself. This is in line with how the Apache Software Foundation configures Tomcat on their own servers.

In general we will set root as owner and tomcat as group on folders and files in the Tomcat installation folder. Tomcat will only have read access to files and folders, the exception being temp, work and logs subfolders, where we have to grant write permission. The reason we don't set tomcat as owner is that we can't meaningfully strip write access to folders such as webapps from the owner, because the owner can always add that permission back.

Because we set umask 022 that strips write permission on newly created files from group and other users, when we unpacked the Tar file, the only thing left to do is to set ownership and modify permissions in a few folders:

$ sudo chown -R root:tomcat apache-tomcat-8.0.32/
$ sudo chmod g+r apache-tomcat-8.0.32/conf/*
$ sudo chmod g+w apache-tomcat-0.32/temp apache-tomcat-8.0.32/work

Inspect the installation folder to check that the tomcat user only has write access to temp, work and logs.

$ ll apache-tomcat-8.0.32/
total 116
drwxr-xr-x  8 root tomcat  4096 Feb 19 21:36 ./
drwxr-xr-x 14 root root    4096 Feb 19 21:24 ../
drwxr-xr-x  2 root tomcat  4096 Feb 19 21:23 bin/
drwxr-xr-x  2 root tomcat  4096 Feb  2 20:39 conf/
drwxr-xr-x  2 root tomcat  4096 Feb 19 21:23 lib/
-rw-r--r--  1 root tomcat 57011 Feb  2 20:39 LICENSE
lrwxrwxrwx  1 root tomcat    15 Feb 19 21:36 logs -> /var/log/tomcat/
-rw-r--r--  1 root tomcat  1444 Feb  2 20:39 NOTICE
-rw-r--r--  1 root tomcat  6741 Feb  2 20:39 RELEASE-NOTES
-rw-r--r--  1 root tomcat 16195 Feb  2 20:39 RUNNING.txt
drwxrwxr-x  2 root tomcat  4096 Feb 19 21:23 temp/
drwxr-xr-x  7 root tomcat  4096 Feb  2 20:38 webapps/
drwxrwxr-x  2 root tomcat  4096 Feb  2 20:35 work/

The final touch is to remove all permissions from other users than owner and group.

$ sudo chmod -R o-rwx tomcat/

Our regular user can no longer browse the folders. If we need to do sustained work on the tomcat folder then su to root:

$ sudo su -

Set up Systemd service

Create a Systemd unit file to set up Tomcat as a service. The service will use Tomcat's own startup and shutdown scripts. That means we are still in known territory, when it comes to Tomcat's use of environment variable such as JAVA_HOME.

$ sudo vi /etc/systemd/system/tomcat.service

Copy this content to the file and save it:

[Unit]
Description=Apache Tomcat
After=syslog.target network.target

[Service]
Type=forking
Environment=CATALINA_HOME=/usr/local/tomcat
ExecStart=/usr/local/tomcat/bin/startup.sh
ExecStop=/usr/local/tomcat/bin/shutdown.sh
SuccessExitStatus=143
User=tomcat
Group=tomcat
Umask=027

[Install]
WantedBy=multi-user.target

Refresh Systemd's configuration.

$ sudo systemctl daemon-reload

Enable the Tomcat service.

$ sudo systemctl enable tomcat

Start the service.

$ sudo systemctl start tomcat

Let's make a few checks. Systemd's journal first:

$ journalctl
Feb 20 01:10:17 bobs systemd[1]: Starting Apache Tomcat...
Feb 20 01:10:17 bobs startup.sh[2326]: Tomcat started.
Feb 20 01:10:17 bobs systemd[1]: Started Apache Tomcat.

Tomcat's log:

$ less /var/log/tomcat/catalina.out
20-Feb-2016 01:10:17 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"] 20-Feb-2016 01:10:17 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"] 20-Feb-2016 01:10:17 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 1070 ms

Logs look good.

Let's spin up a browser to check Tomcat's root page http://localhost:8080.

From this point on the Tomcat service is managed with Systemd commands such as these:

$ sudo systemctl status tomcat
$ sudo systemctl stop tomcat
$ sudo systemctl start tomcat
$ sudo systemctl restart tomcat

Remove built-in web applications

Now that we have seen Tomcat's built-in web applications running, it's time to remove them :) The built-in web applications are a liability to the security of the server and the manager application, that is used to upload WAR files to the server, wont work because the tomcat user has no write access to the webapps folder. We will deal with deployment in a later section.

$ sudo systemctl stop tomcat
$ sudo rm -rf tomcat/webapps/*

Configure logging

Tomcat's default logging configuration is only fit for development. Let's make sure the logs wont slow us down or overflow and sink our boat.

Tomcat uses a fork of Apache Commons Logging set up as a facade for Java's standard API logging (java.util.logging).

The logging configuration in tomcat/conf/logging.properties has two root log handlers that log everything not caught by a more specialized log handler. One is an asynchronous file handler that writes to a file named catalina.<date>.log and the other is a console log handler. All console output from the Tomcat process is being redirected to a file named catalina.out.

Console logging is too slow for a production setup and because the async file handler will log the same information to a file named catalina.<date>.log, we can safely remove the console log handler.

$ sudo vi tomcat/conf/logging.properties

The root handlers are configured in a line that begins with ".handlers".

.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler

Remove the ", java.util.logging.ConsoleHandler" from the line and save the file.

This mostly deflates the catalina.out log file. There could still be libraries that log directly to System.out and on rare occasions the JVM itself may log to System.out. The catalina.out log file is special because it's not created by any logging framework, instead it's simply the stdout from the JVM process that is being redirected to the file with the >> operator, something akin to:

java ... org.apache.catalina.startup.Bootstrap start >> catalina.out

To roll over, compress and delete catalina.log files we need to configure the Logrotate service.

Create a Logrotate config file:

$ sudo vi /etc/logrotate.d/tomcat

Add this content to the file and save it:

/var/log/tomcat/catalina.out {
    copytruncate
    daily
    rotate 30
    compress
    missingok
    create 640 tomcat adm
}

With copytruncate the log file is copied and then truncated on roll over. This method is used because the JVM will not stop logging to the file and replacing the file could cause logging to break.

The asynchronous log handlers that log to the file named catalina.<date>.log and localhost.<date>.log will roll over daily, but they wont compress the log files nor remove old log files. This is fixed with a daily cron job.

$ sudo vi /etc/cron.daily/tomcat

Paste this script to the file:

#!/bin/sh
MAX_AGE_DAYS=30
if [ -d /var/log/tomcat ]; then  
  find /var/log/tomcat -name 'catalina.*.log' -daystart -mtime +0 -print0 | xargs --no-run-if-empty -0 gzip -9
  find /var/log/tomcat -name 'localhost.*.log' -daystart -mtime +0 -print0 | xargs --no-run-if-empty -0 gzip -9 
  find /var/log/tomcat -name 'catalina.*.log.gz' -mtime +$MAX_AGE_DAYS -print0 | xargs --no-run-if-empty -0 rm --
  find /var/log/tomcat -name 'localhost.*.log.gz' -mtime +$MAX_AGE_DAYS -print0 | xargs --no-run-if-empty -0 rm --
fi

Then set the script file permissions.

$ sudo chmod 755 /etc/cron.daily/tomcat

Tomcat's asynchronous log handlers append log statements to a queue instead of writing it directly to disk. A thread that appends a log statement can thus continue with business instead of sitting idle waiting for disk I/O to complete. A consumer thread outputs log statements from the queue to the disk. By default the consumer thread flushes one log statement at the time. This is inefficient for a production environment. It is fixed by adding a buffer that is only flushed to disk when it is full. Note if you are running a low traffic server, then you might want to skip this step, because the buffer will delay output.

Open tomcat/conf/logging.properties for editing:

$ vi tomcat/conf/logging.properties

Add a bufferSize property to the log handlers:

1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.prefix.bufferSize = 2048

2localhost.org.apache.juli.AsyncFileHandler.level = FINE
2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
2localhost.org.apache.juli.AsyncFileHandler.prefix.bufferSize = 2048

Deployment

Tomcat's auto-deployment feature must be disabled because the tomcat user has no write access to the webapps folder.

$ sudo vi tomcat/conf/server.xml

Set unpackWARs and autoDeploy to false in the Host tag, but leave deployOnStartup true.

<Host name="localhost" appBase="webapps" unpackWARs="false" autoDeploy="false" deployOnStartup="true">

To deploy a WAR file in this type of server configuration, unzip the WAR, copy the unzipped folder to webapps, grant the tomcat user read-only access to the folder and restart the server. Shown here with an imaginary WAR file named test.war.

$ sudo systemctl stop tomcat
$ unzip test.war -d test
$ sudo mv test/ tomcat/webapps/

Make sure the tomcat user has read-only access to the folder before Tomcat is started.

$ sudo chgrp -R root:tomcat tomcat/webapps/test
$ sudo chmod -R 750 tomcat/webapps/test
$ sudo systemctl start tomcat

Tomcat determines the URI of the web application from the deployment folder name, in this case /test. The web application is thus available from http://localhost:8080/test. If you want to make it available at the root of the host address http://localhost:8080/ then name the folder /ROOT.

Change the shutdown command

Sending the command "SHUTDOWN" to port 8005 will cause Tomcat to shut down. Any user on the host can open a connection to this port. Changing the command is quick and easy.

Open server.xml for editing:

$ sudo vi tomcat/conf/server.xml

Find the Server tag and change the value of the shutdown attribute to anything random (the shutdown script and the systemctl stop command will still work):

<Server port="8005" shutdown="8394jrd2093drliufwxkj">

Wrapping up

The set up we have now is production worthy, but if the tomcat user's free access to files on the host or ability to connect to arbitrary addresses on the internet makes you feel uneasy then a next possible step is to sandbox Tomcat with a security manager.

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