TLS (Transport Layer Security) support
| TLS support was introduced in nREPL 1.1. |
Historically, the standard way to securely access a remote nREPL server has been
to use ssh with port forwarding, and letting the nREPL server only bind to
127.0.0.1. This will secure and encrypt the communication.
Sometimes however ssh may not be available, and thus you need to bind to
0.0.0.0 (or some specific public address) to make the nREPL server available.
By using TLS (Transport Layer Security) the successor to SSL (Secure Sockets Layer), we can open a secure connection directly on the nREPL server. The server will require each client to authenticate itself.
The following sections will show how to generate the certificates and keys required for running a TLS nREPL server and client.
TLS applies only to TCP sockets - it cannot be combined with UNIX domain
sockets (:socket / --socket). nREPL enables TLSv1.3 and nothing else, so you
need Java 11+, or Java 8u261+ where TLSv1.3 was backported.
|
Generating certificates and keys
First things first - we need to generate some certificates and keys. One simple option is to use the Clojure library locksmith:
$ clojure -Sdeps '{:aliases {:write-cert {:deps {com.github.ivarref/locksmith {:mvn/version "0.1.9"}} :exec-fn com.github.ivarref.locksmith/write-certs!}}}' -T:write-cert :duration-days 365
Wrote server.keys
Wrote client.keys
This will generate two 256-bit ECDSA keys. Both files contain the root certificate. Each file also contains the server/client’s certificate and private key. The root certificate key is thrown away.
These two files are all you need to run and connect to a TLS nREPL server.
It’s also possible to generate certificates and keys using OpenSSL:
$ openssl version
OpenSSL 1.1.1q 5 Jul 2022
$ cat ./generate.sh
#!/usr/bin/env bash
set -ex
DAYS="365"
# openssl ecparam -list_curves
CURVE="prime256v1"
# Generate root key and certificate
openssl ecparam -name "$CURVE" -genkey -noout -out ca-key.pem
openssl req -new -x509 -nodes -days "$DAYS" \
-subj "/C=NO/ST=Hordaland/L=Bergen/O=Sikt/CN=root" \
-key ca-key.pem -out ca-cert.pem
# Generate server key and certificate request
openssl req -newkey ec -pkeyopt ec_paramgen_curve:"$CURVE" -nodes -days "$DAYS" \
-keyout server-key.pem \
-subj "/C=NO/ST=Hordaland/L=Bergen/O=Sikt/CN=server" -out server-req.pem
# Generate server certificate
openssl x509 -req -days "$DAYS" -set_serial 01 \
-in server-req.pem \
-out server-cert.pem \
-CA ca-cert.pem \
-CAkey ca-key.pem
# Client key and cert request
openssl req -newkey ec -pkeyopt ec_paramgen_curve:"$CURVE" -nodes -days "$DAYS" \
-keyout client-key.pem \
-subj "/C=NO/ST=Hordaland/L=Bergen/O=Sikt/CN=client" -out client-req.pem
# Client cert
openssl x509 -req -days "$DAYS" -set_serial 01 \
-in client-req.pem \
-out client-cert.pem \
-CA ca-cert.pem \
-CAkey ca-key.pem
# Write files in format of nREPL:
rm -fv ./client.keys ./server.keys
cat ca-cert.pem server-cert.pem server-key.pem > server.keys
cat ca-cert.pem client-cert.pem client-key.pem > client.keys
chmod 0400 server.keys client.keys
# Clean up:
rm ca-key.pem ca-cert.pem \
server-key.pem server-req.pem server-cert.pem \
client-key.pem client-req.pem client-cert.pem
This will produce roughly the equivalent key files as the clj command above.
The openssl curve parameters do not exactly match the ones used by the clj command. That is why the private key generated by these openssl seems slightly longer.
|
Security model
Before you expose an nREPL server this way, it’s worth knowing exactly what this setup verifies - and what it leaves to you.
What you get:
-
All traffic is encrypted. Only TLSv1.3 is enabled.
-
Mutual authentication against a single trust root. Every keys file pairs its own certificate with the private certificate authority (CA) that signed it. The server only accepts clients presenting a certificate signed by that CA (client authentication is mandatory), and the client only accepts servers whose certificate is signed by the same CA. As an extra guard, the server rejects clients that present the server’s own certificate.
What you don’t get:
-
Hostname verification. The client does not check that the server’s certificate matches the host it connected to. (The certificates generated by the recipes above contain no hostnames anyway.)
-
Role separation. The generated certificates carry no "extended key usage" attributes, so client and server certificates differ only in which file they ended up in. Anyone holding
client.keyscan also run a server that other clients of the same CA will trust - or, with a suitable network position, man-in-the-middle their connections. -
Revocation. There are no certificate revocation checks. To withdraw a client’s access you have to generate a new CA and new certificates, and redistribute them to everyone.
In practice this means that everyone holding a keys file minted from the same
CA must fully trust each other. That’s a good fit for the intended use case -
one server and a small set of mutually trusting clients. Use a dedicated CA
per server (the generation recipes above do this), don’t reuse it across
environments, and treat the keys files as secrets: they contain private keys,
so restrict their permissions (e.g. chmod 0400) and distribute them only
over secure channels.
If you need per-client revocation, role separation or hostname verification, you’ve outgrown this feature - put nREPL behind infrastructure built for that (an SSH tunnel, a VPN, or a TLS-terminating reverse proxy) instead.
Starting the nREPL server
Here’s how you can start nREPL programmatically with TLS enabled:
=> (require '[nrepl.server :refer [start-server stop-server]])
nil
=> (defonce server (start-server :bind "0.0.0.0"
:port 4001
:tls? true ; will cause the server to fail if no certs/keys are given
; if server.keys is on the server
:tls-keys-file "server.keys"))
; use :tls-keys-str if the contents of server.keys is available as a string,
; e.g.
; :tls-keys-str (System/getenv "NREPL_SERVER_KEYS")
='user/server
Connect with cmdline client
$ clj -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.8.0"}}}' -M -m nrepl.cmdline --connect --host 127.0.0.1 --port 4001 --tls-keys-file client.keys
You can also use the nrepls:// URL that the server prints in its startup
message (the TLS keys still have to be supplied separately):
$ clj -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.8.0"}}}' -M -m nrepl.cmdline --connect --host nrepls://127.0.0.1:4001 --tls-keys-file client.keys
Use the TLS TCP proxy
If your editor/IDE does not support nREPL TLS connections yet, you may start a local proxy server:
$ clojure -Sdeps '{:aliases {:proxy {:deps {nrepl/nrepl {:mvn/version "1.8.0"}} :exec-fn nrepl.tls-client-proxy/start-tls-proxy :exec-args {:remote-host "localhost" :remote-port 4001 :tls-keys-file "client.keys"}}}}' -T:proxy
INFO Started TLS proxy at 127.0.0.1:44063. Proxying to localhost:4001.
This will write the locally opened port to the file .nrepl-tls-proxy-port.
Then you may connect to localhost:<.nrepl-tls-proxy-port> using a plain (no TLS) TCP connection.
Third-party clients integration
Depending on how the socket is established, there are different approaches for using TLS as a client.
If you are using nrepl.core/connect, you will need to pass
either :tls-keys-file or :tls-keys-str to that function.
If you have been using (Socket. …) directly, you will first need
to create a SSLContext using nrepl.tls/ssl-context-or-throw.
Then you can create the Socket using nrepl.tls/socket:
(defn ^SSLSocket socket
"Given an SSL context, makes a client SSLSocket."
[^SSLContext context ^String host port connect-timeout-ms]
...)
Handshake timeout
The server waits up to 30 seconds for the TLS handshake to complete before it
gives up on a connection. If your clients are on very slow links, raise
nrepl.tls/handshake-timeout-ms:
(alter-var-root #'nrepl.tls/*handshake-timeout-ms* (constantly 60000))
The server reads the var on every incoming connection, so this takes effect on
a running server too. It has to be alter-var-root rather than binding,
since the connections are accepted on the server’s own threads.
Credits
This feature would not have been possible without Kyle Kingsbury’s (a.k.a "Aphyr") great work and in particular the less-awful-ssl project. Thanks!