├── .github ├── helperScripts │ ├── README.desiredImages │ ├── desiredImages │ ├── dockertags.sh │ ├── findTomcatVersion.sh │ ├── jdkVersions.sh │ ├── remoteBranchNamesToJSON.sh │ └── tomcatVersions.sh ├── testScripts │ ├── expected.html │ └── test.sh └── workflows │ ├── checkUpstream.yml │ └── docker-image.yml ├── .gitignore ├── .org └── readme.org ├── Dockerfile ├── LICENSE ├── README.md ├── compose.env ├── docker-compose.yml ├── entrypoint.sh ├── server.xml ├── server.xml.ORIG ├── start-tomcat.sh ├── web.xml └── web.xml.ORIG /.github/helperScripts/README.desiredImages: -------------------------------------------------------------------------------- 1 | The "desiredImages" file is a plain-text file where each line contains an 2 | Extended Regular Expression (ERE) for a tag of the official tomcat image on 3 | DockerHub, followed by a space (" ") and either a 1 or a 0, denoting whether or 4 | not that pariticular tag is additionally tracked as the "latest" tag. For 5 | example: 6 | 7 | ^10\.[0-9]{1,}\.[0-9]{1,}-jdk17-openjdk$ 0 8 | 9 | Is the tag for tomcat version 10.x.y using the jdk version 17 version of 10 | openjdk. The 0 at the end of the line indicates this won't be pushed to either 11 | GitHub or Docker as "latest." 12 | 13 | Note the "^" and "$" at the beginning and end of the ERE, respecitively. These 14 | are necessary to ensure specificity of the tag. Namely, while you may be wanting 15 | to specify the tag "10.0.12-jdk17-openjdk" with the ERE 16 | 10\.[0-9]{1,}\.[0-9]{1,}-jdk17-openjdk, this would also match the following 17 | tags: 18 | 19 | 10.0.12-jdk17-openjdk-slim-buster 20 | 10.0.12-jdk17-openjdk-slim-bullseye 21 | 10.0.12-jdk17-openjdk-slim 22 | 10.0.12-jdk17-openjdk-buster 23 | 10.0.12-jdk17-openjdk-bullseye 24 | -------------------------------------------------------------------------------- /.github/helperScripts/desiredImages: -------------------------------------------------------------------------------- 1 | ^8\.[0-9]{1,}\.[0-9]{1,}-jdk11-openjdk$ 1 2 | ^10\.[0-9]{1,}\.[0-9]{1,}-jdk17-openjdk$ 0 3 | ^10\.[0-9]{1,}\.[0-9]{1,}-jdk17-temurin-focal$ 0 4 | -------------------------------------------------------------------------------- /.github/helperScripts/dockertags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USAGE=" 4 | dockertags -- list all tags for a Docker image in the DockerHub registry \n 5 | \n 6 | INPUTS: \n 7 | ./dockertags -u|--user \ \n 8 | -p|--password \ \n 9 | -n|--namespace \ \n 10 | -i|--image \ \n 11 | [ -h|--help ] \n 12 | \n 13 | --user: username used for authentication to dockerhub registry \n 14 | --password: password or personal access token \n 15 | --namespace: organization name (e.g. "unidata"); "library" for public repos \n 16 | --image: the name of the image \n 17 | --help: print this help message and exit \n 18 | \n 19 | OUTPUTS: \n 20 | The tags of the desired image, each on a new line. For example: \n 21 | \n 22 | $ ./dockertags --user $USERNAME \ \n 23 | --password $PASSWORD \ \n 24 | --namespace library \ \n 25 | --image $IMAGE \n 26 | \n 27 | tag1 \n 28 | tag2 \n 29 | tag3 \n 30 | " 31 | 32 | while [[ $# > 0 ]] 33 | do 34 | key="$1" 35 | case $key in 36 | -u|--user) 37 | USERNAME="$2" 38 | shift # past argument 39 | ;; 40 | -p|--password) 41 | PASSWORD="$2" 42 | shift # past argument 43 | ;; 44 | -n|--namespace) 45 | NAMESPACE="$2" 46 | shift # past argument 47 | ;; 48 | -i|--image) 49 | IMAGE="$2" 50 | shift # past argument 51 | ;; 52 | -h|--help) 53 | echo -e $USAGE 54 | exit 55 | ;; 56 | esac 57 | shift # past argument or value 58 | done 59 | 60 | # Check if all values were set 61 | [[ -z "$USERNAME" ]] && { echo -e "Must supply a username...Exiting" $USAGE; exit 1; } 62 | [[ -z "$PASSWORD" ]] && { echo -e "Must supply a password...Exiting" $USAGE; exit 1; } 63 | [[ -z "$NAMESPACE" ]] && { echo -e "Must supply a namespace...Exiting" $USAGE; exit 1; } 64 | [[ -z "$IMAGE" ]] && { echo -e "Must supply an image...Exiting" $USAGE; exit 1; } 65 | 66 | # Grab a token for authentiting when querying the registry 67 | token=$(curl -Ls -X POST https://hub.docker.com/v2/users/login \ 68 | -H 'Content-Type: application/json' \ 69 | -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" | jq -r .token) 70 | 71 | # Query the registry for the first 100 entries (the maximum page size) 72 | RESPONSE=$(curl -Ls -G \ 73 | --data-urlencode "page_size=100" \ 74 | --data-urlencode "page=1" \ 75 | -H "Authorization: Bearer ${token}" \ 76 | https://hub.docker.com/v2/namespaces/${NAMESPACE}/repositories/${IMAGE}/tags) 77 | 78 | # Parse using jq 79 | TAGS=$(echo $RESPONSE | jq '.results[].name') 80 | 81 | # If more than 100 tags exist in the repository, we must loop through all pages 82 | # using the "next" field of the JSON response 83 | let PAGE=1 84 | NEXTURL=$(echo $RESPONSE | jq '.next') 85 | 86 | while [[ "${NEXTURL}" != "null" ]]; 87 | do 88 | let PAGE+=1 89 | RESPONSE=$(curl -Ls -G \ 90 | --data-urlencode "page_size=100" \ 91 | --data-urlencode "page=$PAGE" \ 92 | -H "Authorization: Bearer ${token}" \ 93 | https://hub.docker.com/v2/namespaces/${NAMESPACE}/repositories/${IMAGE}/tags) 94 | TAGS+=" $(echo $RESPONSE | jq '.results[].name')" 95 | NEXTURL=$(echo $RESPONSE | jq '.next') 96 | done 97 | 98 | # Print each tag on its own line and remove quotes (") from around each tag 99 | printf '%s\n' $TAGS | sed -e 's/"//g' 100 | -------------------------------------------------------------------------------- /.github/helperScripts/findTomcatVersion.sh: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # ./findTomcatVersion.sh 3 | 4 | # Regex explanation 5 | # ^\( *${MAJORTOMCATVERSION}[.-]\) ---- Find ${MAJORTOMCATVERSION} at the /beginning/ of the search string, followed by either a "." or a "-"; ignore leading white space 6 | # \([0-9]*[.-]\)* ---- Any number of digits from 0-9, followed by a "." or a "-"; find this expression 0 or more times 7 | # \(jdk${JDKVERSION}-openjdk\)$ ---- Find the following string at the /end/ of the search string 8 | 9 | MAJORTOMCATVERSION=$1 10 | JDKVERSION=$2 11 | 12 | set -o pipefail 13 | 14 | awk -F "/" '{print $NF}' | \ 15 | grep "\(^\( *${MAJORTOMCATVERSION}\)[.-]\)\([0-9]*[.-]\)*\(jdk${JDKVERSION}-openjdk\)$" | \ 16 | sort -Vr | \ 17 | head -n1 18 | -------------------------------------------------------------------------------- /.github/helperScripts/jdkVersions.sh: -------------------------------------------------------------------------------- 1 | # echo '"jdk": [ "8", "11", "17", "18" ]' 2 | echo '"jdk": [ "11" ]' 3 | -------------------------------------------------------------------------------- /.github/helperScripts/remoteBranchNamesToJSON.sh: -------------------------------------------------------------------------------- 1 | # What this script does, pipe by pipe: 2 | 3 | # Print all remote branches | 4 | # Find all lines that *don't* have the string HEAD, main, or master (eg origin/HEAD -> origin/main) | 5 | # Find only lines that have "origin/" in the name (ie in case of multiple remotes) | 6 | # Print the branch name by accessing the second string delimited by "/" | 7 | # Use sed to: 8 | # -e "Add quotes around, and a comma at the end of, each line" 9 | # -e "remove comma from the last line" 10 | # -e "insert a "[" before the first line" 11 | # -e "insert a "]" after the last line" 12 | 13 | git branch -r | grep -v "HEAD" | grep -v "main" | grep -v "master" | 14 | grep "origin/" | awk -F "/" '{print $2}' | 15 | sed -e "s/.*/\"&\",/g" -e "$ s/,$//g" -e "1 i \"branch\": [" -e "$ a ]" 16 | -------------------------------------------------------------------------------- /.github/helperScripts/tomcatVersions.sh: -------------------------------------------------------------------------------- 1 | # echo '"tomcat": [ "8", "9", "10" ]' 2 | echo '"tomcat": [ "8" ]' 3 | -------------------------------------------------------------------------------- /.github/testScripts/expected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample "Hello, World" Application 4 | 5 | 6 | 7 | 8 | 9 | 12 | 18 | 19 |
10 | 11 | 13 |

