Tomcat and Spring Boot with Let's Encrypt

It used to be: convert PEM to a keystore and repeat after every renewal. Current Tomcat and Spring Boot versions read the Let's Encrypt files directly, and Spring Boot even reloads them itself.

Which way fits?

Get the certificate

Let's Encrypt validates via HTTP-01 on port 80 or via DNS-01. If nothing runs on port 80, certbot’s standalone mode works; otherwise use webroot or DNS. The ACME helper gives you the commands. Afterwards the files are in /etc/letsencrypt/live/example.com/:

The permission trap

The live and archive directories are readable by root only. Tomcat and Spring Boot run as their own user and get “Permission denied”. Instead of loosening permissions, a deploy hook copies the files after each renewal to a place the application may read:

#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh  (make executable)
for f in cert.pem chain.pem fullchain.pem privkey.pem; do
  install -o tomcat -g tomcat -m 0600 "$RENEWED_LINEAGE/$f" /opt/tomcat/conf/tls/$f
done
systemctl restart tomcat

Tomcat with PEM files

Tomcat 9, 10 and 11 read PEM directly; no keystore needed. In conf/server.xml:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           SSLEnabled="true" maxParameterCount="1000">
  <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
  <SSLHostConfig protocols="TLSv1.2+TLSv1.3">
    <Certificate certificateFile="conf/tls/cert.pem"
                 certificateKeyFile="conf/tls/privkey.pem"
                 certificateChainFile="conf/tls/chain.pem" />
  </SSLHostConfig>
</Connector>

With certificateChainFile Tomcat also delivers the intermediate. To reload the certificate without a restart, use the manager: /manager/text/sslReload. The user needs the manager-script role.

Spring Boot with an SSL bundle

From Spring Boot 3.1 you describe certificates as an SSL bundle. From 3.2 Spring Boot reloads them itself when they change, so the restart in the hook is not needed:

server.port=8443
server.ssl.bundle=web
spring.ssl.bundle.pem.web.keystore.certificate=file:/opt/app/tls/fullchain.pem
spring.ssl.bundle.pem.web.keystore.private-key=file:/opt/app/tls/privkey.pem
spring.ssl.bundle.pem.web.reload-on-update=true

Spring Boot 2.7 and 3.0 have no bundles yet, but PEM works anyway:

server.ssl.certificate=file:/opt/app/tls/fullchain.pem
server.ssl.certificate-private-key=file:/opt/app/tls/privkey.pem

Older versions: create a keystore

Older Tomcat or Spring Boot versions need a keystore. The hook then recreates it at every renewal:

openssl pkcs12 -export -in "$RENEWED_LINEAGE/fullchain.pem" -inkey "$RENEWED_LINEAGE/privkey.pem" \
  -name tomcat -out /opt/app/tls/keystore.p12 -passout pass:secret
server.ssl.key-store=file:/opt/app/tls/keystore.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=secret
server.ssl.key-alias=tomcat

The Java keystore helper does the same in the browser: paste certificate and key, save as PKCS#12.

Port 443 without root

Java should not run as root and therefore cannot open ports below 1024. The usual way is to start the application on 8443 and forward 443 in the firewall:

sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443

Alternatively set AmbientCapabilities=CAP_NET_BIND_SERVICE in the systemd unit. Then check chain and protocols with the SSL check and add the server to the certificate expiry overview.

FAQ

Do I have to restart Tomcat after every renewal?

With Tomcat yes, or use sslReload in the manager. Spring Boot 3.2+ with reload-on-update reloads changed files itself.

Does Let's Encrypt work for internal servers?

Yes, with the DNS-01 challenge. The server does not have to be reachable from the internet; only a TXT record in public DNS is needed.

Tomcat says “Cannot recover key” or “keystore password was incorrect”.

Keystore and key passwords differ, or the alias is wrong. With PKCS#12 both passwords must be the same.

Does an ECDSA key work with Java?

Yes, all current Java versions support ECDSA. For very old clients certbot can get an RSA certificate with --key-type rsa.