Sorcerer's Tower

Configuring Jetty for HTTPS with Let's Encrypt

The Jetty documentation for Configuring SSL/TLS is long and daunting, but makes no mention of how to work with the EFF's Let's Encrypt certificate authority, which provides free automated certificates with the aim of having the entire web available over HTTPS.

This article provides the steps for obtaining a Let's Encrypt certificate, importing it into Jetty, enabling HTTPS using the certificate, and handling renewals.

It assumes you have Jetty setup in a home/base configuration, serving over HTTP for one or more Internet-facing domain names.

As with all such guides, it is recommended to read all steps before making any changes, and ensure you have backups for any existing files you may modify.


1. Getting a Certificate with Certbot

Certbot is a command line tool for requesting Let's Encrypt certificates.

Obtaining a certificate is as easy as certbot certonly --webroot -w {webroot} -d {domain}

For multiple domains, specify multiple webroot/domain pairs in order, for example:

certbot certonly --webroot \
 -w /var/www/example.com -d www.example.com \
 -w /var/www/example.net -d www.example.net

(The backslashes here and in later examples escape the neighbouring newline characters, allowing copy-paste whilst maintaining readability. If typing the command on a single line omit the backslashes.)

The first time you run Certbot, it prompts you to create your Let's Encrypt ACME account - simply follow the prompts. Use a valid email address to receive a notification email sent 20 days before a certificate expires - i.e. letting you know if renewal hasn't worked.

Validation

To validate that the domain(s) in the certificate resolves to the server asking for it, Certbot creates a temporary file in each webroot, at {webroot}/.well-known/acme-challenge/{validationfile} and the Let's Encrypt validation server requests them via http://{domain}/.well-known/acme-challenge/{validationfile}

If you have Jetty URL rewriting in effect, you need to ensure the validation request is excluded. This can be as simple as prefixing your regex with (?!/\.well-known/)

If the validation does not succeed, you can update your configuration and repeat the command. (Do not undo changes afterwards - revalidation will occur at each renewal.)

Once validation is succesful, Certbot creates various certificate files inside the directory /etc/letsencrypt/live/www.example.com - there is one directory per certificate, named after the first domain specified in the command.


2. Importing Let's Encrypt PEM certificates into Jetty

In an ideal world, this stage would be as simple as enabling a module and Jetty would look in the Let's Encrypt directory and use the certificates it found.

Unfortunately, this is not the case, and there are two extra steps.

Convert the certificates from PEM format to PKCS12 format

openssl pkcs12 -export \
    -inkey privkey.pem -in fullchain.pem \
    -out jetty.pkcs12 -passout pass:p

The insecure password of p is assigned to the file jetty.pkcs12 because Java's keytool (next step) cannot import files without passwords.

Import the PKCS12 certificates into a Java keystore file

keytool -importkeystore -noprompt \
    -srckeystore jetty.pkcs12 -srcstoretype PKCS12 -srcstorepass p \
    -destkeystore keystore -deststorepass storep

Again, we set a trivial password of storep because keytool cannot export files without passwords either, and has a six character minimum.

Having these files password protected matters if they will be on a machine with untrusted users. If that is your case, replace the values with something suitably secure.

It is the keystore file generated by the keytool command which Jetty must be configured to use, so move the keystore to {jetty-base}/etc/keystore and use chown/chmod to give it appropriate permissions.


3. Enabling HTTPS in Jetty

Enabling both https and ssl modules can be done by adding --module=https to your start.ini (or equivalent start.d config), and optionally the default port of 8443 can be changed by specifying jetty.ssl.port=443

To use the keystore requires an SslContextFactory, which is done via XML configuration.

Create {jetty-base}/etc/jetty-ssl-context.xml with the following content:

<Configure id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory$Server">
    <Set name="KeyStorePath">etc/keystore</Set>
    <Set name="KeyStorePassword">storep</Set>
</Configure>

(You can optionally add the XML and Jetty DOCTYPE lines, but it works without them.)

Both KeyStorePath and KeyStorePassword must be explicitly specified, even if using default values - whilst a missing KeyStorePassword prevents Jetty from starting, a missing KeyStorePath does not (but it will fail to serve HTTPS).

(If your password needs to be secret, make sure untrusted users are prevented from reading the XML file.)

Launch Jetty and your startup log should contain a line for oejus.SslContextFactory referencing the domain(s) in the certificate and the location of the keystore.

INFO:oejus.SslContextFactory:main: x509=X509@12a3bc4d(1,h=[www.example.com, www.example.net],w=[]) for Server@56efab78[provider=null,keyStore=/opt/jetty-base/etc/keystore,trustStore=null]

Confirm Jetty is serving requests with curl, for example:

curl -IsS https://www.example.com

