How to secure Clojure network traffic with TLS, certificate validation, and safer client and server defaults.
TLS is the normal way to protect Clojure network traffic, but “we turned on HTTPS” is not the same thing as a good transport-security design. The important questions are where TLS terminates, how certificates are issued and rotated, whether clients validate the peer correctly, and whether sensitive internal traffic also needs its own authenticated channel.
On the JVM, Clojure benefits from JSSE, the JDK’s TLS implementation. That gives you mature primitives, but it does not remove the need to make good operational choices.
TLS gives you three important properties when configured correctly:
It does not solve authorization mistakes, excessive data exposure, weak application logging, or unsafe message semantics. Treat it as one layer in a larger security model.
There are usually three sensible patterns:
Public TLS termination at an ingress, load balancer, or reverse proxy
Good default for browser-facing traffic.
End-to-end TLS from client to application service
Useful when you want the application process itself to own certificates and handshake policy.
Internal service-to-service TLS or mTLS
Useful when the network is shared, regulated, zero-trust, or multi-tenant enough that internal plaintext would be a real risk.
The diagram below shows a common split: edge TLS for public traffic, then either trusted internal traffic or another TLS hop between services depending on the environment.
flowchart LR
A["Browser or API client"] -->|TLS| B["Ingress or load balancer"]
B --> C{"Need service-to-service encryption too?"}
C -- No --> D["Clojure service"]
C -- Yes --> E["Clojure service over TLS or mTLS"]
D --> F["Database or downstream service"]
E --> G["Database or downstream service over TLS where required"]
If the service itself owns the public listener, keep the configuration small and predictable:
1(ns myapp.secure-server
2 (:require [org.httpkit.server :as http]))
3
4(defn handler [_request]
5 {:status 200
6 :headers {"content-type" "text/plain; charset=utf-8"}
7 :body "ok"})
8
9(defonce server (atom nil))
10
11(defn start! []
12 (reset! server
13 (http/run-server
14 handler
15 {:port 8443
16 :ssl? true
17 :keystore "/etc/myapp/server-keystore.jks"
18 :key-password (System/getenv "KEYSTORE_PASSWORD")})))
That example is intentionally boring. Production TLS tends to be safer when it is operationally boring:
The most dangerous anti-pattern on the client side is disabling validation to “make the connection work.” If a service uses an internal CA, fix the trust chain. Do not disable hostname checks or accept every certificate.
A modern JDK client already rides on JSSE:
1(ns myapp.secure-client
2 (:import [java.net URI]
3 [java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]))
4
5(def client
6 (HttpClient/newHttpClient))
7
8(defn fetch-health [url]
9 (let [request (-> (HttpRequest/newBuilder (URI/create url))
10 (.header "accept" "application/json")
11 (.GET)
12 (.build))
13 response (.send client request HttpResponse$BodyHandlers/ofString)]
14 {:status (.statusCode response)
15 :body (.body response)}))
The important part is not the syntax. The important part is that certificate and hostname validation stay enabled. If you need a custom trust anchor, install it properly through the JVM trust configuration rather than short-circuiting trust checks in application code.
Reasonable defaults:
When someone says “this service uses TLS,” ask: