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 just this June), 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 365 \
        -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.

 
3
Kudos
 
3
Kudos

Now read this

Hibernate in Fedora

I spent time yesterday making HybridSleep and Hibernate work in Fedora 32 on a ThinkPad X1 Carbon with an encrypted disk. I don’t ever plan to manually use hybrid sleep or hibernate; I’d instead just lock my screen or close the laptop... Continue →