HTTPS for Tomcat 7 with Let’s Encrypt

In my last blog post I described the benefits of HTTPS and how to set up your Synology DiskStation to use HTTPS with a trusted certificate from Let’s Encrypt. However I am also running Tomcat 7 on my DiskStation, which is also accessible from the internet. Needless to say, I want to secure access to Tomcat, too. If you run Tomcat yourself and want to set it up for HTTPS, this is for you.

Update for DSM 6 Users!

In case you own a DSM 6 powered Diskstation, you do not want to follow the instructions described in this post.
I found a much easier and automatable approach by using DSM 6 integrated Let’s Encrypt support and the new reverse proxy feature, which I described in a recent post over here.


Prerequisites

In case you want to follow along, make sure you have Java and Tomcat already running. I won’t cover how to set up Tomcat in this post. However, there is no need to worry! Especially if you are a DiskStation user, because both Java and Tomcat 7 are available in Synology Package Center for your one-click-install pleasure. Check this guide in case you need more advice on how to get Tomcat 7 running.
You will also need openSSL, but I found it was already installed on my DiskStation.
Furthermore I assume you already have a certificate from Let’s Encrypt. If you do not, check out my last post to learn how to get that certificate and set up your DiskStation to use it for HTTPS.
Once you got the certificate issued with the official Let’s Encrypt client, you will find that the client created a directory for you: /etc/letsencrypt. In this directory you will find the path live/yourDomain which contains symbolic links to the latest version of their corresponding file in /etc/letsencrypt/archive/yourDomain. Follow the links fullchain.pem and privkey.pem to grab their corresponding files.
In a terminal you can do this like so:

$ cp -L /etc/letsencrypt/live/yourDomain/fullchain.pem /remember/this/path/
$ cp -L /etc/letsencrypt/live/yourDomain/privkey.pem /remember/this/path/

Now that you have these two files, we are ready to go!

What is acutally required for HTTPS?

To enable Tomcat to serve requests through HTTPS we need to provide it with a certificate and the entire chain required to validate that certificate. This way when accessing Tomcat with a browser, the browser will be able to validate the provided certificate. After making sure the certificate is valid, the browser checks if it is issued to the server the client actually called and if this is the case, the connection is trusted.
But for the connection to also be private, Tomcat needs the private key corresponding to the certificate to decrypt the client generated random key. This client generated random key is encrypted with the server’s public key (which is part of the certificate) by the client, so it can be safely transmitted to your server. Your server decrypts the symmetric key with the private key and now both parties have the symmetric key that is actually used to encrypt the communication between them. If you want to know more about HTTPS, check out Robert Heaton’s post.
Anyway, now you know why we grabbed fullchain.pem and privkey.pem earlier. However we can’t tell Tomcat to just use the PEM files we got from the Let’s Encrypt client. So what we will do is to put the full certificate chain and the private key all together inside one Java Key Store (JKS).

Create a keystore for Tomcat

Basically there are only two steps required to get our fullchain.pem and privkey.pem inside a JKS.
First we bundle both our fullchain and the private key in a PKCS12 keystore. We do this, because apparently Java’s keytool (which we use to create our JKS) is not able to import pre-existing keys and certificates into a JKS, as described here.

So open up your terminal and do something like this:

$ openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out fullchain_and_key.p12 -name tomcat

You will be asked to provide a password (called yourPKCS12pass in the following). Do not forget it. You will need it for our second step.

Now that we have our PKCS12 keystore, we can use Java’s keytool to generate a JKS from our PKCS12 file like so:

$ keytool -importkeystore -deststorepass yourJKSpass -destkeypass yourKeyPass -destkeystore MyDSKeyStore.jks -srckeystore fullchain_and_key.p12 -srcstoretype PKCS12 -srcstorepass yourPKCS12pass -alias tomcat

This will give you a file called MyDSKeyStore.jks. Now you are ready to configure Tomcat for HTTPS.

Configure Tomcat for HTTPS

