git repo structure

Recently I dockerized some JavaEE-apps which run within an Apache Tomcat appserver. I needed a tool for provisioning different configuration files in a dynamic manner — many of those files have environment specific fields which should not be coded statically inside some container layer. According to the 12factor-principles they should be set through granular environment variables. The tool which perfectly fit my needs turned out to be confd by Kelsey Hightower.

As an example I will show in this post how you can

  • Configure the Tomcat manager app with a password provided or generated at container start.
  • Use confd to render the tomcat-users.xml configuration file.
  • Define a HEALTHCHECK command which accesses the tomcat-manager-app using the dynamic password.

My first requirement: I wanted the tomcat’s manager-interface to be secure-by-default and zero-configuration at the same time. $ docker run confd-tomcat should not fail complaining on a missing password but just work out of the box. One advantage of docker is that it makes running applications so easy — let’s stick to that!

A custom script launched at container start takes care of it. It checks if a password has been provided as an environment variable and — if not — generates a new one:

#!/usr/bin/env bash
set -e
if [ -z "${TOMCAT_MANAGER_PASSWORD}" ]; then
    # borrowed from https://stackoverflow.com/a/34329799/2249614
    TOMCAT_MANAGER_PASSWORD=$(od -vN "20" -An -tx1 /dev/urandom | tr -d " \n")
    echo "TOMCAT_MANAGER_PASSWORD not provided. Generated: ${TOMCAT_MANAGER_PASSWORD}"
    export TOMCAT_MANAGER_PASSWORD
fi

/opt/confd/bin/confd -onetime -backend env

exec bin/catalina.sh run

The script first ensures that a TOMCAT_MANAGER_PASSWORD is available. Either set from outside or by generating one. Note that I use the non-blocking urandom-device for getting some random bytes. The security of that approach is at least debatable within container-environments. Then confd is executed to render the tomcat configuration files accordingly. Finally tomcat’s usual catalina.sh start-script is launched.

If the container is launched without a password the output is as follows:

$ docker run confd-tomcat
TOMCAT_MANAGER_PASSWORD not provided. Generated: 61220e1175281e

Alternatively the password can be set from outside:

$ docker run -e TOMCAT_MANAGER_PASSWORD=abc123 confd-tomcat

which will adopt the password silently.

The next step consists of configuring confd to generate the tomcat-users.xml file correctly. Kelsey provides in-depth-documentation on this part. Two files are needed: 1. A template of tomcat-users.xml with a placeholder for the password and 2. a configuration file which describes the rendering-task. Compared to the default tomcat-user.xml file shipped with a factory fresh tomcat you’ll note the embedded Golang-placeholder {{...}} where we find some hardcoded password usually.

<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
    <role rolename="manager-status"/>
    <user name="manager" password="{{getv "/tomcat/manager/password"}}" roles="manager-status"/>
</tomcat-users>
[template]
src = "tomcat-users.xml.tmpl"
dest = "/usr/local/tomcat/conf/tomcat-users.xml"
keys = [
    "/tomcat/manager/password",
]

confd automatically maps the environment variable TOMCAT_MANAGER_PASSWORD to the key /tomcat/manager/password when the run.sh script executes confd -onetime -backend env. Subsequently, it writes the file with the correct password to the specified destination.

The final Dockerfile looks like this:

FROM tomcat:8.5.33-jre10

ENV PATH="/opt/confd/bin:${PATH}" \
# empty password will lead to a random password being generated
    TOMCAT_MANAGER_PASSWORD=""

CMD ["./run.sh"]

RUN set -x \
# download confd
 && curl -sSfL -o confd https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 \
# validate checksum or fail
 && echo "255d2559f3824dd64df059bdc533fd6b697c070db603c76aaf8d1d5e6b0cc334 confd" | sha256sum -c - || exit 1 \
# install confd
 && mkdir -p /opt/confd/bin \
 && mv confd /opt/confd/bin/confd \
 && chmod +x /opt/confd/bin/confd

HEALTHCHECK CMD curl -u manager:\${TOMCAT_MANAGER_PASSWORD} http://localhost:8080/manager/status/all?XML=true  || exit 1

COPY ./confd /etc/confd
COPY run.sh run.sh

RUN chmod +x run.sh

The HEALTHCHECK directive is worth a look here. First, the environment variable needs to be escaped such that it is not evaluated at image-build-time and replaced by the empty String defined in line 5. The variable should be expanded later at container runtime to take the correct value. A second caveat here is that HEALTHCHECK commands are executed from within the docker container. This is essential to be able to use the correct password even when it has been generated automatically. In addition to that the manager-interface of tomcat is only accessible from localhost by default. Trying to access the manager-app from the docker-host will fail because host and container do not share the same network.

However, nothing prevents exposing the manager-app for remote-access now, as the tomcat-password is not longer hard-coded to some insecure dummy-password.

You can find the code used in this blog post on github