A 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), which would have really helped a novice like me. So here we are in case it helps someone else.

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 machines.
# 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
    # 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 tlsby 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 (you can scp them over). For the config above, I placed 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
$ ./path/to/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 #

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.

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

As a sanity check, try omitting one of the options and you 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.

Complete side note: In hindsight, I would have like to have used ioredis (and it’s what I’ll do in the future) because of its Promise API, and I dislike 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 (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"
)

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.

 
71
Kudos
 
71
Kudos

Now read this

Parallel tests in Go

Go surprises me with how simple it makes things. Today, it was parallel testing. In other languages I’ve used, it may require third-party packages, require complicated syntax, or may just not be possible. In Go, it’s as simple as: import... Continue →