Web servers
How to configure SSL on Apache Tomcat with a Let's Encrypt certificate
Apache Tomcat reads a Let's Encrypt certificate directly from PEM files in an SSLHostConfig connector on port 443, so no Java keystore is needed. Tomcat redirects plain HTTP to HTTPS, and a Certbot deploy hook copies renewed files and restarts Tomcat.
The steps apply to Tomcat 10.1 and 11.0 (and 9.0 with the same SSLHostConfig syntax) on Linux with Java 17 or later and Certbot 4.1 or later.
Prerequisites
Certbot needs port 80 free for its standalone challenge server, and Tomcat needs permission to bind port 443.
- Tomcat installed as a systemd service running as user
tomcat, withCATALINA_BASEat/opt/tomcat(adjust paths to your install). - Certbot from snap:
sudo snap install --classic certbot && sudo ln -s /snap/bin/certbot /usr/local/bin/certbot. - DNS
A/AAAArecords forexample.compointing at the server, for example203.0.113.10. - Inbound TCP 80 and 443 open to all sources; nothing else listening on port 80.
Step 1: Obtain the certificate
With port 80 free, Certbot's standalone mode runs its own temporary web server for the HTTP-01 challenge:
sudo certbot certonly --standalone -d example.com -d www.example.com
The files land in /etc/letsencrypt/live/example.com/: cert.pem (leaf), chain.pem (intermediates), fullchain.pem and privkey.pem.
Step 2: Copy the files where Tomcat can read them
/etc/letsencrypt/live is readable only by root. A deploy hook copies the files with the right owner after every issuance and renewal:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
dest=/opt/tomcat/conf/ssl
install -d -o tomcat -g tomcat -m 700 "$dest"
install -o tomcat -g tomcat -m 644 "$RENEWED_LINEAGE/cert.pem" "$dest/cert.pem"
install -o tomcat -g tomcat -m 644 "$RENEWED_LINEAGE/chain.pem" "$dest/chain.pem"
install -o tomcat -g tomcat -m 600 "$RENEWED_LINEAGE/privkey.pem" "$dest/privkey.pem"
systemctl restart tomcat
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh
Step 3: Configure the HTTPS connector
Edit /opt/tomcat/conf/server.xml. Add an HTTPS connector on 443 and point the HTTP connector's redirectPort at it:
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000" redirectPort="443" />
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
SSLEnabled="true" scheme="https" secure="true" maxThreads="150">
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
<SSLHostConfig protocols="+TLSv1.2+TLSv1.3">
<Certificate certificateFile="conf/ssl/cert.pem"
certificateKeyFile="conf/ssl/privkey.pem"
certificateChainFile="conf/ssl/chain.pem" />
</SSLHostConfig>
</Connector>
Relative paths resolve against CATALINA_BASE. Do not put fullchain.pem into certificateChainFile; that attribute must not include the server certificate.
Step 4: Allow port 443 and force HTTPS
An unprivileged Java process cannot bind ports below 1024 by default. Grant the capability in the systemd unit:
sudo systemctl edit tomcat
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
To redirect every request to HTTPS, add a security constraint to conf/web.xml (or to your application's web.xml) before </web-app>:
<security-constraint>
<web-resource-collection>
<web-resource-name>Entire site</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
Because Tomcat now holds port 80, renewals must not use standalone mode on port 80. Switch the renewal to webroot, serving challenges from the ROOT webapp:
sudo systemctl daemon-reload && sudo systemctl restart tomcat
sudo certbot reconfigure --cert-name example.com --webroot -w /opt/tomcat/webapps/ROOT
Verify
The endpoint should present the Let's Encrypt certificate followed by its intermediates, and HTTP should redirect to HTTPS. Check it with the SSL checker, or from any terminal:
curl ssl.sh/example.com
On the server, confirm the chain and the redirect:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9] s:'
curl -sI http://example.com | head -n 3
sudo journalctl -u tomcat -n 50 --no-pager | grep -i -E 'ssl|443'
Automatic renewal
The Certbot snap's systemd timer runs certbot renew twice a day and follows Let's Encrypt's ARI window (Certbot 4.1 and later). After each renewal the deploy hook from Step 2 copies the new files and restarts Tomcat. Run a dry run to check the challenge path:
sudo certbot renew --dry-run
Dry runs skip deploy hooks, so after the first real renewal confirm the new expiry date. Restarting Tomcat drops active connections briefly; Tomcat also exposes a JMX operation to reload SSLHostConfig without a restart (check the documentation for your version). Add the site to ssl.sh monitoring (in development) because Let's Encrypt no longer sends expiry emails.
Troubleshooting
- java.net.BindException: Permission denied on port 443: the capability from Step 4 is missing; run
sudo systemctl daemon-reload && sudo systemctl restart tomcat. - Tomcat starts but the connector fails with a key error:
privkey.pemis not readable by thetomcatuser; re-run the deploy hook. - Java or Android clients fail with PKIX errors: the chain is wrong; see PKIX path building failed and missing intermediate certificate.
- Renewal fails with timeout during connect: the webroot path is not served at
/.well-known/acme-challenge/; see certbot timeout during connect. - ERR_SSL_PROTOCOL_ERROR: the browser reached a plain HTTP connector on 443, or
SSLEnabledis missing; see ERR_SSL_PROTOCOL_ERROR.
FAQ
Can I still use a PKCS#12 keystore instead of PEM files?
Yes. Convert with openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out example.p12 -name tomcat and reference it with certificateKeystoreFile, certificateKeystorePassword and certificateKeystoreType="PKCS12". PEM files are simpler to automate because no conversion step runs on renewal.
Should Tomcat terminate TLS or sit behind a reverse proxy?
Both work. A reverse proxy such as Nginx with Certbot keeps Tomcat on localhost:8080, reloads certificates without a restart and handles HTTP/2 and redirects. Terminate TLS in Tomcat when there is no proxy layer.
Why do Java clients report PKIX path building failed against my Tomcat?
Usually the chain file is missing or contains the wrong certificate, so the client cannot reach a trusted root. Point certificateChainFile at chain.pem and check with the SSL checker. See PKIX path building failed.