You should see a 200 OK (or whatever you've configured the default response to be).

If you get certificate errors that disappear after adding -k to the above, this indicates Jetty is working but your certificate is not correct - pay attention to the error message.


4. Certificate renewal

Renewing Let's Encrypt certificates is really simple - run certbot renew and it renews any certificates that will expire soon (for the default 90 day certificates this means when there is less than 30 days to expiry).

This command can be scheduled to run every 24 hours and it only processes when it needs to, but note that EFF recommend not running it at midnight, or other significant times, to avoid unnecessary server congestion. Having it run daily (rather than every 30 days) ensures that it has plenty of opportunity to retry renewal if there are server issues.

Again, it would be great if it were possible to point Jetty at the PEM files and have it detect Certbot's renewals without restarting, but that does not work. We need to re-convert the certificates after each renewal - i.e. run step 2 again - and then restart Jetty for the new keystore to take effect, and we don't want to be doing that every night.

Certbot Deploy Hook

Fortunately, Certbot provides the ability to run a script when a certificate is succesfully renewed. This can either be specified as part of the renewal command (certbot renew --deploy-hook /path/to/deploy-hook-script) or by putting the script within the /etc/letsencrypt/renewal-hooks/deploy directory.

The Certbot renewal documentation includes an example script - here it is modified to use the commands from step 2:

#!/bin/sh

set -e

for domain in $RENEWED_DOMAINS
do

    case $domain in
        www.example.com)
            pkpass=p
            storepass=storep
            jettybase=/opt/jetty-base

            umask 077

            cd "$RENEWED_LINEAGE"

            openssl pkcs12 -export \
             -inkey privkey.pem -in fullchain.pem \
             -out jetty.pkcs12 -passout "pass:$pkpass"

            keytool -importkeystore -noprompt \
             -srckeystore jetty.pkcs12 -srcstoretype PKCS12 -srcstorepass "$pkpass" \
             -destkeystore keystore -deststorepass "$storepass"

            chown jetty:web keystore
            chmod 400 keystore
            mv keystore "$jettybase/etc/keystore"

            # service jetty restart
            # above line superceded by ssl-reload module; see update below
        ;;
    esac

done

Save that as /etc/letsencrypt/renewal-hooks/deploy/convert-jetty-certs.sh with execute permission and it will run when certbot renew succesfully renews the certificates.

The final service jetty restart is for the new certificate to take effect, but of course it may be better to replace it with something to schedule the restart during the next quiet period, or instructs a loadbalancer to failover, or triggers an email notification, or any other process that makes sense - the script is an example to be updated as necessary.

I recommend not scheduling the certbot renew command immediately but setting a reminder to manually run it and ensure everything works smoothly at a time when any issues can be dealt with. (It's also wise to backup the existing keystore file so it can be restored if, for any reason, there's an issue with the new one.)

If you choose to go down that route, you should check whether Certbot has already setup a scheduled task as part of installation - see Certbot's documentation for further information.

Once you're happy that the hook script is behaving correctly, go ahead and [re]enable scheduled renewals, using either cron or systemd.

Update: Jetty SSL Reload Module

Since version 9.4.31, Jetty can be configured to monitor the keystore file and automatically apply the renewed certificate without a full restart.

The remainder of the deploy hook script is still needed — Jetty still does not handle PEM files — but instead of restarting the Jetty service, only the SSL keystore is reloaded and without the server going offline.

The functionality is activated by enabling the ssl-reload module, and the frequency of checks is controlled by jetty.sslContext.reload.scanInterval setting, so to check the keystore for changes once an hour would be:

--module=ssl-reload
jetty.sslContext.reload.scanInterval=3600

The Jetty documentation for this is at: https://www.eclipse.org/jetty/documentation/jetty-9/index.html#_sslcontextfactory_keystore_reload


5. Recap

1. Get the certificate

certbot certonly --webroot \
 -w /var/www/example.com -d www.example.com \
 -w /var/www/example.net -d www.example.net

2. Convert and import

cd /etc/letsencrypt/live/www.example.com

openssl pkcs12 -export \
 -inkey privkey.pem -in fullchain.pem \
 -out jetty.pkcs12 -passout pass:p

keytool -importkeystore -noprompt \
 -srckeystore jetty.pkcs12 -srcstoretype PKCS12 -srcstorepass p \
 -destkeystore keystore -deststorepass storep

chown jetty:web keystore
mv keystore /opt/jetty-base/etc/keystore

3. Update start.ini and etc/jetty-ssl-context.xml

cd /opt/jetty-base

echo -e '\n--module=https\njetty.ssl.port=443' >> start.ini
echo -e '\n--module=ssl-reload\njetty.sslContext.reload.scanInterval=3600' >> start.ini

echo -e '<Configure id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory$Server">' \
 '\n\t<Set name="KeyStorePath">etc/keystore</Set>\n\t<Set name="KeyStorePassword">storep</Set>' \
 '\n</Configure>' \
 > etc/jetty-ssl-context.xml

4. Automatic renewal.

Verify Jetty is serving over HTTPS (i.e. steps 1..3 worked ok), then add the deploy hook script: /etc/letsencrypt/renewal-hooks/deploy/convert-jetty-certs.sh

You may want to manually execute the first certbot renew to ensure everything works smoothly - setup a reminder (or two) for thirty days before the certificate expires.

Check the Certbot automated renewal documentation.




That's it! Hopefully this guide is both effective and easy to follow, but feedback is welcome.

I also monitor the jetty-users mailing list, which is likely the best avenue for any general Jetty questions.