08 April 2017
LetsEncrypt certs with embedded Jetty on
This blog is for you if you use embedded Jetty on linux (including Amazon’s own Linux variation on EC2) and want free SSL certs that automatically renew themselves. This describes what I did to make certificates work for https://www.crickam.com/
It covers:
- Generating a LetsEncrypt cert and converting it to a keystore that Jetty can use
- Adding a script to automatically renew the cert, which runs daily (it only renews the cert if it is near expiry)
- Getting Jetty to automatically refresh the renewed cert, without having to restart the server and with no downtime.
This blog will go over the scripts I made and the changes I made to my Jetty app. I’ll assume you’ve managed to
install and can run ~/certbot-auto
Enabling certbot to validate certificates
When creating certificates, you need to put able to prove that you own the domain name. cerbot
does this for you
automatically by hosting a file on your domain. While certbot can run in “standalone” mode (where it temporarily binds
to your server’s port to serve the file itself), if you want to generate certs while your app is running you need to be
able to serve files yourself from the file system. The app I was using was an uber jar serving everything from the classpath,
so I added a handler especially for certbot to use for cert validations:
private static ResourceHandler dirForLetsEncryptValidation(String dir) {
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setResourceBase(dir);
return resourceHandler;
}
public void createServer() {
// ...snip...
handlers.addHandler(dirForLetsEncryptValidation("/opt/myapp/letsencryptvalidationdir");
// ...snip...
}
Generating the cert for the first time
This is one off, so just ran manually from my user’s home dir: sudo ./certbot-auto certonly
It will ask some questions: select Place files in webroot directory (webroot)
and give it the path to the directory
specified in the special resource handler - /opt/myapp/letsencryptvalidationdir
in my example. Assuming your web app
is running (it will use http on port 80, or https on port 443, not sure which) then it will output a bunch of files
in a directory similar to /etc/letsencrypt/live/www.your-domain.com
These are pem files - to convert this to a keystore format that Java could use, I created the following script (which
needs to run as root), which lives at ~/convertkeystore.sh
#!/bin/sh
set -e
set -x
(
cd /etc/letsencrypt/live/www.crickam.com
openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out /opt/crickam/keystore.p12 -name crickam -CAfile chain.pem -caname root -password file:/opt/crickam/keystore.pw
chmod a+r /opt/crickam/keystore.p12
)
Where:
-out
is the path to the keystore you want to create-name
can be whatever you want (I’m not sure what it’s used for)-password file:path
is the path to a file that just contains the keystore password (my java code reads this same file to access the keystore password)- All other options should be left as is, as they point to files that certbot created
Using the generated key
It’s quite straightforward - just create an SslContextFactory
and point to the generated keystore.p12
file:
Server jettyServer = new Server();
HttpConfiguration config = new HttpConfiguration();
config.addCustomizer(new SecureRequestCustomizer());
config.addCustomizer(new ForwardedRequestCustomizer());
// Create the HTTP connection
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config);
ServerConnector httpConnector = new ServerConnector(jettyServer, httpConnectionFactory);
httpConnector.setPort(8080); // IP tables redirect 80 -> 8080
jettyServer.addConnector(httpConnector);
// Create the HTTPS end point
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStoreType("PKCS12");
sslContextFactory.setKeyStorePath("/opt/crickam/keystore.p12");
String keyStorePassword = FileUtils.readFileToString(new File("/opt/crickam/keystore.pw"), "UTF-8").trim();
sslContextFactory.setKeyStorePassword(keyStorePassword);
sslContextFactory.setKeyManagerPassword(keyStorePassword);
ServerConnector httpsConnector = new ServerConnector(jettyServer, sslContextFactory, new HttpConnectionFactory(config));
httpsConnector.setPort(8443); // IP tables redirect 8443 -> 443
jettyServer.addConnector(httpsConnector);
With this, you should be able to use your certificate, however it will expire in 90 days. So time to automate some stuff.
Automated renewal and hot reloading of SSL certs
After the first manual creation of the cert, certbot will remember the cert and so renewel of the cert is extremely easy:
just run sudo ./certbot-auto renew
This will check the validity of the cert and do nothing if it is okay, or renew it if it has been revoked or near expiry. If it is renewed, we want to convert the cert to a java keystore. With certbot, you can specify a hook that only runs if it actually renews the cert, so we can tell it to run the script specified above if it needs to:
sudo ./certbot-auto renew --post-hook "./convertkeystore.sh"
So that’s nice, but Jetty will still be using the old certificate for requests. Fortunately, on newer versions of Jetty
you can tell it to hot reload your certs by calling the following on the SslContextFactory
you create:
sslContextFactory.reload(scf -> log.info("Reloaded SSL cert"));
Now we just need to run that when the keystore changes. For this,
I created a FileWatcher class, and then simply
registered the keystore path with that and ran the reload
method in the callback:
Path keystorePath = Paths.get(URI.create(sslContextFactory.getKeyStorePath()));
FileWatcher.onFileChange(keystorePath, () ->
sslContextFactory.reload(scf -> log.info("Reloaded SSL cert")));
Putting it all together and scheduling the updates
The following script puts together the certbot check and the conversation of the keystore, and lives in /home/ec2-user/renewcerts.sh
#!/bin/sh
set -e
set -x
/home/ec2-user/certbot-auto renew --post-hook "/home/ec2-user/convertkeystore.sh"
It needs to run as root, so sudo crontab -e
to schedule it:
39 14 * * * /home/ec2-user/renewcerts.sh > /opt/crickam/logs/cronoutput.log 2>&1
That runs at 2:39pm every day - doesn’t matter when exactly, and it could run hourly, daily, or weekly, as normally it
will do nothing. (Tip - temporarily add --force-renewal
to the renew command to test a renewal.)
Done
That’s it. You now have free SSL certs that will auto renew themselves.