Guide to using built-in TLS in Redis

Redis version 6 added TLS as a built-in feature. This makes me super happy, because I'm now able to use Redis as a store with my applications running on App Engine considering that the traffic is encrypted, without additional tools or a paid Redis Cloud plan.

Keep in mind that you will have to build Redis with TLS support at compile time. See https://redis.io/topics/encryption.

I couldn't find an end-to-end example for setting up TLS (not very surprising considering that the feature was released April 30 this year). So here we go.

Generate cert files

To use TLS with Redis, you'll have to generate:

  1. a certificate-key pair for the server (redis.crt, redis.key), and
  2. a root CA certificate (ca.crt)

The TL;DR is that you run

$ FQDN=<redis-server-ip> make generate

with this Makefile:

FQDN ?= 127.0.0.1
OUTDIR ?= tls
# may be /etc/pki/tls in some systems.
# use `openssl version -a | grep OPENSSLDIR` to find out.
OPENSSLDIR ?= /etc/ssl

.PHONY: generate
generate: prepare redis.crt clean

.PHONY: prepare
prepare:
	mkdir ${OUTDIR}

.PHONY: clean
clean:
	rm -f ${OUTDIR}/openssl.cnf

openssl.cnf:
	cat ${OPENSSLDIR}/openssl.cnf > ${OUTDIR}/openssl.cnf
	echo "" >> ${OUTDIR}/openssl.cnf
	echo "[ san_env ]" >> ${OUTDIR}/openssl.cnf
	echo "subjectAltName = IP:${FQDN}" >> ${OUTDIR}/openssl.cnf

ca.key:
	openssl genrsa 4096 > ${OUTDIR}/ca.key

ca.crt: ca.key
	openssl req \
		-new \
		-x509 \
		-nodes \
		-sha256 \
		-key ${OUTDIR}/ca.key \
		-days 3650 \
		-subj "/C=AU/CN=example" \
		-out ${OUTDIR}/ca.crt

redis.csr: openssl.cnf
	# TODO(nishanths): is -extensions necessary?
	# https://security.stackexchange.com/a/86999
	SAN=IP:$(FQDN) openssl req \
		-reqexts san_env \
		-extensions san_env \
		-config ${OUTDIR}/openssl.cnf \
		-newkey rsa:4096 \
		-nodes -sha256 \
		-keyout ${OUTDIR}/redis.key \
		-subj "/C=AU/CN=$(FQDN)" \
		-out ${OUTDIR}/redis.csr

redis.crt: openssl.cnf ca.key ca.crt redis.csr
	SAN=IP:$(FQDN) openssl x509 \
		-req -sha256 \
		-extfile ${OUTDIR}/openssl.cnf \
		-extensions san_env \
		-days 3650 \
		-in ${OUTDIR}/redis.csr \
		-CA ${OUTDIR}/ca.crt \
		-CAkey ${OUTDIR}/ca.key \
		-CAcreateserial \
		-out ${OUTDIR}/redis.crt

This will produce the required cert files in a directory named tls by default. Optionally you can can set OUTDIR=<path> to specify a custom output directory. A couple of notes:

The Makefile is adapted from Stack Overflow.

Start the server

Once you have Redis built with TLS support, you'll have to set TLS-specific options in your redis.conf. Here are the relevant options from mine.

port 0
tls-port 6379
tls-cert-file tls/redis.crt
tls-key-file tls/redis.key
tls-ca-cert-file tls/ca.crt

You can find these options with detailed comments in the "TLS/SSL" section of a new redis.conf.

Place the generated cert files in your server. For the config above, I place my cert files in a directory named tls, relative to the directory from where I would start the Redis server.

With that done, you can start the Redis server.

$ ls
redis.conf  tls
$ redis-server redis.conf

Connect with a client

With the server running, you'll want to try connecting with a client. I've been able to connect successfully with redis-cli, Node.js programs, and Go programs.

Redis-cli client

Likely the easiest client to connect with to a Redis TLS server. For simplicity and to potentially save debugging time, you should try this first, locally from the same machine running the Redis server.

Run redis-cli as you mostly would, with extra options specifying the cert files. Run a sample PING command to make sure you've connected successfully.

$ redis-cli --tls --cert tls/redis.crt --key tls/redis.key --cacert tls/ca.crt
redis> ping
"PONG"

As a soundness check, try omitting one of the options. It should fail to connect.

Node.js client

For Node, I was using https://github.com/NodeRedis/node-redis, but https://github.com/luin/ioredis should work equally well.

Side note: In hindsight, I would have like to have used ioredis (and it's what I'll do in the future) because I disagree with node-redis's handling of null/undefined in SET calls.

Both packages support a tls property in their client config object. The type SecureContextOptions is from tls.createSecureContext().

tls: SecureContextOptions

So you can do (code below uses some TypeScript syntax):

import * as fs from "fs"
import redis from "redis"

const redisHost = process.env["REDIS_HOST"]! // e.g. "1.2.3.4", "127.0.0.1", "localhost", "redis.acmecorp.com"

const client = redis.createClient({
    host: redisHost,
    port: 6379,
    tls: {
        cert: fs.readFileSync("redis/tls/redis.crt"),
        key: fs.readFileSync("redis/tls/redis.key"),
        ca: fs.readFileSync("redis/tls/ca.crt"),
    },
})

Go client

I used https://github.com/go-redis/redis. The configuration is similar to Node's. The package's *redis.Options type has a field:

// TLS Config to use. When set TLS will be negotiated.
TLSConfig *tls.Config

You can do:

import (
  "crypto/tls"
  "crypto/x509"
  "fmt"
  "io/ioutil"
  "net"
  "os"

  "github.com/go-redis/redis"
)

func main() {
  redisHost := os.Getenv("REDIS_HOST") // e.g. "1.2.3.4", "127.0.0.1", "localhost", "redis.acmecorp.com"

  cert, err := tls.LoadX509KeyPair("redis/tls/redis.crt", "redis/tls/redis.key")
  if err != nil {
    ...
  }

  caCert, err := ioutil.ReadFile("redis/tls/ca.crt")
  if err != nil {
    ...
  }
  pool := x509.NewCertPool()
  pool.AppendCertsFromPEM(caCert)

  client := redis.NewClient(&redis.Options{
    Addr: net.JoinHostPort(redisHost, "6379"),
    TLSConfig: &tls.Config{
      ServerName:   redisHost,
      Certificates: []tls.Certificate{cert},
      RootCAs:      pool,
    },
  })
}

And you should hopefully have functioning clients at this stage.