SSL in Jetty (Compojure)

Clojure.jpgI've been working on a project at The Shop and it's a simple-ish clojure back-end with a Javascript/jQuery/Bootstrap front-end, and it's not too bad. Getting there. But recently, it was time to add in the user authentication code, and that meant passwords. That meant security, and simple HTTP requests weren't going to do. We had to have security. HTTPS. So I dove down the rabbit hole of getting Jetty 7 to run as SSL only within a compojure/ring application.

What I found was that there really wasn't a wealth of information on the subject. Most folks expected a front-end service like nginx to handle the SSL, and then forward on to the clojure app requests on simple port 8080. While this is possible, it also means that port 8080 is there listening, and it's possible to have an intercept on that port and all of a sudden we're not so secure.

Nope. It needed to be Jetty on SSL on port 8443. Period.

Get the Certificate File

No doubt, getting the initial certificate file is the hardest part for most folks, as it's all about money. But I suppose self-signed certificates are useful - to a point, but being in a commercial organization, it's nice to see that there's a real certificate file, and all I need to do is to convert it to a Java Key Store file. In this case, it started as a pox file - a PKCS formatted certificate. Then:

  $ keytool -importkeystore -srckeystore my_cert.pfx -srcstoretype pkcs12 \
      -destkeystore my_cert.jks -deststoretype jks -deststorepass MY_PASSWORD

where it's clear what the file names are, and passwords. This gets you a my_cert.jks file that's what you are going to need. The MY_PASSWORD is also important as it will have to be in the code as well to read the file.

Put the KeyStore File in resources/certs

Like everything additional in a Meiningen project, this needs to go into the resources directory, and for convention, I'm throwing it into resources/certs as it's not the only thing in my resources directory for this project, and a little separation is a good thing.

Convert the Code to Dual-Mode

Working with the following dependencies in the project.clj file:

    ;; command line option processing
    [org.clojure/tools.cli "0.2.2"]
    ;; web server
    [compojure "1.3.4"]
    [ring/ring-core "1.3.2"]
    [ring/ring-jetty-adapter "1.3.2"]
    [ring.middleware.jsonp "0.1.6"]
    [ring/ring-defaults "0.1.5"]

and assuming you have a it running on port 8080, it might look a little like this:

(ns myapp.main
  (:require [clojure.java.io :refer [resource input-stream]]
            [clojure.tools.cli :refer [cli]]
            [clojure.tools.logging :refer [error info infof]]
            [myapp.server :refer [app]]
            [ring.adapter.jetty :as jt])
  (:import java.security.KeyStore)
  (:gen-class))
 
  (defn handle-args
    "Function to parse the arguments to the main entry point of this
    project and do what it's asking. By the time we return, it's all
    done and over."
    [args]
    (let [[params [action]] (cli args
               ["-p" "--port" "Listen on this port"
                 :default 8080 :parse-fn #(Integer. %)]
               ["-v" "--verbose" :flag true])]
      (cond
        (= "web" action)
          (jt/run-jetty app { :port (:port params) }))
        :else
          (do
            (info "Welcome to My App!")
            (println "Welcome to My App!")))))
 
  (defn -main
    "Function to kick off everything and clean up afterwards"
    [& args]
    (with-error-handling (handle-args args)))