Sample "Hello, World" Application

14 |

This is the home page for a sample application used to illustrate the 15 | source directory organization of a web application utilizing the principles 16 | outlined in the Application Developer's Guide. 17 |

20 | 21 |

To prove that they work, you can execute either of the following links: 22 |

26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/testScripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Grab index from local tomcat server and compare the file with what is known to be expected 4 | # If there is no difference, both diff and cmp exit with code 0 5 | # If there is a difference, script will exit with 1, i.e., error out 6 | 7 | curl -o ./.github/testScripts/actual.html http://127.0.0.1:8080/sample/index.html && \ 8 | diff ./.github/testScripts/expected.html ./.github/testScripts/actual.html && \ 9 | cmp ./.github/testScripts/expected.html ./.github/testScripts/actual.html && \ 10 | echo Tomcat Server OK 11 | -------------------------------------------------------------------------------- /.github/workflows/checkUpstream.yml: -------------------------------------------------------------------------------- 1 | ####################################################### 2 | # Check dockerhub for updates to the tomcat container # 3 | ####################################################### 4 | 5 | name: Check For Upstream Updates 6 | 7 | on: 8 | schedule: 9 | # Once a day at 00:00 UTC 10 | - cron: '0 0 * * *' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | createMatrix: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | matrix: ${{ steps.set-matrix.outputs.desiredImages }} 18 | dockertags: ${{ steps.set-dockertags.outputs.dockertags }} 19 | 20 | steps: 21 | - name: Checkout default branch 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set environment variables 27 | run: | 28 | echo "upstream_image=tomcat" >> $GITHUB_ENV 29 | echo "scriptsdir=.github/helperScripts" >> $GITHUB_ENV 30 | 31 | # Funny workaround to output a multiline string to another job 32 | # Pipe the multiline string into jq to create a minified JSON array where 33 | # each element is an individual line. 34 | # This minified array is output to GitHub Actions as a single line, can be 35 | # read by another job, and parsed back to a multiline string using jq 36 | - name: Fetch all tags and set as Github Actions output 37 | id: set-dockertags 38 | run: | 39 | dockertags=$(\ 40 | ${{ env.scriptsdir }}/dockertags.sh \ 41 | --user ${{ secrets.registryuser }} \ 42 | --password ${{ secrets.registrypwd }} \ 43 | --namespace library \ 44 | --image ${{ env.upstream_image }} \ 45 | | jq -cR '[., inputs]' \ 46 | ) 47 | echo "dockertags=$dockertags" >> $GITHUB_OUTPUT 48 | 49 | # Create a minified JSON object from the desiredImages file (see 50 | # README.desiredImages for a description of its contents) 51 | # The JSON object has the form: 52 | # { 53 | # tag: [ 54 | # { 55 | # "ERE": , 56 | # "isLatest": <0|1> 57 | # }, 58 | # ] 59 | # } 60 | - name: Create matrix output 61 | id: set-matrix 62 | run: | 63 | echo "desiredImages=$( 64 | jq -cnR '{ 65 | "tag": [ 66 | inputs/" " | {"ERE": .[0], "isLatest": .[1]} 67 | ] 68 | }' ${{ env.scriptsdir }}/desiredImages \ 69 | )" >> $GITHUB_OUTPUT 70 | 71 | checkUpstream: 72 | runs-on: ubuntu-latest 73 | needs: createMatrix 74 | strategy: 75 | matrix: ${{ fromJson(needs.createMatrix.outputs.matrix) }} 76 | 77 | steps: 78 | 79 | - name: Set environment variables 80 | run: | 81 | echo "scriptsdir=.github/helperScripts" >> $GITHUB_ENV 82 | 83 | - name: Checkout default branch 84 | uses: actions/checkout@v2 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: Check the most recent upstream tomcat version 89 | run: | 90 | upstream=$(\ 91 | echo '${{ needs.createMatrix.outputs.dockertags }}' \ 92 | | jq -r '.[]' \ 93 | | grep -E -e '${{ matrix.tag.ERE }}' \ 94 | | sort -Vr \ 95 | | head -n 1 \ 96 | ) 97 | echo $upstream 98 | echo "upstream=$upstream" >> $GITHUB_ENV 99 | 100 | - name: Check if a branch on the repo is up to date 101 | run: | 102 | current=$(git branch --list -r "origin/*" \ 103 | | sed 's|^ *origin/||g' \ 104 | | grep -E -e '${{ matrix.tag.ERE }}' \ 105 | | sort -Vr \ 106 | | head -n 1) 107 | echo Most current branch: $current 108 | test "$current" = "${{ env.upstream }}" && 109 | up2date=true || up2date=false 110 | echo "Up to date with latest version (${{ env.upstream }})?" 111 | echo $up2date 112 | echo "up2date=$up2date" >> $GITHUB_ENV 113 | 114 | - name: Already up to date 115 | if: ${{ env.up2date == 'true'}} 116 | run: | 117 | echo "Already up to date with upstream: ${{ env.upstream }}" 118 | 119 | - name: Checkout new branch 120 | if: ${{ env.up2date != 'true' }} 121 | run: | 122 | echo "tag=${{ env.upstream }}" >> $GITHUB_ENV 123 | echo "New upstream version ${{ env.upstream }}" 124 | echo "Creating new branch from origin/latest ..." 125 | git checkout -b ${{ env.upstream }} origin/latest 126 | 127 | - name: Update Dockerfile 128 | if: ${{ env.up2date != 'true' }} 129 | run: | 130 | sed -e "s/FROM tomcat:.*/FROM tomcat:${{ env.upstream }}/g" Dockerfile -i 131 | grep "FROM tomcat:" Dockerfile 132 | 133 | - name: Build image 134 | if: ${{ env.up2date != 'true' }} 135 | run: docker build --no-cache -t ${{ secrets.imagename }}:${{ env.tag }} . 136 | 137 | - name: Download sample web app 138 | if: ${{ env.up2date != 'true' }} 139 | run: | 140 | wget -O $(pwd)/.github/testScripts/sample.war \ 141 | https://tomcat.apache.org/tomcat-8.5-doc/appdev/sample/sample.war 142 | 143 | - name: Run container 144 | if: ${{ env.up2date != 'true' }} 145 | run: | 146 | docker run --name tomcat \ 147 | -e TOMCAT_USER_ID=$(id -u) \ 148 | -e TOMCAT_GROUP_ID=$(getent group $USER | cut -d : -f3) \ 149 | -v $(pwd)/.github/testScripts:/testScripts \ 150 | -v $(pwd)/.github/testScripts:/usr/local/tomcat/webapps \ 151 | -d \ 152 | -p 8080:8080 \ 153 | unidata/tomcat-docker:latest 154 | 155 | - name: Wait and listen for Tomcat to fire up 156 | if: ${{ env.up2date != 'true' }} 157 | run: | 158 | nc -z -w300 127.0.0.1 8080 159 | for i in {1..5}; do curl -o /dev/null http://127.0.0.1:8080/sample/index.html && break || \ 160 | (echo sleeping 15... && sleep 15); done 161 | 162 | - name: Run test script 163 | if: ${{ env.up2date != 'true' }} 164 | run: | 165 | ./.github/testScripts/test.sh \ 166 | && rm -rf $(pwd)/.github/testScripts/{sample.war,sample,actual.html} 167 | 168 | - name: Push to git 169 | if: ${{ env.up2date != 'true' }} 170 | run: | 171 | git config --global user.name $GITHUB_ACTOR 172 | git config --global user.email $GITHUB_ACTOR@users.noreply.github.com 173 | git add . && git commit -m "Update to tomcat:${{ env.upstream }}" && \ 174 | git push origin ${{ env.upstream }} 175 | 176 | - name: Push to dockerhub 177 | if: ${{ env.up2date != 'true' }} 178 | run: | 179 | docker logout 180 | echo ${{ secrets.registrypwd }} | docker login -u ${{ secrets.registryuser }} --password-stdin 181 | docker push ${{ secrets.imagename }}:${{ env.tag }} && \ 182 | { docker logout && echo "Successfully pushed ${{ secrets.imagename }}:${{ env.tag }} to dockerhub"; } || 183 | { docker logout && echo "Docker push failed" && exit 1; } 184 | 185 | - name: Create PR targetting latest 186 | if: ${{ env.up2date != 'true' }} 187 | run: | 188 | if [[ "${{ matrix.tag.isLatest }}" == "1" ]]; 189 | then 190 | gh pr create --title "Update to tomcat: ${{ env.upstream }}" --body "PR created by GitHub Actions" 191 | fi 192 | env: 193 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 194 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: [ workflow_dispatch, pull_request ] 4 | 5 | jobs: 6 | 7 | buildAndTest: 8 | runs-on: ubuntu-latest 9 | steps: 10 | # Checkout the commit that triggered the workflow 11 | - uses: actions/checkout@v2 12 | 13 | - name: Build the Docker image 14 | run: docker build --no-cache -t unidata/tomcat-docker:latest . 15 | 16 | - name: Download sample wep app 17 | run: | 18 | wget -O $(pwd)/.github/testScripts/sample.war \ 19 | https://tomcat.apache.org/tomcat-8.5-doc/appdev/sample/sample.war 20 | 21 | - name: Run the container 22 | run: | 23 | docker run --name tomcat \ 24 | -e TOMCAT_USER_ID=$(id -u) \ 25 | -e TOMCAT_GROUP_ID=$(getent group $USER | cut -d : -f3) \ 26 | -v $(pwd)/.github/testScripts:/testScripts \ 27 | -v $(pwd)/.github/testScripts:/usr/local/tomcat/webapps \ 28 | -d \ 29 | -p 8080:8080 \ 30 | unidata/tomcat-docker:latest 31 | 32 | # Give chance for Tomcat to fire up 33 | - name: Wait and listen for Tomcat to fire up 34 | run: | 35 | nc -z -w300 127.0.0.1 8080 36 | for i in {1..5}; do curl -o /dev/null http://127.0.0.1:8080/sample/index.html && break || \ 37 | (echo sleeping 15... && sleep 15); done 38 | 39 | - name: Run test script 40 | run: ./.github/testScripts/test.sh 41 | 42 | - name: Push to Dockerhub 43 | if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == 'true'}} 44 | run: | 45 | docker logout 46 | echo ${{ secrets.registrypwd }} | docker login -u ${{ secrets.registryuser }} --password-stdin 47 | docker push ${{ secrets.imagename }}:${{ env.tag }} && 48 | { docker logout && echo "Successfully pushed ${{ secrets.imagename }}:${{ env.tag }}"; } || 49 | { docker logout && echo "Docker push failed" && exit 1; } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /.org/readme.org: -------------------------------------------------------------------------------- 1 | #+options: ':nil *:t -:t ::t <:t H:4 \n:nil ^:t arch:headline author:t 2 | #+options: broken-links:nil c:nil creator:nil d:(not "LOGBOOK") date:t e:t 3 | #+options: email:nil f:t inline:t num:t p:nil pri:nil prop:nil stat:t tags:t 4 | #+options: tasks:t tex:t timestamp:t title:t toc:t todo:t |:t 5 | #+options: auto-id:t 6 | #+options: H:6 7 | 8 | #+title: readme 9 | #+date: <2023-04-20 Thu> 10 | #+author: Julien Chastang 11 | #+email: chastang@ucar.edu 12 | #+language: en 13 | #+select_tags: export 14 | #+exclude_tags: noexport 15 | #+creator: Emacs 26.3 (Org mode 9.2.1) 16 | 17 | #+property: :eval no :results none 18 | 19 | #+STARTUP: overview 20 | 21 | * Setup :noexport: 22 | :PROPERTIES: 23 | :CUSTOM_ID: h-A21B78FB 24 | :END: 25 | 26 | #+begin_src emacs-lisp :eval yes 27 | (setq org-confirm-babel-evaluate nil) 28 | #+end_src 29 | 30 | Publishing 31 | 32 | #+begin_src emacs-lisp :eval yes 33 | (setq base-dir (concat (projectile-project-root) ".org")) 34 | 35 | (setq pub-dir (projectile-project-root)) 36 | 37 | (setq org-publish-project-alist 38 | `(("unidata-tomcat-readme" 39 | :base-directory ,base-dir 40 | :recursive t 41 | :base-extension "org" 42 | :publishing-directory ,pub-dir 43 | :publishing-function org-gfm-publish-to-gfm))) 44 | #+end_src 45 | 46 | * Unidata Tomcat Docker 47 | :PROPERTIES: 48 | :CUSTOM_ID: h-C944C5F1 49 | :END: 50 | 51 | A security-hardened Tomcat container for [[https://github.com/Unidata/thredds-docker][thredds-docker]] and [[https://github.com/Unidata/ramadda-docker][ramadda-docker]]. 52 | 53 | ** Introduction 54 | :PROPERTIES: 55 | :CUSTOM_ID: h-1411CF81 56 | :END: 57 | 58 | This repository contains files necessary to build and run a security hardened Tomcat Docker container, based off of a canonical [[https://hub.docker.com/_/tomcat/][Tomcat base image]]. The Unidata Tomcat Docker images associated with this repository are [[https://hub.docker.com/r/unidata/tomcat-docker/][available on Docker Hub]]. All default web applications have been expunged from this container so it will primarily serve as a base image for other containers. 59 | 60 | *** Security Hardening Measures 61 | :PROPERTIES: 62 | :CUSTOM_ID: h-6C9EE33A 63 | :END: 64 | 65 | **** Introduction 66 | :PROPERTIES: 67 | :CUSTOM_ID: h-F5641083 68 | :END: 69 | This Tomcat container was security hardened according to [[https://www.owasp.org/index.php/Securing_tomcat][OWASP recommendations]]. Specifically, 70 | 71 | - Eliminated default Tomcat web applications 72 | - Run Tomcat with unprivileged user ~tomcat~ (via ~entrypoint.sh~) 73 | - Start Tomcat via Tomcat Security Manager (via ~entrypoint.sh~) 74 | - All files in ~CATALINA_HOME~ are owned by user ~tomcat~ (via 75 | ~entrypoint.sh~) 76 | - Files in ~CATALINA_HOME/conf~ are read only (~400~) by user ~tomcat~ 77 | (via ~entrypoint.sh~) 78 | - Container-wide ~umask~ of ~007~ 79 | 80 | **** web.xml Enhancements 81 | :PROPERTIES: 82 | :CUSTOM_ID: h-76CE835C 83 | :END: 84 | 85 | The following changes have been made to [[./web.xml][web.xml]] from the out-of-the-box version: 86 | 87 | - Added ~SAMEORIGIN~ anti-clickjacking option 88 | - HTTP header security filter (~httpHeaderSecurity~) uncommented/enabled 89 | - Cross-origin resource sharing (CORS) filtering (~CorsFilter~) added/enabled (see below to disable) 90 | - Stack traces are not returned to user through ~error-page~ element. 91 | 92 | ***** CORS 93 | :PROPERTIES: 94 | :CUSTOM_ID: h-6D53D9B2 95 | :END: 96 | 97 | This image enables the [[https://tomcat.apache.org/tomcat-8.5-doc/config/filter.html#CORS_Filter][Apache Tomcat CORS filter]] by default. To disable it (maybe you want to handle CORS uniformly in a proxying webserver?), set environment variable ~DISABLE_CORS~ to ~1~. 98 | 99 | **** server.xml Enhancements 100 | :PROPERTIES: 101 | :CUSTOM_ID: h-8027E0B0 102 | :END: 103 | 104 | The following changes have been made to [[./server.xml][server.xml]] from the out-of-the-box version: 105 | 106 | - Server version information is obscured to user via ~server~ attribute for all ~Connector~ elements 107 | - ~secure~ attribute set to ~true~ for all ~Connector~ elements 108 | - Shutdown port disabled 109 | - Digested passwords. See next section. 110 | 111 | The active ~Connector~ has ~relaxedPathChars~ and ~relaxedQueryChars~ attributes. This change may not be optimal for security, but must be done [[https://github.com/Unidata/thredds-docker/issues/209][to accommodate DAP requests]] which THREDDS and RAMADDA must perform. 112 | 113 | **** Digested Passwords 114 | :PROPERTIES: 115 | :CUSTOM_ID: h-4CE92D2E 116 | :END: 117 | 118 | This container has a ~UserDatabaseRealm~, ~Realm~ element in ~server.xml~ with a default ~CredentialHandler~ ~algorithm~ of ~sha-512~. This modification is an improvement over the clear text password default that comes with the parent container (~tomcat:8.5-jdk11~). Passwords defined in ~tomcat-users.xml~ must 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: 119 | 120 | #+begin_src sh 121 | docker run tomcat /usr/local/tomcat/bin/digest.sh -a "sha-512" mysupersecretpassword 122 | #+end_src 123 | 124 | This command will yield something like: 125 | 126 | #+begin_src sh 127 | mysupersecretpassword:94e334bc71163a69f2e984e73741f610e083a8e11764ee3e396f6935c3911f49$1$a5530e17501f83a60286f6363a8647a277c9cfdb 128 | #+end_src 129 | 130 | The hash after the ~:~ is what you will use for the ~password~ attribute in ~tomcat-users.xml~. 131 | 132 | More information about this topic is available in the [[https://tomcat.apache.org/tomcat-8.5-doc/realm-howto.html#Digested_Passwords][Tomcat documentation]]. 133 | 134 | **** CVEs 135 | :PROPERTIES: 136 | :CUSTOM_ID: h-C1DF14EF 137 | :END: 138 | 139 | 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 [[mailto:security@unidata.ucar.edu][security@unidata.ucar.edu]] to bring them to our attention. 140 | 141 | ** Versions 142 | :PROPERTIES: 143 | :CUSTOM_ID: h-6C0AB867 144 | :END: 145 | 146 | See tags listed [[https://hub.docker.com/r/unidata/tomcat-docker/tags][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. 147 | 148 | ** Prerequisites 149 | :PROPERTIES: 150 | :CUSTOM_ID: h-61809CB7 151 | :END: 152 | 153 | Before you begin using this Docker container project, make sure your system has Docker installed. Docker Compose is optional but recommended. 154 | 155 | ** Installation 156 | :PROPERTIES: 157 | :CUSTOM_ID: h-FB3558BB 158 | :END: 159 | 160 | You can either pull the image from DockerHub with: 161 | 162 | #+begin_src sh 163 | docker pull unidata/tomcat-docker: 164 | #+end_src 165 | 166 | Or you can build it yourself with: 167 | 168 | 1. **Clone the repository**: ~git clone https://github.com/Unidata/tomcat-docker.git~ 169 | 2. **Navigate to the project directory**: ~cd tomcat-docker~ 170 | 3. **Build the Docker image**: ~docker build -t tomcat-docker:~ . 171 | 172 | ** Usage 173 | :PROPERTIES: 174 | :CUSTOM_ID: h-B602CE28 175 | :END: 176 | 177 | 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: 178 | 179 | #+begin_src sh 180 | FROM unidata/tomcat-docker:8.5-jdk11 181 | #+end_src 182 | 183 | Sometimes it is useful to enter this container via bash and poke around, just to see what is there. For example, 184 | 185 | #+begin_src sh 186 | docker run -it unidata/tomcat-docker:8.5-jdk11 bash 187 | #+end_src 188 | 189 | ** Configuration 190 | :PROPERTIES: 191 | :CUSTOM_ID: h-AFA7F4DC 192 | :END: 193 | *** Configurable Tomcat UID and GID 194 | :PROPERTIES: 195 | :CUSTOM_ID: h-E4632DC9 196 | :END: 197 | 198 | The problem with mounted Docker volumes and UID/GID mismatch headaches is best explained here: https://denibertovic.com/posts/handling-permissions-with-docker-volumes/. 199 | 200 | This container allows the possibility of controlling the UID/GID of the ~tomcat~ user inside the container via ~TOMCAT_USER_ID~ and ~TOMCAT_GROUP_ID~ environment variables. If not set, the default UID/GID is ~1000/1000~. For example, 201 | 202 | #+begin_src sh 203 | docker run --name tomcat \ 204 | -e TOMCAT_USER_ID=`id -u` \ 205 | -e TOMCAT_GROUP_ID=`getent group $USER | cut -d':' -f3` \ 206 | -v `pwd`/logs:/usr/local/tomcat/logs/ \ 207 | -v /path/to/your/webapp:/usr/local/tomcat/webapps \ 208 | -d -p 8080:8080 unidata/tomcat-docker: 209 | #+end_src 210 | 211 | where ~TOMCAT_USER_ID~ and ~TOMCAT_GROUP_ID~ have been configured with the UID/GID of the user running the container. If using ~docker-compose~, see ~compose.env~ to configure the UID/GID of user ~tomcat~ inside the container. 212 | 213 | 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~). 214 | 215 | Note that containers that inherit this container and have overridden ~entrypoint.sh~ will have to take into account user ~tomcat~ is no longer assumed in the ~Dockerfile~. Rather the ~tomcat~ user is now created within the ~entrypoint.sh~ and those overriding ~entrypoint.sh~ should take this fact into account. Also note that this UID/GID configuration option will not work on operating systems where Docker is not native (e.g., macOS). 216 | 217 | *** HTTPS 218 | :PROPERTIES: 219 | :CUSTOM_ID: h-D725A36E 220 | :END: 221 | 222 | This Tomcat container can support HTTPS for either self-signed certificates which can be useful for experimentation or certificates from a CA for a production server. For a complete treatment on this topic, see https://tomcat.apache.org/tomcat-8.5-doc/ssl-howto.html. 223 | 224 | **** Self-signed Certificates 225 | :PROPERTIES: 226 | :CUSTOM_ID: h-C24884FC 227 | :END: 228 | 229 | This Tomcat container can support HTTP over SSL. For example, generate a self-signed certificate with ~openssl~ (or better yet, obtain a real certificate from a certificate authority): 230 | 231 | #+begin_src sh 232 | openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj \ 233 | "/C=US/ST=Colorado/L=Boulder/O=Unidata/CN=tomcat.example.com" -keyout \ 234 | ./ssl.key -out ./ssl.crt 235 | #+end_src 236 | 237 | Then augment the ~server.xml~ from this repository with this additional XML snippet for [[https://tomcat.apache.org/tomcat-8.0-doc/ssl-howto.html][Tomcat SSL capability]]: 238 | 239 | #+begin_src xml 240 | 250 | #+end_src 251 | 252 | Mount over the existing ~server.xml~ and add the SSL certificate and 253 | private key with: 254 | 255 | #+begin_src sh 256 | docker run -it -d -p 80:8080 -p 443:8443 \ 257 | -v /path/to/server.xml:/usr/local/tomcat/conf/server.xml \ 258 | -v /path/to/ssl.crt:/usr/local/tomcat/conf/ssl.crt \ 259 | -v /path/to/ssl.key:/usr/local/tomcat/conf/ssl.key \ 260 | unidata/tomcat-docker: 261 | #+end_src 262 | 263 | or if using ~docker-compose~ the ~docker-compose.yml~ will look like: 264 | 265 | #+begin_src yaml 266 | unidata-tomcat: 267 | image: unidata/tomcat-docker: 268 | ports: 269 | - "80:8080" 270 | - "443:8443" 271 | volumes: 272 | - /path/to/ssl.crt:/usr/local/tomcat/conf/ssl.crt 273 | - /path/to/ssl.key:/usr/local/tomcat/conf/ssl.key 274 | - /path/to/server.xml:/usr/local/tomcat/conf/server.xml 275 | #+end_src 276 | 277 | **** Certificate from CA 278 | :PROPERTIES: 279 | :CUSTOM_ID: h-B5E124BB 280 | :END: 281 | 282 | First, obtain a certificate from a certificate authority (CA). This process will yield a ~.key~ and ~.crt~ file. To meet enhanced security guidelines you, will want to serve a certificate with the intermediate and root certificates present in the ~ssl.crt~ file. For Tomcat to serve the certificate chain, you have to put your ~.key~ and ~.crt~ (containing the intermediate and root certificates) in a Java keystore. The [[https://keystore-explorer.org/][Keystore Explorer]] tool is a helpful app to assist you in building a valid certificate chain as well as exploring Java keystores. 283 | 284 | First put the ~.key~ and ~.crt~ in a ~.p12~ file: 285 | 286 | #+begin_src sh 287 | openssl pkcs12 -export -in ssl.crt.fullchain -inkey ssl.key -out ssl.p12 -name \ 288 | mydomain.com 289 | #+end_src 290 | 291 | Then add the ~.p12~ file to the keystore: 292 | 293 | #+begin_src 294 | keytool -importkeystore -destkeystore keystore.jks -srckeystore ssl.p12 \ 295 | -srcstoretype PKCS12 296 | #+end_src 297 | 298 | When prompted for passwords in the two steps above, consider reusing the same password to reduce cognitive load. If you see the following message 299 | 300 | #+begin_example 301 | Warning: The JKS keystore uses a proprietary format. It is recommended to 302 | migrate to PKCS12 which is an industry standard format using "keytool 303 | -importkeystore -srckeystore keystore.jks -destkeystore keystore.jks 304 | -deststoretype pkcs12". 305 | #+end_example 306 | 307 | ignore it. 308 | 309 | You'll then refer to that keystore in your ~server.xml~: 310 | 311 | #+begin_src xml 312 | 328 | #+end_src 329 | 330 | Note there are a few differences with the ~Connector~ described for the self-signed certificate above. These additions are made according to enhanced security guidelines. 331 | 332 | Mount over the existing ~server.xml~ and add the SSL certificate and private key with: 333 | 334 | #+begin_src sh 335 | docker run -it -d -p 80:8080 -p 443:8443 \ 336 | -v /path/to/server.xml:/usr/local/tomcat/conf/server.xml \ 337 | -v /path/to/ssl.jks:/usr/local/tomcat/conf/ssl.jks \ 338 | unidata/tomcat-docker: 339 | #+end_src 340 | 341 | or if using ~docker-compose~ the ~docker-compose.yml~ will look like: 342 | 343 | #+begin_src yaml 344 | unidata-tomcat: 345 | image: unidata/tomcat-docker: 346 | ports: 347 | - "80:8080" 348 | - "443:8443" 349 | volumes: 350 | - /path/to/ssl.jks:/usr/local/tomcat/conf/ssl.jks 351 | - /path/to/server.xml:/usr/local/tomcat/conf/server.xml 352 | #+end_src 353 | 354 | **** Force HTTPS 355 | :PROPERTIES: 356 | :CUSTOM_ID: h-787A700F 357 | :END: 358 | 359 | Once you have your certificates in order, make HTTPS mandatory. Add this snippet as the final element in ~web.xml~. Mount over the ~web.xml~ inside the container with this enhanced ~web.xml~ in the same manner we have been doing to ~server.xml~ as discussed herein. 360 | 361 | #+begin_src xml 362 | 363 | 364 | 365 | Protected Context 366 | /* 367 | 368 | 369 | CONFIDENTIAL 370 | 371 | 372 | #+end_src 373 | 374 | ** Testing 375 | :PROPERTIES: 376 | :CUSTOM_ID: h-32889858 377 | :END: 378 | 379 | If you would like to do a small test to ensure the Unidata Tomcat Docker image is working: 380 | 381 | #+begin_src sh 382 | mkdir -p /tmp/test 383 | wget -O /tmp/test/sample.war https://tomcat.apache.org/tomcat-8.5-doc/appdev/sample/sample.war 384 | docker run --name tomcat -e TOMCAT_USER_ID=1000 -e TOMCAT_GROUP_ID=1000 -v /tmp/test/:/usr/local/tomcat/webapps -d -p 8080:8080 unidata/tomcat-docker: 385 | curl http://127.0.0.1:8080/sample/index.html 386 | #+end_src 387 | 388 | This should yield some HTML that starts like this: 389 | 390 | #+begin_src html 391 | 392 | 393 | Sample "Hello, World" Application 394 | 395 | ... 396 | #+end_src 397 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | # Dockerfile for Unidata Tomcat. 3 | ### 4 | FROM tomcat:9.0-jdk11 5 | 6 | MAINTAINER Unidata 7 | 8 | # Install necessary packages 9 | RUN apt-get update && \ 10 | apt-get install -y --no-install-recommends \ 11 | gosu \ 12 | zip \ 13 | unzip \ 14 | && \ 15 | # Cleanup 16 | apt-get clean && \ 17 | rm -rf /var/lib/apt/lists/* && \ 18 | # Eliminate default web applications 19 | rm -rf ${CATALINA_HOME}/webapps/* && \ 20 | rm -rf ${CATALINA_HOME}/webapps.dist && \ 21 | # Obscuring server info 22 | cd ${CATALINA_HOME}/lib && \ 23 | mkdir -p org/apache/catalina/util/ && \ 24 | unzip -j catalina.jar org/apache/catalina/util/ServerInfo.properties \ 25 | -d org/apache/catalina/util/ && \ 26 | sed -i 's/server.info=.*/server.info=Apache Tomcat/g' \ 27 | org/apache/catalina/util/ServerInfo.properties && \ 28 | zip -ur catalina.jar \ 29 | org/apache/catalina/util/ServerInfo.properties && \ 30 | rm -rf org && cd ${CATALINA_HOME} && \ 31 | # Setting restrictive umask container-wide 32 | echo "session optional pam_umask.so" >> /etc/pam.d/common-session && \ 33 | sed -i 's/UMASK.*022/UMASK 007/g' /etc/login.defs 34 | 35 | # Security enhanced web.xml 36 | COPY web.xml ${CATALINA_HOME}/conf/ 37 | 38 | # Security enhanced server.xml 39 | COPY server.xml ${CATALINA_HOME}/conf/ 40 | 41 | # Tomcat start script 42 | COPY start-tomcat.sh ${CATALINA_HOME}/bin 43 | COPY entrypoint.sh / 44 | 45 | # Start container 46 | ENTRYPOINT ["/entrypoint.sh"] 47 | CMD ["start-tomcat.sh"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016-2020, University Corporation for Atmospheric Research 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Unidata Tomcat Docker](#h-C944C5F1) 2 | - [Introduction](#h-1411CF81) 3 | - [Security Hardening Measures](#h-6C9EE33A) 4 | - [Introduction](#h-F5641083) 5 | - [web.xml Enhancements](#h-76CE835C) 6 | - [CORS](#h-6D53D9B2) 7 | - [server.xml Enhancements](#h-8027E0B0) 8 | - [Digested Passwords](#h-4CE92D2E) 9 | - [CVEs](#h-C1DF14EF) 10 | - [Versions](#h-6C0AB867) 11 | - [Prerequisites](#h-61809CB7) 12 | - [Installation](#h-FB3558BB) 13 | - [Usage](#h-B602CE28) 14 | - [Configuration](#h-AFA7F4DC) 15 | - [Configurable Tomcat UID and GID](#h-E4632DC9) 16 | - [HTTPS](#h-D725A36E) 17 | - [Self-signed Certificates](#h-C24884FC) 18 | - [Certificate from CA](#h-B5E124BB) 19 | - [Force HTTPS](#h-787A700F) 20 | - [Testing](#h-32889858) 21 | 22 | 23 | 24 | 25 | 26 | # Unidata Tomcat Docker 27 | 28 | A security-hardened Tomcat container for [thredds-docker](https://github.com/Unidata/thredds-docker) and [ramadda-docker](https://github.com/Unidata/ramadda-docker). 29 | 30 | 31 | 32 | 33 | ## Introduction 34 | 35 | This repository contains files necessary to build and run a security hardened Tomcat Docker container, based off of a canonical [Tomcat base image](https://hub.docker.com/_/tomcat/). The Unidata Tomcat Docker images associated with this repository are [available on Docker Hub](https://hub.docker.com/r/unidata/tomcat-docker/). All default web applications have been expunged from this container so it will primarily serve as a base image for other containers. 36 | 37 | 38 | 39 | 40 | ### Security Hardening Measures 41 | 42 | 43 | 44 | 45 | #### Introduction 46 | 47 | This Tomcat container was security hardened according to [OWASP recommendations](https://www.owasp.org/index.php/Securing_tomcat). Specifically, 48 | 49 | - Eliminated default Tomcat web applications 50 | - Run Tomcat with unprivileged user `tomcat` (via `entrypoint.sh`) 51 | - Start Tomcat via Tomcat Security Manager (via `entrypoint.sh`) 52 | - All files in `CATALINA_HOME` are owned by user `tomcat` (via `entrypoint.sh`) 53 | - Files in `CATALINA_HOME/conf` are read only (`400`) by user `tomcat` (via `entrypoint.sh`) 54 | - Container-wide `umask` of `007` 55 | 56 | 57 | 58 | 59 | #### web.xml Enhancements 60 | 61 | The following changes have been made to [web.xml](./web.xml) from the out-of-the-box version: 62 | 63 | - Added `SAMEORIGIN` anti-clickjacking option 64 | - HTTP header security filter (`httpHeaderSecurity`) uncommented/enabled 65 | - Cross-origin resource sharing (CORS) filtering (`CorsFilter`) added/enabled (see below to disable) 66 | - Stack traces are not returned to user through `error-page` element. 67 | 68 | 69 | 70 | 71 | ##### CORS 72 | 73 | This image enables the [Apache Tomcat CORS filter](https://tomcat.apache.org/tomcat-8.5-doc/config/filter.html#CORS_Filter) by default. To disable it (maybe you want to handle CORS uniformly in a proxying webserver?), set environment variable `DISABLE_CORS` to `1`. 74 | 75 | 76 | 77 | 78 | #### server.xml Enhancements 79 | 80 | The following changes have been made to [server.xml](./server.xml) from the out-of-the-box version: 81 | 82 | - Server version information is obscured to user via `server` attribute for all `Connector` elements 83 | - `secure` attribute set to `true` for all `Connector` elements 84 | - Shutdown port disabled 85 | - Digested passwords. See next section. 86 | 87 | The active `Connector` has `relaxedPathChars` and `relaxedQueryChars` attributes. This change may not be optimal for security, but must be done [to accommodate DAP requests](https://github.com/Unidata/thredds-docker/issues/209) which THREDDS and RAMADDA must perform. 88 | 89 | 90 | 91 | 92 | #### Digested Passwords 93 | 94 | This container has a `UserDatabaseRealm`, `Realm` element in `server.xml` with a default `CredentialHandler` `algorithm` of `sha-512`. This modification is an improvement over the clear text password default that comes with the parent container (`tomcat:8.5-jdk11`). Passwords defined in `tomcat-users.xml` must 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: 95 | 96 | ```sh 97 | docker run tomcat /usr/local/tomcat/bin/digest.sh -a "sha-512" mysupersecretpassword 98 | ``` 99 | 100 | This command will yield something like: 101 | 102 | ```sh 103 | mysupersecretpassword:94e334bc71163a69f2e984e73741f610e083a8e11764ee3e396f6935c3911f49$1$a5530e17501f83a60286f6363a8647a277c9cfdb 104 | ``` 105 | 106 | The hash after the `:` is what you will use for the `password` attribute in `tomcat-users.xml`. 107 | 108 | More information about this topic is available in the [Tomcat documentation](https://tomcat.apache.org/tomcat-8.5-doc/realm-howto.html#Digested_Passwords). 109 | 110 | 111 | 112 | 113 | #### CVEs 114 | 115 | 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](mailto:security@unidata.ucar.edu) to bring them to our attention. 116 | 117 | 118 | 119 | 120 | ## Versions 121 | 122 | See tags listed [on dockerhub](https://hub.docker.com/r/unidata/tomcat-docker/tags). 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. 123 | 124 | 125 | 126 | 127 | ## Prerequisites 128 | 129 | Before you begin using this Docker container project, make sure your system has Docker installed. Docker Compose is optional but recommended. 130 | 131 | 132 | 133 | 134 | ## Installation 135 | 136 | You can either pull the image from DockerHub with: 137 | 138 | ```sh 139 | docker pull unidata/tomcat-docker: 140 | ``` 141 | 142 | Or you can build it yourself with: 143 | 144 | 1. ****Clone the repository****: `git clone https://github.com/Unidata/tomcat-docker.git` 145 | 2. ****Navigate to the project directory****: `cd tomcat-docker` 146 | 3. ****Build the Docker image****: `docker build -t tomcat-docker:` . 147 | 148 | 149 | 150 | 151 | ## Usage 152 | 153 | 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: 154 | 155 | ```sh 156 | FROM unidata/tomcat-docker:8.5-jdk11 157 | ``` 158 | 159 | Sometimes it is useful to enter this container via bash and poke around, just to see what is there. For example, 160 | 161 | ```sh 162 | docker run -it unidata/tomcat-docker:8.5-jdk11 bash 163 | ``` 164 | 165 | 166 | 167 | 168 | ## Configuration 169 | 170 | 171 | 172 | 173 | ### Configurable Tomcat UID and GID 174 | 175 | The problem with mounted Docker volumes and UID/GID mismatch headaches is best explained here: . 176 | 177 | This container allows the possibility of controlling the UID/GID of the `tomcat` user inside the container via `TOMCAT_USER_ID` and `TOMCAT_GROUP_ID` environment variables. If not set, the default UID/GID is `1000/1000`. For example, 178 | 179 | ```sh 180 | docker run --name tomcat \ 181 | -e TOMCAT_USER_ID=`id -u` \ 182 | -e TOMCAT_GROUP_ID=`getent group $USER | cut -d':' -f3` \ 183 | -v `pwd`/logs:/usr/local/tomcat/logs/ \ 184 | -v /path/to/your/webapp:/usr/local/tomcat/webapps \ 185 | -d -p 8080:8080 unidata/tomcat-docker: 186 | ``` 187 | 188 | where `TOMCAT_USER_ID` and `TOMCAT_GROUP_ID` have been configured with the UID/GID of the user running the container. If using `docker-compose`, see `compose.env` to configure the UID/GID of user `tomcat` inside the container. 189 | 190 | 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`). 191 | 192 | Note that containers that inherit this container and have overridden `entrypoint.sh` will have to take into account user `tomcat` is no longer assumed in the `Dockerfile`. Rather the `tomcat` user is now created within the `entrypoint.sh` and those overriding `entrypoint.sh` should take this fact into account. Also note that this UID/GID configuration option will not work on operating systems where Docker is not native (e.g., macOS). 193 | 194 | 195 | 196 | 197 | ### HTTPS 198 | 199 | This Tomcat container can support HTTPS for either self-signed certificates which can be useful for experimentation or certificates from a CA for a production server. For a complete treatment on this topic, see . 200 | 201 | 202 | 203 | 204 | #### Self-signed Certificates 205 | 206 | This Tomcat container can support HTTP over SSL. For example, generate a self-signed certificate with `openssl` (or better yet, obtain a real certificate from a certificate authority): 207 | 208 | ```sh 209 | openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj \ 210 | "/C=US/ST=Colorado/L=Boulder/O=Unidata/CN=tomcat.example.com" -keyout \ 211 | ./ssl.key -out ./ssl.crt 212 | ``` 213 | 214 | Then augment the `server.xml` from this repository with this additional XML snippet for [Tomcat SSL capability](https://tomcat.apache.org/tomcat-8.0-doc/ssl-howto.html): 215 | 216 | ```xml 217 | 227 | ``` 228 | 229 | Mount over the existing `server.xml` and add the SSL certificate and private key with: 230 | 231 | ```sh 232 | docker run -it -d -p 80:8080 -p 443:8443 \ 233 | -v /path/to/server.xml:/usr/local/tomcat/conf/server.xml \ 234 | -v /path/to/ssl.crt:/usr/local/tomcat/conf/ssl.crt \ 235 | -v /path/to/ssl.key:/usr/local/tomcat/conf/ssl.key \ 236 | unidata/tomcat-docker: 237 | ``` 238 | 239 | or if using `docker-compose` the `docker-compose.yml` will look like: 240 | 241 | ```yaml 242 | unidata-tomcat: 243 | image: unidata/tomcat-docker: 244 | ports: 245 | - "80:8080" 246 | - "443:8443" 247 | volumes: 248 | - /path/to/ssl.crt:/usr/local/tomcat/conf/ssl.crt 249 | - /path/to/ssl.key:/usr/local/tomcat/conf/ssl.key 250 | - /path/to/server.xml:/usr/local/tomcat/conf/server.xml 251 | ``` 252 | 253 | 254 | 255 | 256 | #### Certificate from CA 257 | 258 | First, obtain a certificate from a certificate authority (CA). This process will yield a `.key` and `.crt` file. To meet enhanced security guidelines you, will want to serve a certificate with the intermediate and root certificates present in the `ssl.crt` file. For Tomcat to serve the certificate chain, you have to put your `.key` and `.crt` (containing the intermediate and root certificates) in a Java keystore. The [Keystore Explorer](https://keystore-explorer.org/) tool is a helpful app to assist you in building a valid certificate chain as well as exploring Java keystores. 259 | 260 | First put the `.key` and `.crt` in a `.p12` file: 261 | 262 | ```sh 263 | openssl pkcs12 -export -in ssl.crt.fullchain -inkey ssl.key -out ssl.p12 -name \ 264 | mydomain.com 265 | ``` 266 | 267 | Then add the `.p12` file to the keystore: 268 | 269 | ``` 270 | keytool -importkeystore -destkeystore keystore.jks -srckeystore ssl.p12 \ 271 | -srcstoretype PKCS12 272 | ``` 273 | 274 | When prompted for passwords in the two steps above, consider reusing the same password to reduce cognitive load. If you see the following message 275 | 276 | ``` 277 | Warning: The JKS keystore uses a proprietary format. It is recommended to 278 | migrate to PKCS12 which is an industry standard format using "keytool 279 | -importkeystore -srckeystore keystore.jks -destkeystore keystore.jks 280 | -deststoretype pkcs12". 281 | ``` 282 | 283 | ignore it. 284 | 285 | You'll then refer to that keystore in your `server.xml`: 286 | 287 | ```xml 288 | 304 | ``` 305 | 306 | Note there are a few differences with the `Connector` described for the self-signed certificate above. These additions are made according to enhanced security guidelines. 307 | 308 | Mount over the existing `server.xml` and add the SSL certificate and private key with: 309 | 310 | ```sh 311 | docker run -it -d -p 80:8080 -p 443:8443 \ 312 | -v /path/to/server.xml:/usr/local/tomcat/conf/server.xml \ 313 | -v /path/to/ssl.jks:/usr/local/tomcat/conf/ssl.jks \ 314 | unidata/tomcat-docker: 315 | ``` 316 | 317 | or if using `docker-compose` the `docker-compose.yml` will look like: 318 | 319 | ```yaml 320 | unidata-tomcat: 321 | image: unidata/tomcat-docker: 322 | ports: 323 | - "80:8080" 324 | - "443:8443" 325 | volumes: 326 | - /path/to/ssl.jks:/usr/local/tomcat/conf/ssl.jks 327 | - /path/to/server.xml:/usr/local/tomcat/conf/server.xml 328 | ``` 329 | 330 | 331 | 332 | 333 | #### Force HTTPS 334 | 335 | Once you have your certificates in order, make HTTPS mandatory. Add this snippet as the final element in `web.xml`. Mount over the `web.xml` inside the container with this enhanced `web.xml` in the same manner we have been doing to `server.xml` as discussed herein. 336 | 337 | ```xml 338 | 339 | 340 | 341 | Protected Context 342 | /* 343 | 344 | 345 | CONFIDENTIAL 346 | 347 | 348 | ``` 349 | 350 | 351 | 352 | 353 | ## Testing 354 | 355 | If you would like to do a small test to ensure the Unidata Tomcat Docker image is working: 356 | 357 | ```sh 358 | mkdir -p /tmp/test 359 | wget -O /tmp/test/sample.war https://tomcat.apache.org/tomcat-8.5-doc/appdev/sample/sample.war 360 | docker run --name tomcat -e TOMCAT_USER_ID=1000 -e TOMCAT_GROUP_ID=1000 -v /tmp/test/:/usr/local/tomcat/webapps -d -p 8080:8080 unidata/tomcat-docker: 361 | curl http://127.0.0.1:8080/sample/index.html 362 | ``` 363 | 364 | This should yield some HTML that starts like this: 365 | 366 | ```html 367 | 368 | 369 | Sample "Hello, World" Application 370 | 371 | ... 372 | ``` 373 | -------------------------------------------------------------------------------- /compose.env: -------------------------------------------------------------------------------- 1 | # Set to your liking 2 | TOMCAT_USER_ID=1000 3 | TOMCAT_GROUP_ID=1000 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | ### 2 | # docker-compose.yml for testing and experimentation 3 | ### 4 | 5 | ### 6 | # Unidata Docker Tomcat 7 | ### 8 | 9 | unidata-tomcat: 10 | image: unidata/tomcat-docker:latest 11 | ports: 12 | - "80:8080" 13 | container_name: unidata-tomcat 14 | volumes: 15 | - ./logs/:/usr/local/tomcat/logs/ 16 | env_file: 17 | - "compose.env" 18 | 19 | ### 20 | # Generic Tomcat 21 | ### 22 | 23 | tomcat: 24 | image: tomcat:8.5-jre8 25 | ports: 26 | - "80:8080" 27 | container_name: tomcat 28 | volumes: 29 | - ./logs/:/usr/local/tomcat/logs/ 30 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # preferable to fire up Tomcat via start-tomcat.sh which will start Tomcat with 5 | # security manager, but inheriting containers can also start Tomcat via 6 | # catalina.sh 7 | 8 | if [ "$1" = 'start-tomcat.sh' ] || [ "$1" = 'catalina.sh' ]; then 9 | 10 | USER_ID=${TOMCAT_USER_ID:-1000} 11 | GROUP_ID=${TOMCAT_GROUP_ID:-1000} 12 | 13 | ### 14 | # Tomcat user 15 | ### 16 | # create group for GROUP_ID if one doesn't already exist 17 | if ! getent group $GROUP_ID &> /dev/null; then 18 | groupadd -r tomcat -g $GROUP_ID 19 | fi 20 | # create user for USER_ID if one doesn't already exist 21 | if ! getent passwd $USER_ID &> /dev/null; then 22 | useradd -u $USER_ID -g $GROUP_ID tomcat 23 | fi 24 | # alter USER_ID with nologin shell and CATALINA_HOME home directory 25 | usermod -d "${CATALINA_HOME}" -s /sbin/nologin $(id -u -n $USER_ID) 26 | 27 | ### 28 | # Change CATALINA_HOME ownership to tomcat user and tomcat group 29 | # Restrict permissions on conf 30 | ### 31 | 32 | chown -R $USER_ID:$GROUP_ID ${CATALINA_HOME} && find ${CATALINA_HOME}/conf \ 33 | -type d -exec chmod 755 {} \; -o -type f -exec chmod 400 {} \; 34 | sync 35 | 36 | ### 37 | # Deactivate CORS filter in web.xml if DISABLE_CORS=1 38 | # Useful if CORS is handled outside of Tomcat (e.g. in a proxying webserver like nginx) 39 | ### 40 | if [ "$DISABLE_CORS" == "1" ]; then 41 | echo "Deactivating Tomcat CORS filter" 42 | sed -i 's/\n/-->/' \ 43 | ${CATALINA_HOME}/conf/web.xml 44 | fi 45 | 46 | exec gosu $USER_ID "$@" 47 | fi 48 | 49 | exec "$@" 50 | -------------------------------------------------------------------------------- /server.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 41 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 69 | 76 | 77 | 85 | 92 | 103 | 109 | 121 | 122 | 123 | 131 | 132 | 137 | 138 | 141 | 142 | 143 | 146 | 149 | 150 | 152 | 153 | 157 | 159 | 160 | 161 | 162 | 163 | 165 | 166 | 168 | 171 | 172 | 175 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /server.xml.ORIG: -------------------------------------------------------------------------------- 1 | 2 | 18 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 41 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 69 | 74 | 75 | 83 | 90 | 101 | 107 | 121 | 122 | 123 | 131 | 132 | 137 | 138 | 141 | 142 | 143 | 146 | 149 | 150 | 152 | 153 | 157 | 159 | 160 | 161 | 163 | 164 | 166 | 169 | 170 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /start-tomcat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | trap "echo TRAPed signal" HUP INT QUIT KILL TERM 7 | 8 | startup.sh -security 9 | 10 | # never exit 11 | while true; do sleep 10000; done 12 | 13 | --------------------------------------------------------------------------------