Now that we have the required JKS, we configure Tomcat’s SSL Connector to use the generated JKS the way it is documented here and here. For this we need to edit Tomcat’s server.xml file found in $CATALINA_BASE/conf/. For DiskStations users this should look something like this:
/volume1/@appstore/Tomcat7/src/conf/
Open that file and find the block starting with <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol". Uncomment that block and add your details to the connector. The result should look something like this:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol" URIEncoding="UTF-8" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreFile="/volume1/NetBackup/myds_certs/MyDSKeyStore.jks" keystorePass="yourJKSpass" keyAlias="tomcat" keyPass="yourKeyPass"/>

Note the sections in bold. Make sure you point to your JKS file and that the other values correspond to the values you used when creating the JKS file.

The careful reader might have noticed that I added URIEncoding="UTF-8" to the connector. This is for GitBlit and Jenkins. Without that setting, if you use non-ASCII characters as a git repository name or as a Jenkins job name, it will cause problems. See Containers and Tomcat i18n for Jenkins and GitBlit FAQ for more details.

Anyway, we are all set!
Restart Tomcat (I use DSM’s Package Center for that) and access Tomcat through HTTPS, which would look something like this:
https://yourDomain:8443

You should see the Tomcat landing page and you can test that everything is setup correctly by providing your URL to Symantec CryptoReport (SSL Labs does not support ports other than 443).

Note of Thanks

I did not come up with this solution on my own. Actually it was quite a ride until I figured out how this might work. Gladly the Let’s Encrypt Community is a wonderful place to ask questions, find people trying to overcome the same obstacles and to help each other. So I want to thank the Let’s Encrypt Community for helping me out to get my Tomcat server up and running with HTTPS!

This opens up quite a few interesting usecases. One of them: Running GitBlit in Tomcat on my DiskStation and thus having a HTTPS accessible private Git server.
In case you are interested in that, be sure to check back soon. I will definitely write a blog post about how to turn your DiskStation into a private Git server for you and your team in the near future.

