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.
Generating certificates and keys
First things first - we need to generate the 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.6"}} :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.
|
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.3.0"}}}' -M -m nrepl.cmdline --connect --host 127.0.0.1 --port 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.3.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]
...)
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!