mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
68c150eba4
The redis.enableTLS / redis.tlsSkipVerify settings were accepted by the config layer but silently dropped before reaching the connection pool, so the plugin always dialed Redis in plaintext. This blocked TLS-only Redis deployments such as AWS ElastiCache with in-transit encryption. - Add EnableTLS, TLSSkipVerify, TLSServerName to backends.Config and PoolConfig and forward them through universal_cache_singleton -> backends.Config -> PoolConfig. - In the connection pool, dial via tls.Dialer.DialContext (TLS 1.2 minimum) with SNI defaulting to the host part of the configured Address when TLSServerName is empty, so ElastiCache cluster endpoints validate out of the box. Plain dial path now also propagates ctx. - Add regression tests covering successful TLS negotiation with skip- verify, rejection of self-signed certs without skip-verify, rejection of plain TCP servers when EnableTLS=true, and unaffected plaintext behavior. - Document maxRefreshTokenAgeSeconds (added in1b6c861) and the implicit SSE / WebSocket auth bypass (added in684a990) in README.md, docs/CONFIGURATION.md and docs/index.html. - Add the missing redis.tlsSkipVerify row to docs/index.html and clarify the redis.enableTLS description. patch-release
231 lines
5.7 KiB
Go
231 lines
5.7 KiB
Go
package backends
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"math/big"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// drainRESPRequest consumes a single RESP request (array or inline) from r and
|
|
// returns true on success. Any read error returns false.
|
|
func drainRESPRequest(r *bufio.Reader) bool {
|
|
header, err := r.ReadString('\n')
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !strings.HasPrefix(header, "*") {
|
|
return true // inline command (single line) — already consumed
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimRight(strings.TrimPrefix(header, "*"), "\r\n"))
|
|
if err != nil || n <= 0 {
|
|
return false
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
// Each bulk: "$len\r\n<bytes>\r\n"
|
|
if _, err := r.ReadString('\n'); err != nil {
|
|
return false
|
|
}
|
|
if _, err := r.ReadString('\n'); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// startTLSPingServer spins up a TLS listener that speaks just enough RESP to
|
|
// answer PING with +PONG. Returns the listener address and a self-signed cert.
|
|
func startTLSPingServer(t *testing.T) (addr string, certPEM []byte, stop func()) {
|
|
t.Helper()
|
|
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "localhost"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
DNSNames: []string{"localhost"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
|
require.NoError(t, err)
|
|
|
|
tlsCert := tls.Certificate{
|
|
Certificate: [][]byte{der},
|
|
PrivateKey: priv,
|
|
}
|
|
|
|
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
|
|
Certificates: []tls.Certificate{tlsCert},
|
|
MinVersion: tls.VersionTLS12,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
stopCh := make(chan struct{})
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-stopCh:
|
|
return
|
|
default:
|
|
}
|
|
c, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
wg.Add(1)
|
|
go func(conn net.Conn) {
|
|
defer wg.Done()
|
|
defer conn.Close()
|
|
reader := bufio.NewReader(conn)
|
|
for {
|
|
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
if !drainRESPRequest(reader) {
|
|
return
|
|
}
|
|
_, _ = conn.Write([]byte("+PONG\r\n"))
|
|
}
|
|
}(c)
|
|
}
|
|
}()
|
|
|
|
stop = func() {
|
|
close(stopCh)
|
|
_ = listener.Close()
|
|
wg.Wait()
|
|
}
|
|
return listener.Addr().String(), der, stop
|
|
}
|
|
|
|
// TestConnectionPool_TLSDial_SkipVerify verifies that EnableTLS=true with
|
|
// TLSSkipVerify=true successfully negotiates TLS and exchanges a Redis command.
|
|
// Regression test for issue #133 (enableTLS not propagated to client).
|
|
func TestConnectionPool_TLSDial_SkipVerify(t *testing.T) {
|
|
addr, _, stop := startTLSPingServer(t)
|
|
defer stop()
|
|
|
|
pool, err := NewConnectionPool(&PoolConfig{
|
|
Address: addr,
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 2 * time.Second,
|
|
ReadTimeout: 1 * time.Second,
|
|
WriteTimeout: 1 * time.Second,
|
|
EnableTLS: true,
|
|
TLSSkipVerify: true,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
defer pool.Put(conn)
|
|
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
}
|
|
|
|
// TestConnectionPool_TLSDial_VerifyFails verifies that EnableTLS=true with
|
|
// TLSSkipVerify=false rejects a self-signed server cert.
|
|
func TestConnectionPool_TLSDial_VerifyFails(t *testing.T) {
|
|
addr, _, stop := startTLSPingServer(t)
|
|
defer stop()
|
|
|
|
pool, err := NewConnectionPool(&PoolConfig{
|
|
Address: addr,
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 2 * time.Second,
|
|
ReadTimeout: 1 * time.Second,
|
|
WriteTimeout: 1 * time.Second,
|
|
EnableTLS: true,
|
|
TLSSkipVerify: false,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
_, err = pool.Get(context.Background())
|
|
require.Error(t, err)
|
|
assert.Contains(t, strings.ToLower(err.Error()), "tls")
|
|
}
|
|
|
|
// TestConnectionPool_TLSDial_PlainServerRejected verifies that EnableTLS=true
|
|
// fails to handshake against a plain (non-TLS) listener.
|
|
func TestConnectionPool_TLSDial_PlainServerRejected(t *testing.T) {
|
|
plain, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer plain.Close()
|
|
|
|
go func() {
|
|
for {
|
|
c, acceptErr := plain.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
|
|
pool, err := NewConnectionPool(&PoolConfig{
|
|
Address: plain.Addr().String(),
|
|
MaxConnections: 1,
|
|
ConnectTimeout: 1 * time.Second,
|
|
ReadTimeout: 1 * time.Second,
|
|
WriteTimeout: 1 * time.Second,
|
|
EnableTLS: true,
|
|
TLSSkipVerify: true,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
_, err = pool.Get(context.Background())
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestConnectionPool_PlainDial_StillWorks ensures non-TLS path is unaffected
|
|
// when EnableTLS=false (default).
|
|
func TestConnectionPool_PlainDial_StillWorks(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
pool, err := NewConnectionPool(&PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 1,
|
|
ConnectTimeout: 2 * time.Second,
|
|
ReadTimeout: 1 * time.Second,
|
|
WriteTimeout: 1 * time.Second,
|
|
EnableTLS: false,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
defer pool.Put(conn)
|
|
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
}
|