Network Security and TLS

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.

What TLS Gives You

TLS gives you three important properties when configured correctly:

  • confidentiality for traffic on the wire
  • server authentication, and optionally client authentication
  • integrity protection so in-transit tampering is detectable

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.

Choose the Right TLS Boundary

There are usually three sensible patterns:

  1. Public TLS termination at an ingress, load balancer, or reverse proxy
    Good default for browser-facing traffic.

  2. End-to-end TLS from client to application service
    Useful when you want the application process itself to own certificates and handshake policy.

  3. 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"]

Server-Side TLS in a Clojure Service

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:

  • keep keys out of the repo
  • inject passwords or secrets through the environment or secret manager
  • rotate certificates with a real renewal process
  • let a stable ingress layer handle HSTS, redirects, and certificate automation when that fits the platform better than app-owned certificates

Client-Side Validation Matters More Than “Use HTTPS”

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.

Certificate and Truststore Discipline

Reasonable defaults:

  • use publicly trusted certificates for public services
  • use an internal CA or platform-managed identity system for internal services
  • automate renewal and deployment
  • monitor expiration windows
  • keep distinct truststores for environments when that reduces blast radius
  • use mTLS only when peer identity really matters, not just because it sounds stronger

Operational Mistakes That Actually Cause Trouble

  • disabling hostname verification for convenience
  • trusting self-signed certificates in production without a real distribution model
  • hardcoding keystore passwords into config files
  • forgetting that TLS termination at the edge means internal hops may still be plaintext
  • assuming encrypted traffic is automatically authorized traffic
  • rotating certificates manually with no expiry alerting

What To Review in a Design Discussion

When someone says “this service uses TLS,” ask:

  • where does TLS terminate?
  • what issues and rotates certificates?
  • how do clients learn to trust the server certificate chain?
  • do internal calls also need TLS or mTLS?
  • what logging exists for handshake or certificate failures?
  • what breaks first when a certificate expires?

Key Takeaways

  • TLS protects confidentiality, integrity, and peer identity on the wire, but it is only one layer of application security.
  • The important deployment choice is where TLS terminates and whether internal hops also need protection.
  • Client-side certificate and hostname validation must stay on.
  • Certificate renewal, trust distribution, and expiry monitoring are operational requirements, not cleanup work for later.

References and Further Reading

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026