this will assume port 8080, and yet allow the command-line args to override this. In order to add the SSL component to this, we simply have to add a few options to Jetty:

  (defn handle-args
    "Function to parse the arguments to the main entry point of this
    project and do what it's asking. By the time we return, it's all
    done and over."
    [args]
    (let [[params [action]] (cli args
               ["-p" "--port" "Listen on this port"
                 :default 8080 :parse-fn #(Integer. %)]
               ["-s" "--ssl-port" "Listen on this port"
                 :default 8443 :parse-fn #(Integer. %)]
               ["-v" "--verbose" :flag true])]
      (cond
        (= "web" action)
          (jt/run-jetty app { :port (:port params)
                              :ssl? true
                              :ssl-port (:ssl-port params)
                              :keystore "resources/certs/my_cert.jks"
                              :key-password "MY_PASSWORD" }))
        :else
          (do
            (info "Welcome to My App!")
            (println "Welcome to My App!")))))

Start this guy now, and it'll answer on port 8080 for normal HTTP traffic, and port 8443 for HTTPS traffic. So far, so good. But there's a catch here, and we'll get to it soon.

Make it SSL Only

The real request was to make is SSL-only, so we to remove the port 8080 traffic, but we can't just tell Jetty not to run that one - we have to actively remove it. Thankfully, composure allows us this flexibility. If we make a function:

  (defn remove-non-ssl-connectors
    "Function to configure the Jetty instance to remove all non-SSL
    connectors so that there's **no way** to get into this service BUT
    by SSL (https)."
    [server]
    (doseq [c (.getConnectors server)]
      (when-not (or (nil? c)
                    (instance? org.eclipse.jetty.server.ssl.
                                    SslSelectChannelConnector c))
        (.removeConnector server c)))
    server)

then we can add that as an option to Jetty to tell it to run on it's configuration:

  (defn handle-args
    "Function to parse the arguments to the main entry point of this
    project and do what it's asking. By the time we return, it's all
    done and over."
    [args]
    (let [[params [action]] (cli args
               ["-p" "--port" "Listen on this port"
                 :default 8080 :parse-fn #(Integer. %)]
               ["-s" "--ssl-port" "Listen on this port"
                 :default 8443 :parse-fn #(Integer. %)]
               ["-v" "--verbose" :flag true])]
      (cond
        (= "web" action)
          (jt/run-jetty app { :configurator remove-non-ssl-connectors
                              :port (:port params)
                              :ssl? true
                              :ssl-port (:ssl-port params)
                              :keystore "resources/certs/my_cert.jks"
                              :key-password "MY_PASSWORD" }))
        :else
          (do
            (info "Welcome to My App!")
            (println "Welcome to My App!")))))

and now if you restart the app, it won't answer on port 8080, but it will still answer on port 8443 with HTTPS. Getting very close.

Making it Deployable in an Uberjar

The final wrinkle is that the :keystone option is a location in the filesystem of the key store file. That's not good for deployments because it means that while the keystore file will be packaged up in the uberjar, it's not going to be referenced that way - and it will have to also exist in the filesystem in the same relative location.

This stinks.

So I did some more digging, and ring-jetty had what I needed - the ability to pass it a java.security.KeyStore instance. Now I needed to read the keystone file from the jar, into an instance of that object, and pass it in.

Start with:

  (defn load-keystore
    "Function to load the SSL KeyStore from the resources so that it's ready
    to be used to run the SSL connections for Jetty. This is a preferred method
    to having just a path for locating the certificate, as this allows the cert
    to be _included_ in the uberjar itself."
    [loc pw]
    (if (and (string? loc) (string? pw))
      (doto (KeyStore/getInstance (KeyStore/getDefaultType))
        (.load (input-stream (resource loc)) (char-array pw)))))

which will load a keystone file from the uberjar - placed in the resources directory. This was a major find for me, and made it all possible. That it only takes a few lines of clojure and java is just amazing.

Now we can put it all together:

  (defn handle-args
    "Function to parse the arguments to the main entry point of this
    project and do what it's asking. By the time we return, it's all
    done and over."
    [args]
    (let [[params [action]] (cli args
               ["-p" "--port" "Listen on this port"
                 :default 8080 :parse-fn #(Integer. %)]
               ["-s" "--ssl-port" "Listen on this port"
                 :default 8443 :parse-fn #(Integer. %)]
               ["-v" "--verbose" :flag true])]
      (cond
        (= "web" action)
          (let [loc "certs/my_cert.jks"   ;; SSL keystore location
                pw "MY_PASSWORD"]         ;; ...and password
            (jt/run-jetty app { :configurator remove-non-ssl-connectors
                                :port (:port params)
                                :ssl? true
                                :ssl-port (:ssl-port params)
                                :keystore (load-keystore loc pw)
                                :key-password pw }))
        :else
          (do
            (info "Welcome to My App!")
            (println "Welcome to My App!")))))