22 thoughts on “HTTPS for Tomcat 7 with Let’s Encrypt”

  1. Looks like a typo in the invocation keytool – “youKeyPass” appears as though it’s intended to be “yourKeyPass”.

    Thanks for the article, very helpful!

    1. Hey there!
      You are perfectly right, good catch! I just updated the post accordingly.
      Thanks for pointing it out!

      I’m glad my article was helpful to you.

    1. Hi. I guess one could easily add the openssl command to a script and run it as cron job so that the Tomcat keystore always contains your latest certificate. However in my experience a change in the keystore was not noticed by Tomcat without restarting it.
      This is one of the reasons that made me turn away from configuring SSL in Tomcat itself and use the reverse proxy approach I described in my blog post here: https://melo.myds.me/wordpress/reverse-proxy-tomcat-for-https
      This way I can fully leverage the benefits of Let’s Encrypts automated certificate renewal.

  2. TomCat 8.5.3 can use LetsEncrypt .pem files directly with it’s new openssl support. Install openssl, install tomcat 8.5.3+

    <Certificate certificateFile="/etc/letsencrypt/live//cert.pem”
    certificateChainFile=”/etc/letsencrypt/live//chain.pem”
    certificateKeyFile=”/etc/letsencrypt/live//privkey.pem”
    certificateKeyAlias=””
    certificateKeyPassword=””
    type=”RSA” />

    No need to convert them to pkcs12 or jks.

    Enjoy,

    1. That is interesting, thanks for sharing! I wonder whether Synology will update their Tomcat package for DSM any time soon or if I’m able to install Tomcat 8 on my DiskStation myself.

    2. I tried to use following configuration,

      But I got the exceptions when startup tomcat
      ===============================================

      06-Jul-2016 17:18:35.177 SEVERE [main] org.apache.catalina.core.StandardService.initInternal Failed to initialize connector [Connector[HTTP/1.1-8443]]
      org.apache.catalina.LifecycleException: Failed to initialize component [Connector[HTTP/1.1-8443]]
      at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:111)
      at org.apache.catalina.core.StandardService.initInternal(StandardService.java:549)
      at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
      at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:873)
      at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
      at org.apache.catalina.startup.Catalina.load(Catalina.java:606)
      at org.apache.catalina.startup.Catalina.load(Catalina.java:629)
      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:498)
      at org.apache.catalina.startup.Bootstrap.load(Bootstrap.java:311)
      at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:494)
      Caused by: org.apache.catalina.LifecycleException: Protocol handler initialization failed
      at org.apache.catalina.connector.Connector.initInternal(Connector.java:1012)
      at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
      … 12 more
      Caused by: java.security.KeyStoreException: Cannot store non-PrivateKeys
      at sun.security.provider.JavaKeyStore.engineSetKeyEntry(JavaKeyStore.java:258)
      at sun.security.provider.JavaKeyStore$JKS.engineSetKeyEntry(JavaKeyStore.java:56)
      at sun.security.provider.KeyStoreDelegator.engineSetKeyEntry(KeyStoreDelegator.java:117)
      at sun.security.provider.JavaKeyStore$DualFormatJKS.engineSetKeyEntry(JavaKeyStore.java:70)
      at java.security.KeyStore.setKeyEntry(KeyStore.java:1140)
      at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:302)
      at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:91)
      at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:245)
      at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:839)
      at org.apache.tomcat.util.net.AbstractJsseEndpoint.init(AbstractJsseEndpoint.java:196)
      at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:558)
      at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:65)
      at org.apache.catalina.connector.Connector.initInternal(Connector.java:1010)
      … 13 more

      ===============================================

      Is there anything wrong with my configration?

      1. I am not sure if I don’t understand or you simply forgot to attach your config in the comment.

        Anyway, based on the “KeyStoreException: Cannot store non-PrivateKeys” message I assume that something must be wrong with your config where you define the pem files to use.

        Have you checked out the thread on the LE community forum regarding the topic? You can find a little more detail on how the config should look over there: https://community.letsencrypt.org/t/how-to-use-the-certificate-for-tomcat/3677/39?u=melo

      2. I have the same problem with tomcat 9.0.0.M8.

        29-Jul-2016 09:58:52.366 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler [“http-nio-8080”]
        29-Jul-2016 09:58:52.384 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
        29-Jul-2016 09:58:52.387 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler [“https-jsse-nio-8443”]
        29-Jul-2016 09:58:52.742 SEVERE [main] org.apache.coyote.AbstractProtocol.init Failed to initialize end point associated with ProtocolHandler [“https-jsse-nio-8443”]
        java.security.KeyStoreException: Cannot store non-PrivateKeys
        at sun.security.provider.JavaKeyStore.engineSetKeyEntry(JavaKeyStore.java:258)
        at sun.security.provider.JavaKeyStore$JKS.engineSetKeyEntry(JavaKeyStore.java:56)
        at sun.security.provider.KeyStoreDelegator.engineSetKeyEntry(KeyStoreDelegator.java:117)
        at sun.security.provider.JavaKeyStore$DualFormatJKS.engineSetKeyEntry(JavaKeyStore.java:70)
        at java.security.KeyStore.setKeyEntry(KeyStore.java:1140)
        at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:302)
        at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:90)
        at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:245)
        at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:839)
        at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:558)
        at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:65)
        at org.apache.catalina.connector.Connector.initInternal(Connector.java:1010)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        at org.apache.catalina.core.StandardService.initInternal(StandardService.java:549)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:873)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:606)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:629)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.apache.catalina.startup.Bootstrap.load(Bootstrap.java:311)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:494)

        29-Jul-2016 09:58:52.744 SEVERE [main] org.apache.catalina.core.StandardService.initInternal Failed to initialize connector [Connector[HTTP/1.1-8443]]
        org.apache.catalina.LifecycleException: Failed to initialize component [Connector[HTTP/1.1-8443]]
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:112)
        at org.apache.catalina.core.StandardService.initInternal(StandardService.java:549)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:873)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:606)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:629)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.apache.catalina.startup.Bootstrap.load(Bootstrap.java:311)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:494)
        Caused by: org.apache.catalina.LifecycleException: Protocol handler initialization failed
        at org.apache.catalina.connector.Connector.initInternal(Connector.java:1013)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
        … 12 more
        Caused by: java.security.KeyStoreException: Cannot store non-PrivateKeys
        at sun.security.provider.JavaKeyStore.engineSetKeyEntry(JavaKeyStore.java:258)
        at sun.security.provider.JavaKeyStore$JKS.engineSetKeyEntry(JavaKeyStore.java:56)
        at sun.security.provider.KeyStoreDelegator.engineSetKeyEntry(KeyStoreDelegator.java:117)
        at sun.security.provider.JavaKeyStore$DualFormatJKS.engineSetKeyEntry(JavaKeyStore.java:70)
        at java.security.KeyStore.setKeyEntry(KeyStore.java:1140)
        at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:302)
        at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:90)
        at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:245)
        at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:839)
        at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:558)
        at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:65)
        at org.apache.catalina.connector.Connector.initInternal(Connector.java:1010)
        … 13 more

        Relevant part from server.xml:

    1. Relevant part from server.xml (can this comment field not handle tags?):

      _Service name=”Catalina”>

      _!– Connector port=”8080″ protocol=”HTTP/1.1″… –>
      _Connector port=”8080″ protocol=”org.apache.coyote.http11.Http11NioProtocol”
      connectionTimeout=”20000″ redirectPort=”8443″ />

      _Connector port=”8443″ protocol=”org.apache.coyote.http11.Http11NioProtocol”
      maxThreads=”150″ SSLEnabled=”true” scheme=”https” secure=”true”
      defaultSSLHostConfigName=”abcde.xyz-informatik.de”>
      _SSLHostConfig hostName=”abcde.xyz-informatik.de”>
      _!– Certificate certificateKeystoreFile=”conf/localhost-rsa.jks”
      type=”RSA” /–>
      _Certificate certificateFile=”conf/ssl/abcde.xyz-informatik.de/domain.crt”
      certificateChainFile=”conf/ssl/abcde.xyz-informatik.de/chain.crt”
      certificateKeyFile=”conf/ssl/abcde.xyz-informatik.de/domain.key”
      certificateKeyPassword=””
      certificateKeyAlias=””
      type=”RSA” />
      _/SSLHostConfig>

      _/Connector>

      1. … Maybe reproducing this can be done with uninstalling openssl… Just a guess…
        I just tried with tomcat 9.0.0.M9 and it gives completely different exceptions, but does not work neither.
        I want to rely purely on java, to not be affected by openssl security holes. So I created a virtual server with no openssl installed in it.

  3. Is it possible to setup an auto renewal of let’s encrypt certificate. like via cron ? If that’s possible, could you please provide an example?

    1. It is absolutely possible and also documented in the official certbot doc (the Let’s Encrypt client, after it has been renamed). It can take as little as setting up a cron or a systemd job calling the client with the renew parameter. Check out the doc here: https://certbot.eff.org/docs/using.html#renewing-certificates

      If you are looking into Tomcat specific steps, make sure to checkout the related topic in the LE forum: https://community.letsencrypt.org/t/how-to-use-the-certificate-for-tomcat/3677

      I myself can’t provide you with an example, because I decided to front Tomcat with a webserver acting as a reverse proxy for Tomcat and thus the webserver is taking care of https. To me this is much easier and convenient.

      However, one guy in that discussion linked to his blog post where he describes a script he put together to automate the process of renewing the certs, getting the LE client’s output and provide it to Tomcat. Check it out here:
      http://blog.ivantichy.cz/blogpost/view/74

      Hope it helps.

  4. Everything worked very well, thank you:) I did one adjustment, I omitted the “destkeypass” argument. I haven’t used it before, so I decided to try without this time as well (simply because I’m confused of all the various passwords). It seemed to work just as well without.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.