Securing my Clojure photo gallery: Let’s Encrypt certs on NFSN

I have a photo gallery site written in Clojure, hosted at NearlyFreeSpeech.NET. I just spent less than an hour learning about ACME and successfully hooked up my app with TLS! Here's what I did.

In my case, the Clojure application is packaged up as a .jar file, and is run as a daemon behind NFSN's reverse proxy, which is capable of terminating TLS. They also provide a script,, that can do the ACME challenge-response. However, it does this by dropping the response files in the Apache-served public directory, which isn't exposed in my configuration.

I chose to modify my application to proxy the acme-challenge dir. It has risks; if you screw up filesystem proxying, you risk path traversal attacks that allow an attacker to read anything on your filesystem! Anyway, it takes very little code, and it worked on the first try, so I would encourage people to use this method.

Below is the meat of the commit that implements this. It's not very much!

(ns org.timmc.pellucida.res.acme
  "Pass through ACME requests to filesystem. (ACME: Automated
Certificate Management Environment)"
   [compojure.core :refer [defroutes GET]]
   [org.timmc.pellucida.settings :as cnf]))

(defn is-token-shaped?
  "Does this string look like an ACME http-01 token? In particular,
does it *not* look like a path traversal attack?"
  [^String s]
  (boolean (re-matches #"[a-zA-Z0-9\-_=]+" s)))

(defn token-response
  "Answer with the contents of the ACME challenge file."
  ;; The ACME http-01 token value is used as the filename as well
  ;; as the value of a field within the JSON file.
  (when (is-token-shaped? magic-token)
    (let [resp-path (str (:acme-challenge-dir @cnf/config) "/" magic-token)]
      {:status 200
       :headers {"Content-Type" "text/plain"} ;; Spec: This or none
       :body ( resp-path)})))

(defroutes acme-routes
  (GET ["/.well-known/acme-challenge/:magic-token"] [magic-token :as r]
       (when (:acme-challenge-dir @cnf/config)
         (token-response magic-token))))

Incidentally, there were a few other options I thought of, but did not pursue or fully evaluate:

  • Hook: uses dehydrated to implement ACME, and you can hook into it for various events, including challenge generation. I could use that to notify the app to do something.
  • Proxy: I already configure the NFSN site to proxy the root path to my app's port 8080. Could I add another proxy that just mirrors /home/public/.well-known/acme-challenge? Not sure. From the forums, it sounds like if I change the site type to "Apache2.4 generic" instead of "Custom", I could possibly do this. Ah well, the code is already written!

No comments yet. [feed]

Start the discussion