Add ability to specify client certificate and key for redis connection

This commit is contained in:
Nick Sherron 2024-02-23 22:04:06 -08:00
parent d1b889456d
commit 0223c44c82
No known key found for this signature in database
GPG Key ID: 3D8176919889DF27
7 changed files with 394 additions and 1 deletions

3
.gitignore vendored
View File

@ -29,6 +29,9 @@ package-json.lock
/api
dist/
# testdata
/cmd/asynqmon/testdata/
# Editor configs
.idea/
.vscode/

View File

@ -3,6 +3,7 @@ package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"log"
@ -30,6 +31,9 @@ type Config struct {
RedisDB int
RedisPassword string
RedisTLS string
RedisCaCert string
RedisClientCert string
RedisClientKey string
RedisURL string
RedisInsecureTLS bool
RedisClusterNodes string
@ -64,6 +68,9 @@ func parseFlags(progname string, args []string) (cfg *Config, output string, err
flags.IntVar(&conf.RedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number")
flags.StringVar(&conf.RedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server")
flags.StringVar(&conf.RedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server")
flags.StringVar(&conf.RedisCaCert, "redis-ca-cert", getEnvDefaultString("REDIS_CA_CERT", ""), "path to CA certificate file used when connecting to redis server")
flags.StringVar(&conf.RedisClientCert, "redis-client-cert", getEnvDefaultString("REDIS_CLIENT_CERT", ""), "path to client certificate file used when connecting to redis server")
flags.StringVar(&conf.RedisClientKey, "redis-client-key", getEnvDefaultString("REDIS_CLIENT_KEY", ""), "path to client key file used when connecting to redis server")
flags.StringVar(&conf.RedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server")
flags.BoolVar(&conf.RedisInsecureTLS, "redis-insecure-tls", getEnvOrDefaultBool("REDIS_INSECURE_TLS", false), "disable TLS certificate host checks")
flags.StringVar(&conf.RedisClusterNodes, "redis-cluster-nodes", getEnvDefaultString("REDIS_CLUSTER_NODES", ""), "comma separated list of host:port addresses of cluster nodes")
@ -128,6 +135,32 @@ func makeRedisConnOpt(cfg *Config) (asynq.RedisConnOpt, error) {
if connOpt.TLSConfig == nil {
connOpt.TLSConfig = makeTLSConfig(cfg)
}
if cfg.RedisClientCert == "" && cfg.RedisCaCert == "" {
return connOpt, nil
}
if connOpt.TLSConfig == nil {
connOpt.TLSConfig = &tls.Config{}
}
if cfg.RedisClientCert != "" {
cert, err := tls.LoadX509KeyPair(cfg.RedisClientCert, cfg.RedisClientKey)
if err != nil {
return nil, fmt.Errorf("get certificate error RedisClientCert:%s RedisClientKey:%s error:%s", cfg.RedisClientCert, cfg.RedisClientKey, err)
}
connOpt.TLSConfig.Certificates = []tls.Certificate{cert}
}
if cfg.RedisCaCert != "" {
caCert, err := os.ReadFile(cfg.RedisCaCert)
if err != nil {
return nil, fmt.Errorf("read ca cert error RedisCaCert:%s error:%s", cfg.RedisCaCert, err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
connOpt.TLSConfig.RootCAs = caCertPool
}
return connOpt, nil
}

View File

@ -1,13 +1,30 @@
package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"io"
"math/big"
"net"
"os"
"os/exec"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
)
func TestParseFlags(t *testing.T) {
@ -135,3 +152,252 @@ func TestMakeRedisConnOpt(t *testing.T) {
})
}
}
type redisServer struct {
addr string
port int
pid int
args []string
output io.ReadWriter
cmd *exec.Cmd
}
func findFreePort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("net.Listen failed: %v", err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func newRedisServer(t *testing.T, args ...string) *redisServer {
t.Helper()
port := findFreePort(t)
addr := fmt.Sprintf("127.0.0.1:%d", port)
cmdArgs := []string{}
defaultArgs := map[string]string{
"--port": strconv.Itoa(port),
"--save": "",
"--appendonly": "no",
}
for _, arg := range args {
cmdArgs = append(cmdArgs, arg)
if _, ok := defaultArgs[arg]; ok {
// Remove the default argument from the map.
delete(defaultArgs, arg)
}
}
for k, v := range defaultArgs {
cmdArgs = append(cmdArgs, k, v)
}
buf := new(bytes.Buffer)
cmd := exec.Command("redis-server", cmdArgs...)
cmd.Stderr = buf
cmd.Stdout = buf
if err := cmd.Start(); err != nil {
t.Fatalf("redis-server failed to start: %v", err)
}
return &redisServer{
addr: addr,
port: port,
pid: cmd.Process.Pid,
args: cmdArgs,
output: buf,
cmd: cmd,
}
}
var regenerateTLSFiles = flag.Bool("regenerate-tls-files", false, "regenerate TLS files")
// newRedisServerWithTLS creates a new redis-server instance with TLS enabled.
// If the regenerate-tls-files flag is set, it will generate new TLS files.
func newRedisServerWithTLS(t *testing.T, args ...string) *redisServer {
t.Helper()
caFile := "testdata/ca.crt"
caPrivKeyFile := "testdata/ca.key"
serverCertFile := "testdata/server.crt"
serverPrivKeyFile := "testdata/server.key"
if *regenerateTLSFiles {
caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("rsa.GenerateKey failed: %v", err)
}
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
CommonName: "asynqmon test CA",
Organization: []string{"asynqmon"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
t.Fatalf("x509.CreateCertificate failed: %v", err)
}
writePemToFile(caPrivKeyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(caPrivKey))
writePemToFile(caFile, "CERTIFICATE", caBytes)
// Generate server private key
serverPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
// Define server certificate template
cert := &x509.Certificate{
SerialNumber: big.NewInt(2024),
Subject: pkix.Name{
CommonName: "localhost",
},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0), // 1 year
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
// Create server Certificate
serverCert, err := x509.CreateCertificate(rand.Reader, cert, ca, &serverPrivKey.PublicKey, caPrivKey)
if err != nil {
panic(err)
}
writePemToFile(serverPrivKeyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverPrivKey))
writePemToFile(serverCertFile, "CERTIFICATE", serverCert)
}
tlsPort := findFreePort(t)
args = append(args, "--tls-port", strconv.Itoa(tlsPort))
args = append(args, "--tls-cert-file", serverCertFile)
args = append(args, "--tls-key-file", serverPrivKeyFile)
args = append(args, "--tls-ca-cert-file", caFile)
args = append(args, "--tls-auth-clients", "no")
red := newRedisServer(t, args...)
red.port = tlsPort
red.addr = fmt.Sprintf("127.0.0.1:%d", tlsPort)
return red
}
// writePemToFile writes PEM-encoded data to a file with the specified filename and PEM block type.
func writePemToFile(filename, pemType string, bytes []byte) {
file, err := os.Create(filename)
if err != nil {
panic(err)
}
defer file.Close()
pemBlock := &pem.Block{
Type: pemType,
Bytes: bytes,
}
if err := pem.Encode(file, pemBlock); err != nil {
panic(err)
}
}
func TestConfigureAndPingRedis(t *testing.T) {
_, err := exec.LookPath("redis-server")
if err != nil {
t.Skip("redis-server not found in PATH")
}
tests := []struct {
desc string
cfg *Config
getServer func() *redisServer
}{
{
desc: "Basic",
cfg: &Config{
RedisAddr: "localhost:6380",
},
getServer: func() *redisServer {
return newRedisServer(t)
},
},
{
desc: "With TLS certs",
cfg: &Config{
RedisCaCert: "testdata/ca.crt",
RedisClientCert: "testdata/server.crt",
RedisClientKey: "testdata/server.key",
RedisTLS: "",
},
getServer: func() *redisServer {
return newRedisServerWithTLS(t)
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
red := tc.getServer()
defer red.cmd.Process.Kill()
time.Sleep(5 * time.Second)
cfg := tc.cfg
cfg.RedisAddr = red.addr
cfg.RedisDB = 1
printServerOutput := func() {
t.Helper()
t.Logf("redis-server output:\n%s", red.output)
}
got, err := makeRedisConnOpt(cfg)
if err != nil {
printServerOutput()
t.Fatalf("makeRedisConnOpt returned error: %v", err)
}
client, ok := got.MakeRedisClient().(redis.UniversalClient)
if !ok {
printServerOutput()
t.Fatalf("got.MakeRedisClient() returned a non-redis.UniversalClient type")
}
defer client.Close()
if _, err := client.Ping(context.Background()).Result(); err != nil {
printServerOutput()
t.Errorf("client.Ping() returned error: %v", err)
}
client.Set(context.Background(), "foo", "bar", 0)
key, err := client.Get(context.Background(), "foo").Result()
if err != nil {
printServerOutput()
t.Errorf("client.Get() returned error: %v", err)
}
if key != "bar" {
printServerOutput()
t.Errorf("client.Get() returned %q, want %q", key, "bar")
}
})
}
}

19
cmd/asynqmon/testdata/ca.crt vendored Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDGjCCAgKgAwIBAgICB+MwDQYJKoZIhvcNAQELBQAwLjERMA8GA1UEChMIYXN5
bnFtb24xGTAXBgNVBAMTEGFzeW5xbW9uIHRlc3QgQ0EwHhcNMjQwMjI4MjA0ODA2
WhcNMjUwMjI4MjA0ODA2WjAuMREwDwYDVQQKEwhhc3lucW1vbjEZMBcGA1UEAxMQ
YXN5bnFtb24gdGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN327FkhXwh8KKitVIEsZydLFBJWzpZ1rRGOF554IEsptXQZhCxTrwJfcLWDilVU
PCPATqJ5X4v4HxAUNhrV5g9TBjeR8YKn753YPwDfOYllC1NN6tOyCe++3rg5YPt2
8GF9oNkNWbAEnkV9E6mejLOJns/XTfY/VtAox9qqTK0q5ER6b9pMWEVT0sFcgQAy
cQbnx9rgB8hkNddiQb28e7gaPI2H+NBYOkdSZYy1lL2uQxEq/pP1RmVa8wS+Cfwv
WwIYytw06yQRim0q1Wm/76mU5D3cSSkoe10Y0zLUL7spw3DPD1Wj9H1McAjeuyRe
EEE8/JiZ1As7KDrgSItvf9cCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKEMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFEakM/CIItCYPgz48kDktCAoTNxrMA0GCSqG
SIb3DQEBCwUAA4IBAQA682/3dsF/bqhrYgOHrzPvjtITx6CmwCDfVRJdSLBiMwEz
YCo4muNScfftXyFl3GYmRROydDjgWqsYkwWBGD/wqNXBoxn61EsWhuNAp6Yzr9hm
YnshBAdjVlqu4Nwfhi5fwbzUnJViD/jwBjV+IU8YzHjF+ZApalboiUCDDLt8m8z/
m5rc7T/5Z0wNacc6+hNMhNbT8FFlARgV74yqXhDHECbIgqlsG+udOeHoGUIQrmoW
k/bhRMerORtxPvHrrLPIf1YTDmohCz5++kaKXLen874NsaZ0SpseoUK28GucQpUN
I/1PqmsO/vi/r7cUvPtbIHL0dJRrN5zBwIS1vPgy
-----END CERTIFICATE-----

27
cmd/asynqmon/testdata/ca.key vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA3fbsWSFfCHwoqK1UgSxnJ0sUElbOlnWtEY4XnnggSym1dBmE
LFOvAl9wtYOKVVQ8I8BOonlfi/gfEBQ2GtXmD1MGN5Hxgqfvndg/AN85iWULU03q
07IJ777euDlg+3bwYX2g2Q1ZsASeRX0TqZ6Ms4mez9dN9j9W0CjH2qpMrSrkRHpv
2kxYRVPSwVyBADJxBufH2uAHyGQ112JBvbx7uBo8jYf40Fg6R1JljLWUva5DESr+
k/VGZVrzBL4J/C9bAhjK3DTrJBGKbSrVab/vqZTkPdxJKSh7XRjTMtQvuynDcM8P
VaP0fUxwCN67JF4QQTz8mJnUCzsoOuBIi29/1wIDAQABAoIBAQDb8wl1sRno4I+x
xkCM2CFH0KANJDQG6Ikdcj55a/QkRypl57sP6cTshwK6+6Qithv6GWBSpA9INhEh
78VFhlw5Jz5r5pT5scxCD70u8gSj35r/a6CdMjmidvNgfotZ5ByDnue67f3H7Guh
1DWdyV0HtAHJV0MMFuvBzgds6YCdvqBpKxvFJUq0N++QFGa3BHaHllaGtIDHsEjb
L+ntHe2Gvc6EVPDaDmoyXHZne3shy1A0rtl5Our7sjuLHO4ZNLHr8SmzkKYSHgSw
F4CVKWBVEoCfCwLpx3nZcb+CbF8X0PIcWTzXihpkfzpOhSP1Ke4QRPhOcqmMKKWR
+hEHLOfRAoGBAPSowes/niv0nlXRJDdl+aa2Qxrwi2xZVgzNdTWbsazhwQrLhIjK
DkqM1PeYWT1X9XgAV4ElYwamPz26pg1ZfTyBbszhDbRrBcbb2zIVIy5c1JTvdd8A
2CzTCFFpmJ09zYDcGNixHJE7FcgzUs/RV9JDh47hAtfCyuxHx5tmLrONAoGBAOhA
3CxlwlE0zBWWZeip3+6WwoABFFzhuSlE/VLI8jkmW4HdwFcnle3NPkir6Feg+9qr
CD2sjazr0DjmH4z5eKwUiAF6inFUQ9TbUSoy4om5WXT2OjGYR7b5d2WRlpQi/osz
7EjqJftQbgV/H/JQ8YayNTWiCgMvonzTAJI1gpXzAoGAE8VPZmNNtN+fq++qrY9g
DUjNQ3AM1ESj34T648ohIYdcwjKQEz3AyeV3kEqPa5WgEIJ2j8klp3PnyGU85fdF
V45eFdBZ+ypq3RcHL5TlsulthFuVet/mmDi1g161Jn/IC5G9sEUfudy8deEv3/ta
zXMHkVQ9lpH3NADY8IXhYEECgYEA5VDFE4EVr6B1sQribCruU2C/giuOs3abn8fi
Z47IuuzIhR0x/9uyCS4RRSeXLI5inbEpXdu1tvrOiJ+On17iauWKtAsODn+oyc4S
AZxkWJ+NWBKVusokZOFDpiFtj65NrZwCvKuT/OOY/gxauqJ5Fwl1yBLJ2AN8Z8re
UX5MBUkCgYEA7bW2Y2LLZYnqAhkwVCpSFNelX+lD9jG5JGmgnU4Q2hukRME9UrmJ
AoNBbKDOZchiSRDQcogTUujS5ZlEqpsC69gXg/E+c6KyccmSVTDfMkzBBBnFzKlT
M7tSkU9xUbVyVN0iNqRfRZsEQEp5m+6roCnFymxmkhcqyHB3ZcNZnk0=
-----END RSA PRIVATE KEY-----

18
cmd/asynqmon/testdata/server.crt vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9jCCAd6gAwIBAgICB+gwDQYJKoZIhvcNAQELBQAwLjERMA8GA1UEChMIYXN5
bnFtb24xGTAXBgNVBAMTEGFzeW5xbW9uIHRlc3QgQ0EwHhcNMjQwMjI4MjA0ODA2
WhcNMjUwMjI4MjA0ODA2WjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfTiic0uPF6OOVR6f+RPf2W3JI5bEzMAl8
Jgn6eXGdkRwCDfWJb70idQjAifqnpdaRKPcaLI9G9zSbIXXRTowMwhmJCN9qH9Qi
iSJ9Sg8iLQdU2pcffTl0b3NpFhipCOReDpYLiPwwX8h5Q+OAWDW75XDfsBcukV8m
d3ejgZQQkBqRad5zGpmkZsRhzYkb6Wojzcww58OZtcVsmvUJZ8RE++uuu6rIh82t
KHxYRmTN+0bOFYYTMRrkMzbv6akNY/qPoEwaqA/DdMQqUXlvzKg/XIQECHGLUqxu
xRH426df7bkkJOoEx+zvKj/ZEY5xWcIGBHj/spWyK7zmNX+TIDeTAgMBAAGjODA2
MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAG
hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBjbzlAnCmCk4O7e9i/ysnJeBcAcQeC
x18HSNr1hD8iXsA/r5giBeBJQx1KGezyi3kc77d8j9yk60eclQLtJs5Lsr305xx6
a0djy23SvXnH6rU6Hr09nyi4C14VFoc7pd7vc7MSzDiga24nuTBF2jXDbCOYIdnV
1j1f35/2RL4O4rLwwe20P3gnPAQ6Ju61JWAdxhrn3sOWtiqjIypLDUcJpx0UMZas
QamN54tMByWSkS139eqXPaXgoYLVz/2vUSlsZ8P4R9uLhRKtBc2Ku9ZBFdyTlUV5
b0SUEcNKVU9MNHcl41bHC9tXIoZVyMUfS+qB0wP6F5rI+DzK3EmUyvSg
-----END CERTIFICATE-----

27
cmd/asynqmon/testdata/server.key vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA304onNLjxejjlUen/kT39ltySOWxMzAJfCYJ+nlxnZEcAg31
iW+9InUIwIn6p6XWkSj3GiyPRvc0myF10U6MDMIZiQjfah/UIokifUoPIi0HVNqX
H305dG9zaRYYqQjkXg6WC4j8MF/IeUPjgFg1u+Vw37AXLpFfJnd3o4GUEJAakWne
cxqZpGbEYc2JG+lqI83MMOfDmbXFbJr1CWfERPvrrruqyIfNrSh8WEZkzftGzhWG
EzEa5DM27+mpDWP6j6BMGqgPw3TEKlF5b8yoP1yEBAhxi1KsbsUR+NunX+25JCTq
BMfs7yo/2RGOcVnCBgR4/7KVsiu85jV/kyA3kwIDAQABAoIBAEyfrR/i3XWTrEQV
CngdglhumJCbAGroGNkY1GO2OF4w5MNvtskqJmQkdJRcxD2ykiXNQL0ifSeEu/Bf
UuY3ZacbE1gKS19G/Ku9ErCbMQYxHUroluKfPY/OjnOIuX2HJ5V+u83Je3+93jR+
LxpjKk0HNewLqGi6SUQRymO4mu3zYHFLOj57zCcPLFre6xFoSYn+7bMWwAQWKRGg
+fj+5naymssUwHZV+pXtTgjeB+ow2LZ7zpVwUopLNo/3yH33LPfLTSzvNB0gV5R/
awZHDLQSpVwLURoppgnG793OoCT0PBK5e+A9TEOH/4GU8ZFDy7oNv1geCW9N6cDe
dn+DosECgYEA5X88lwvrb/L+yyAMqCTRSBH73IbFu7sD5eF/CYDZ5jPXyxHDhi3r
Dx/3peW5jZk2mRflSspSAumW1QS5EFc3fwrvngDOgrnclwoeM2nPtg4BC/Jtrdg4
9YCJkj8luCQfOOLkwawAsO9yt53YiXppYTVMO4ZuiaQ0zLizCGNPXC8CgYEA+Rff
X4RwtC3csAM92fL1OCx/nJXGA46qEtGTFzmV+pl2QZttIO0MkZvyC3iyWlh1mJdD
kmzbXClDwsglewxUnH9nrQFkmqYYKxEvXOiMcJBCrnP0C9s7OfFFrh4WfiyMR+TX
cS8N8fLrIny+OFlTULhjErHZv/MbLB3AffL0zd0CgYEAlEFjAezoVnSy1sPIiWLn
c9hyTR8fY8xHk1zd9WSw3z7Ee+Ho3qiRPj8Xe6tw+CFvHO1L6cnTux/tmYUojH7b
Ug3dh8PbpKWu9D/MDMihL2nSkUY2RmT1Ptufg8OZeWCUbupcfyS/eY3mHOoydXWH
2A1XRujsRay3kz0KIzQMk28CgYEAtB+2MF0WHsTXRBRkApn1B1TuRq3rjaD5jTgt
dGr48ElOwWyCQoAISbcKFY+G8VvsVZZ0j4rWKVPRoyWWLN+iw7RBpVJPjKE08tev
dzDWdYNsJLjGrlgvANxeteUeAMl3+3kY7cjH/cDalYq9BwRZAhMD2X3wZySF7qXp
D2rD6aUCgYAgpH/a/MVs7PEEUjIpzk/JwGos0EjEHVGbxOJGfk80RBYNzJdfaVrP
cXUlv5IuU9nCzF5/1dPfbyKekFp7o3NIPF+oe6oIn55QIaAFRh40CT75C+OSwnx5
JRGog1u0Iu3mxBHBCI8fwThp68NMD2OVIX/kGO+gl7jAtfVOtjQPQA==
-----END RSA PRIVATE KEY-----