- Unidata Tomcat Docker
A security-hardened Tomcat container for thredds-docker.
This release modernizes the image and simplifies its security configuration. If you maintain an image derived from tomcat-docker, review the following changes before upgrading:
-
Tomcat 11 and Java 17: The image now uses Tomcat 11 with JDK 17. Applications must be compatible with Tomcat 11, including its Jakarta Servlet APIs.
-
Non-root execution and improved filesystem permissions: As before, Tomcat runs as an unprivileged user (UID/GID
1000by default, configurable throughTOMCAT_USER_IDandTOMCAT_GROUP_ID). What's new is that the entrypoint grants the runtime user ownership only oflogs,temp, andworkby default. Ownership and permissions elsewhere inCATALINA_HOMEare left unchanged. The Tomcat installation, configuration, and deployed applications remain protected from runtime modification (though see next bullet point). If you supply your own versions ofserver.xmlandweb.xmlfiles, bind-mount them read-only (:ro). -
Additional writable directories for derived images: Applications that need writable directories beyond the standard Tomcat runtime directories can declare them using
TOMCAT_ADDITIONAL_WRITABLE_DIRS. Supply whitespace-separated paths relative toCATALINA_HOME; the entrypoint recursively assigns ownership to the configured runtime UID/GID at startup. For example,thredds-dockersets this variable tocontent. This can also change ownership of files in bind-mounted directories on the host. -
Simplified startup: Tomcat now runs in the foreground with normal container signal handling. The obsolete Tomcat Security Manager configuration has been removed.
-
Revised default security configuration: The image enables standard HTTP security headers, suppresses Tomcat-generated error reports in a more modern way, and removes the old global CORS filter. The filter imposed a global policy unnecessarily restricting application-specific configuration. Applications requiring cross-origin access must configure CORS themselves.
-
Updated TLS configuration: The documentation now uses Tomcat 11's supported TLS configuration.
This repository contains files necessary to build and run a security hardened Tomcat Docker container, based off of a canonical Tomcat base image. The Unidata Tomcat Docker images associated with this repository are available on DockerHub. All default web applications have been expunged from this container so it will primarily serve as a base image for other containers.
This image includes the security-related configuration changes listed below. Deployment and application security require additional configuration and validation.
- Eliminated default Tomcat web applications
- Run Tomcat with an unprivileged runtime UID/GID via
entrypoint.sh - By default, only writable Tomcat runtime directories
$CATALINA_HOME/logs,$CATALINA_HOME/temp, and$CATALINA_HOME/workare owned by the configured runtime UID/GID. Ownership and permissions elsewhere in$CATALINA_HOME, includingconf,bin,lib, andwebapps, are left unchanged.
In your runtime configuration, ensure server.xml and web.xml bind mounts are read-only. This prevents the Tomcat process from modifying configuration files supplied by the host and is a recommended security practice. For example, with Docker Compose:
- ./files/server.xml:/usr/local/tomcat/conf/server.xml:ro
- ./files/web.xml:/usr/local/tomcat/conf/web.xml:roThe following changes have been made to web.xml from the out-of-the-box version:
- Added
SAMEORIGINanti-clickjacking option - HTTP header security filter (
httpHeaderSecurity) uncommented/enabled
The following changes have been made to server.xml from the out-of-the-box version:
- The HTTP connector sets the Server response header to Apache, preventing disclosure of the Tomcat version in that header.
- Shutdown port disabled
- Tomcat-generated error responses omit stack traces, error details, and server information via
ErrorReportValve. Application-defined error responses must separately avoid exposing sensitive details. - Digested passwords. See next section.
The active Connector has relaxedPathChars and relaxedQueryChars attributes. This change may not be optimal for security, but must be done to accommodate DAP requests which THREDDS must perform.
This container configures a UserDatabaseRealm in server.xml with a CredentialHandler using the sha-512 digest algorithm. Passwords defined in tomcat-users.xml must therefore use digested passwords in the password attributes of the user elements. Generating a digested password is simple. Here is an example for the sha-512 digest algorithm:
docker run --rm tomcat:11-jdk17 \
/usr/local/tomcat/bin/digest.sh -a sha-512 mysupersecretpasswordThis command will yield something like:
mysupersecretpassword:94e334bc71163a69f2e984e73741f610e083a8e11764ee3e396f6935c3911f49$1$a5530e17501f83a60286f6363a8647a277c9cfdbThe hash after the : is what you will use for the password attribute in tomcat-users.xml.
More information about this topic is available in the Tomcat documentation.
We strive to maintain the security of this project's DockerHub images by updating them with the latest upstream security improvements. If you have any security concerns, please email us at security@unidata.ucar.edu to bring them to our attention.
See tags listed on dockerhub. Note, these versions are not necessarily static and will evolve due to upstream image changes. It's recommended to check regularly to ensure you have the latest image.
Before you begin using this Docker container project, make sure your system has Docker installed. Docker Compose is optional but recommended.
You can either pull the image from DockerHub with:
docker pull unidata/tomcat-docker:<version>Or you can build it yourself with:
- Clone the repository:
git clone https://github.com/Unidata/tomcat-docker.git - Navigate to the project directory:
cd tomcat-docker - Build the Docker image:
docker build -t tomcat-docker:<version>.
Note that this project is meant to serve as a base image for other containerized Docker Tomcat web applications. Refer to the image created by this project in your Dockerfile. For example:
FROM unidata/tomcat-docker:<version>Sometimes it is useful to enter this container via bash and poke around, just to see what is there. For example,
docker run -it unidata/tomcat-docker:<version> bashThe problem with mounted Docker volumes and UID/GID mismatch headaches is best explained here: https://denibertovic.com/posts/handling-permissions-with-docker-volumes/.
This container allows you to control the Tomcat runtime UID/GID via TOMCAT_USER_ID and TOMCAT_GROUP_ID environment variables. If not set, the default UID/GID is 1000/1000. For example,
docker run --name tomcat \
-e TOMCAT_USER_ID=`id -u` \
-e TOMCAT_GROUP_ID=`getent group $USER | cut -d':' -f3` \
-v `pwd`/logs:/usr/local/tomcat/logs/ \
-v /path/to/your/webapp:/usr/local/tomcat/webapps:ro \
-d -p 8080:8080 unidata/tomcat-docker:<version>where TOMCAT_USER_ID and TOMCAT_GROUP_ID have been configured with the desired runtime UID/GID. If using docker compose, see compose.env to configure the Tomcat runtime UID/GID inside the container.
Bind-mounted files must be readable by the configured runtime UID/GID. For example, a TLS private key with mode 0600 must be owned by TOMCAT_USER_ID.
This feature enables greater control of file permissions written outside the container via mounted volumes (e.g., files contained within the Tomcat logs directory such as catalina.out).
Note that containers that inherit this container and override entrypoint.sh must arrange the Tomcat runtime UID/GID themselves. The supplied entrypoint.sh creates an account when needed or reuses an existing account with the configured UID. On Docker Desktop for macOS, bind-mount ownership and permission behavior differs from native Linux, so matching container UID/GID values may not produce the same host filesystem behavior.
By default, the Tomcat runtime user owns only logs, temp, and work within CATALINA_HOME. Derived images that require additional writable directories in CATALINA_HOME can declare them using TOMCAT_ADDITIONAL_WRITABLE_DIRS.
For example, thredds-docker requires its content directory to be writable:
ENV TOMCAT_ADDITIONAL_WRITABLE_DIRS="content"The variable accepts whitespace-separated directory paths relative to CATALINA_HOME. At container startup, the entrypoint recursively changes ownership of these directories to the configured TOMCAT_USER_ID and TOMCAT_GROUP_ID. Directories must already exist and cannot resolve outside CATALINA_HOME.
Important: If these directories are bind-mounted from the host, their contents will also be recursively chown'd. Only declare directories your application actually needs to write to; configuration files, application binaries, and other protected parts of the Tomcat installation should remain read-only to the runtime user.
This Tomcat container supports HTTPS with either a self-signed certificate for local testing or a CA-issued certificate for production. The examples below use a PKCS#12 keystore (.p12) in both cases. PKCS#12 is the default keystore format in modern Java releases, is supported directly by Tomcat, and avoids an unnecessary conversion to JKS.
For a complete treatment of TLS configuration, see https://tomcat.apache.org/tomcat-11.0-doc/ssl-howto.html.
For local HTTPS testing, use the JDK keytool command to create a PKCS#12 keystore containing a 30-day self-signed RSA certificate. The subject alternative names make the certificate valid for localhost and 127.0.0.1. This certificate is not intended for production.
keytool -genkeypair \
-alias localhost \
-keyalg RSA -keysize 4096 -sigalg SHA256withRSA \
-validity 30 \
-dname "CN=localhost" \
-ext "SAN=dns:localhost,ip:127.0.0.1" \
-storetype PKCS12 \
-keystore keystore.p12The command prompts for the keystore password. Use that password in the connector configuration below. To let command-line clients trust this test certificate, export its public certificate (this does not expose the private key):
keytool -exportcert -rfc \
-alias localhost \
-keystore keystore.p12 \
-file localhost.crtContinue with Configure Tomcat below.
Obtain the server certificate (ssl.crt), its private key (ssl.key), and any intermediate CA certificates (intermediates.crt) in PEM format. The intermediate file should contain any intermediate CA certificates needed to build the certificate chain; the root CA certificate is normally omitted.
Package these files in a PKCS#12 keystore:
openssl pkcs12 -export \
-in ssl.crt \
-inkey ssl.key \
-certfile intermediates.crt \
-name server \
-out keystore.p12OpenSSL prompts for the password that protects keystore.p12. If the CA did not provide intermediate certificates, omit the -certfile intermediates.crt line. Use the same password and the alias server in the connector configuration below.
The tools differ because the inputs differ: keytool creates a self-signed certificate directly in a PKCS#12 keystore, while openssl packages an existing PEM certificate, private key, and certificate chain into the same format. Tomcat consumes the resulting keystore.p12 identically in either case.
Add this connector inside the Service element in server.xml:
<Connector port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="${catalina.base}/conf/keystore.p12"
certificateKeystoreType="PKCS12"
certificateKeyAlias="server"
certificateKeystorePassword="xxxx" />
</SSLHostConfig>
</Connector>Replace xxxx with the keystore password. For the self-signed example, also replace the alias server with localhost.
For production deployments, Tomcat also supports certificateKeystorePasswordFile, which avoids storing the keystore password directly in server.xml. Keep that password file out of version control and mount it with appropriately restrictive permissions.
If using the commented TLS connector from the supplied server.xml as a starting point, replace its JKS-based certificate configuration with the PKCS#12 configuration above. A JKS keystore still works, but there is no need to create one when using these instructions.
Mount server.xml and the keystore read-only. The following publishes HTTPS on loopback; adjust the published address as appropriate for production:
docker run -d -p 127.0.0.1:8443:8443 \
-v "$PWD/server.xml:/usr/local/tomcat/conf/server.xml:ro" \
-v "$PWD/keystore.p12:/usr/local/tomcat/conf/keystore.p12:ro" \
tomcat-docker:<version>Alternatively, after building the image, use this docker-compose.yml with docker compose up -d:
services:
unidata-tomcat:
image: tomcat-docker:<version>
ports:
- "127.0.0.1:8443:8443"
volumes:
- ./keystore.p12:/usr/local/tomcat/conf/keystore.p12:ro
- ./server.xml:/usr/local/tomcat/conf/server.xml:roThe keystore contains the private key. Restrict access to it while ensuring it is readable by the Tomcat runtime UID (1000 by default). For example, on a native Linux Docker host:
chown 1000:1000 keystore.p12
chmod 600 keystore.p12After Tomcat starts, test a CA-issued certificate normally. For the self-signed example, explicitly trust the exported certificate:
curl --cacert ./localhost.crt https://localhost:8443/A TLS connector initialization failure does not necessarily terminate Tomcat if another connector can still start. Check the container logs if the HTTPS endpoint is unavailable.
If you would like to do a small test to ensure the Unidata Tomcat Docker image is working:
mkdir -p /tmp/test/ROOT
echo 'It works' > /tmp/test/ROOT/index.html
docker run --name tomcat \
-e TOMCAT_USER_ID=1000 \
-e TOMCAT_GROUP_ID=1000 \
-v /tmp/test:/usr/local/tomcat/webapps:ro \
-d -p 127.0.0.1:8080:8080 \
tomcat-docker:<version>
curl http://127.0.0.1:8080/Expected result: It works.