Files
go/tls_cert_auth_test.go

274 lines
9.0 KiB
Go

package redis_test
import (
"context"
"crypto/tls"
"crypto/x509"
"os"
"strconv"
"strings"
"testing"
"github.com/hanzoai/kv-go/v9"
)
func init() {
// Initialize RedisVersion from environment variable for regular Go tests
// (Ginkgo tests initialize this in BeforeSuite)
if version := os.Getenv("REDIS_VERSION"); version != "" {
if v, err := strconv.ParseFloat(strings.Trim(version, "\""), 64); err == nil && v > 0 {
RedisVersion = v
}
}
}
// skipBeforeRedisVersion checks if Redis version is below the specified version and skips the test if so
func skipBeforeRedisVersion(t *testing.T, version float64, msg string) {
t.Helper()
if RedisVersion < version {
t.Skipf("Skipping test: Redis version %.1f < %.1f: %s", RedisVersion, version, msg)
}
}
// TestTLSCertificateAuthentication tests that Redis automatically authenticates
// a user based on the CN field in the client's TLS certificate.
//
// This test requires:
// 1. Redis 8.6+ configured with: tls-auth-clients-user CN
// 2. A client certificate with CN matching the Redis ACL username
// 3. The Docker image generates testcertuser.{crt,key} when TLS_CLIENT_CNS=testcertuser
//
// The test flow:
// 1. Create a Redis ACL user with a specific username (testcertuser)
// 2. Load the pre-generated client certificate with that username in the CN field
// 3. Connect using TLS with that certificate
// 4. Verify that Redis automatically authenticates as that user (no AUTH command needed)
func TestTLSCertificateAuthentication(t *testing.T) {
skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")
ctx := context.Background()
testUsername := "testcertuser"
tlsCertDir := "dockers/standalone/tls"
// Step 1: Create a non-TLS client to set up the ACL user
setupClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Non-TLS port
})
defer setupClient.Close()
// Verify connection
if err := setupClient.Ping(ctx).Err(); err != nil {
t.Fatalf("Redis not available: %v", err)
}
// Clean up any existing test user
setupClient.ACLDelUser(ctx, testUsername)
// Step 2: Create ACL user with specific permissions
// The user can read/write keys but has limited command access
err := setupClient.ACLSetUser(ctx,
testUsername,
"on", // Enable the user
"nopass", // No password required (will use cert auth)
"~*", // Can access all keys
"+get", // Allow GET command
"+set", // Allow SET command
"+ping", // Allow PING command
"+acl|whoami", // Allow ACL WHOAMI command
).Err()
if err != nil {
t.Fatalf("Failed to create ACL user: %v", err)
}
defer setupClient.ACLDelUser(ctx, testUsername) // Cleanup
// Verify user was created
users, err := setupClient.ACLUsers(ctx).Result()
if err != nil {
t.Fatalf("Failed to list ACL users: %v", err)
}
t.Logf("ACL users: %v", users)
// Step 3: Load CA certificate for server verification
caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
if err != nil {
t.Fatalf("CA cert not found: %v", err)
}
// Step 4: Load the pre-generated client certificate with CN=testcertuser
// This certificate is generated by the Docker image when TLS_CLIENT_CNS=testcertuser
clientCert, err := tls.LoadX509KeyPair(
tlsCertDir+"/"+testUsername+".crt",
tlsCertDir+"/"+testUsername+".key",
)
if err != nil {
t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
}
// Step 5: Create TLS config with the client certificate
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertPEM)
tlsConfig := &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{clientCert},
ServerName: "localhost",
InsecureSkipVerify: true, // Using self-signed certs
}
// Step 6: Connect with TLS using the certificate
// NOTE: This test requires Redis to be configured with:
// tls-auth-clients-user CN
// Without this config, the certificate CN won't be used for authentication
tlsClient := redis.NewClient(&redis.Options{
Addr: "localhost:6666", // TLS port
TLSConfig: tlsConfig,
// NO Username/Password - authentication should happen via certificate!
})
defer tlsClient.Close()
// Step 7: Verify we're authenticated as the correct user
whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
if err != nil {
t.Fatalf("ACL WHOAMI failed: %v (this test requires Redis to be configured with: tls-auth-clients-user CN)", err)
}
if whoami != testUsername {
t.Fatalf("Expected to be authenticated as %q, but got %q. Ensure Redis is configured with: tls-auth-clients-user CN", testUsername, whoami)
}
t.Logf("✅ Successfully authenticated as %q using certificate CN", whoami)
// Step 8: Test that we can execute allowed commands
err = tlsClient.Set(ctx, "test_cert_auth_key", "test_value", 0).Err()
if err != nil {
t.Fatalf("SET command failed (should be allowed): %v", err)
}
val, err := tlsClient.Get(ctx, "test_cert_auth_key").Result()
if err != nil {
t.Fatalf("GET command failed (should be allowed): %v", err)
}
if val != "test_value" {
t.Errorf("Expected 'test_value', got %q", val)
}
// Step 9: Test that we CANNOT execute disallowed commands
// The user doesn't have +del permission, so this should fail
err = tlsClient.Del(ctx, "test_cert_auth_key").Err()
if err == nil {
t.Error("DEL command succeeded but should have failed (user doesn't have +del permission)")
} else {
t.Logf("✅ DEL command correctly denied: %v", err)
}
// Cleanup
setupClient.Del(ctx, "test_cert_auth_key")
t.Log("✅ TLS certificate authentication test passed")
}
// TestTLSCertificateAuthenticationNoUser tests that when a certificate CN
// doesn't match any existing ACL user, Redis falls back to the default user.
//
// This test:
// 1. Ensures the testcertuser ACL user does NOT exist
// 2. Connects with a certificate that has CN=testcertuser
// 3. Verifies that Redis authenticates as "default" (fallback behavior)
func TestTLSCertificateAuthenticationNoUser(t *testing.T) {
skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")
ctx := context.Background()
testUsername := "testcertuser"
tlsCertDir := "dockers/standalone/tls"
// Step 1: Create a non-TLS client to ensure the user does NOT exist
setupClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Non-TLS port
})
defer setupClient.Close()
// Verify connection
if err := setupClient.Ping(ctx).Err(); err != nil {
t.Fatalf("Redis not available: %v", err)
}
// Delete the test user if it exists - we want to test fallback behavior
setupClient.ACLDelUser(ctx, testUsername)
// Verify user does not exist
users, err := setupClient.ACLUsers(ctx).Result()
if err != nil {
t.Fatalf("Failed to list ACL users: %v", err)
}
for _, u := range users {
if u == testUsername {
t.Fatalf("User %q should not exist for this test", testUsername)
}
}
t.Logf("ACL users (should not contain %s): %v", testUsername, users)
// Step 2: Load CA certificate for server verification
caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
if err != nil {
t.Fatalf("CA cert not found: %v", err)
}
// Step 3: Load the client certificate with CN=testcertuser
// Even though the user doesn't exist, we still use this certificate
clientCert, err := tls.LoadX509KeyPair(
tlsCertDir+"/"+testUsername+".crt",
tlsCertDir+"/"+testUsername+".key",
)
if err != nil {
t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
}
// Step 4: Create TLS config with the client certificate
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertPEM)
tlsConfig := &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{clientCert},
ServerName: "localhost",
InsecureSkipVerify: true, // Using self-signed certs
}
// Step 5: Connect with TLS using the certificate
tlsClient := redis.NewClient(&redis.Options{
Addr: "localhost:6666", // TLS port
TLSConfig: tlsConfig,
})
defer tlsClient.Close()
// Step 6: Verify we're authenticated as "default" (fallback)
whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
if err != nil {
t.Fatalf("ACL WHOAMI failed: %v (Redis may not be configured for certificate-based authentication)", err)
}
// When the CN user doesn't exist, Redis should fall back to "default"
if whoami != "default" {
t.Fatalf("Expected to be authenticated as %q (fallback), but got %q", "default", whoami)
}
t.Logf("✅ Correctly fell back to %q user (CN user %q does not exist)", whoami, testUsername)
// Step 7: Verify we can execute commands as default user
err = tlsClient.Set(ctx, "test_cert_auth_fallback_key", "fallback_value", 0).Err()
if err != nil {
t.Fatalf("SET command failed: %v", err)
}
val, err := tlsClient.Get(ctx, "test_cert_auth_fallback_key").Result()
if err != nil {
t.Fatalf("GET command failed: %v", err)
}
if val != "fallback_value" {
t.Errorf("Expected 'fallback_value', got %q", val)
}
// Cleanup
tlsClient.Del(ctx, "test_cert_auth_fallback_key")
t.Log("✅ TLS certificate authentication fallback test passed")
}