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.
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 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
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
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-8.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 -
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
Environment=JRE_HOME=/usr/local/jre
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
Adjust the JRE_HOME environment variable in the file above to match the root folder of the Java Runtime Environment on your system. If you are using the JDK then add a JAVA_HOME variable pointing to the JDK root folder instead.
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
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/*
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.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.bufferSize = 2048
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.
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">
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.