mirror of
https://github.com/hanzoai/redis.git
synced 2026-10-30 18:25:12 +00:00
This PR implements support for automatic client authentication based on a field in the client's TLS certificate. We adopt ValKey’s PR: https://github.com/valkey-io/valkey/pull/1920 API Changes: Add New configuration tls-auth-clients-user - Allowed values: `off` (default), `CN`. - `off` – disable TLS certificate–based auto-authentication. - `CN` – derive the ACL username from the Common Name (CN) field of the client certificate. New INFO stat - `acl_access_denied_tls_cert` - Counts failed TLS certificate–based authentication attempts, i.e. TLS connections where a client certificate was presented, a username was derived from it, but no matching ACL user was found. New ACL LOG reason - Reason string: `"tls-cert"` - Emitted when a client certificate’s Common Name fails to match any existing ACL user. Implementation Details: - Added getCertFieldByName() utility to extract fields from peer certificates. - Added autoAuthenticateClientFromCert() to handle automatic login logic post-handshake. - Integrated automatic authentication into the TLSAccept function after handshake completion. - Updated test suite (tests/integration/tls.tcl) to validate the feature.
220 lines
8.0 KiB
Tcl
220 lines
8.0 KiB
Tcl
start_server {tags {"tls"}} {
|
|
if {$::tls} {
|
|
package require tls
|
|
|
|
test {TLS: Not accepting non-TLS connections on a TLS port} {
|
|
set s [redis [srv 0 host] [srv 0 port]]
|
|
catch {$s PING} e
|
|
set e
|
|
} {*I/O error*}
|
|
|
|
test {TLS: Verify tls-auth-clients behaves as expected} {
|
|
set s [redis [srv 0 host] [srv 0 port]]
|
|
::tls::import [$s channel]
|
|
catch {$s PING} e
|
|
assert_match {*error*} $e
|
|
|
|
r CONFIG SET tls-auth-clients no
|
|
|
|
set s [redis [srv 0 host] [srv 0 port]]
|
|
::tls::import [$s channel]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
r CONFIG SET tls-auth-clients optional
|
|
|
|
set s [redis [srv 0 host] [srv 0 port]]
|
|
::tls::import [$s channel]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
r CONFIG SET tls-auth-clients yes
|
|
|
|
set s [redis [srv 0 host] [srv 0 port]]
|
|
::tls::import [$s channel]
|
|
catch {$s PING} e
|
|
assert_match {*error*} $e
|
|
}
|
|
|
|
test {TLS: Verify tls-protocols behaves as expected} {
|
|
r CONFIG SET tls-protocols TLSv1.2
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-tls1.2 0}]
|
|
catch {$s PING} e
|
|
assert_match {*I/O error*} $e
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-tls1.2 1}]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
r CONFIG SET tls-protocols ""
|
|
}
|
|
|
|
test {TLS: Verify tls-ciphers behaves as expected} {
|
|
r CONFIG SET tls-protocols TLSv1.2
|
|
r CONFIG SET tls-ciphers "DEFAULT:-AES128-SHA256"
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-cipher "-ALL:AES128-SHA256"}]
|
|
catch {$s PING} e
|
|
assert_match {*I/O error*} $e
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-cipher "-ALL:AES256-SHA256"}]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
r CONFIG SET tls-ciphers "DEFAULT"
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-cipher "-ALL:AES128-SHA256"}]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
r CONFIG SET tls-protocols ""
|
|
r CONFIG SET tls-ciphers "DEFAULT"
|
|
}
|
|
|
|
test {TLS: Verify tls-prefer-server-ciphers behaves as expected} {
|
|
r CONFIG SET tls-protocols TLSv1.2
|
|
r CONFIG SET tls-ciphers "AES128-SHA256:AES256-SHA256"
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-cipher "AES256-SHA256:AES128-SHA256"}]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
assert_equal "AES256-SHA256" [dict get [::tls::status [$s channel]] cipher]
|
|
|
|
r CONFIG SET tls-prefer-server-ciphers yes
|
|
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1 {-cipher "AES256-SHA256:AES128-SHA256"}]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
|
|
assert_equal "AES128-SHA256" [dict get [::tls::status [$s channel]] cipher]
|
|
|
|
r CONFIG SET tls-protocols ""
|
|
r CONFIG SET tls-ciphers "DEFAULT"
|
|
}
|
|
|
|
test {TLS: Verify tls-cert-file is also used as a client cert if none specified} {
|
|
set master [srv 0 client]
|
|
set master_host [srv 0 host]
|
|
set master_port [srv 0 port]
|
|
|
|
# Use a non-restricted client/server cert for the replica
|
|
set redis_crt [format "%s/tests/tls/redis.crt" [pwd]]
|
|
set redis_key [format "%s/tests/tls/redis.key" [pwd]]
|
|
|
|
start_server [list overrides [list tls-cert-file $redis_crt tls-key-file $redis_key] \
|
|
omit [list tls-client-cert-file tls-client-key-file]] {
|
|
set replica [srv 0 client]
|
|
$replica replicaof $master_host $master_port
|
|
wait_for_condition 30 100 {
|
|
[string match {*master_link_status:up*} [$replica info replication]]
|
|
} else {
|
|
fail "Can't authenticate to master using just tls-cert-file!"
|
|
}
|
|
}
|
|
}
|
|
|
|
test {TLS: switch between tcp and tls ports} {
|
|
set srv_port [srv 0 port]
|
|
|
|
# TLS
|
|
set rd [redis [srv 0 host] $srv_port 0 1]
|
|
$rd PING
|
|
|
|
# TCP
|
|
$rd CONFIG SET tls-port 0
|
|
$rd CONFIG SET port $srv_port
|
|
$rd close
|
|
|
|
set rd [redis [srv 0 host] $srv_port 0 0]
|
|
$rd PING
|
|
|
|
# TLS
|
|
$rd CONFIG SET port 0
|
|
$rd CONFIG SET tls-port $srv_port
|
|
$rd close
|
|
|
|
set rd [redis [srv 0 host] $srv_port 0 1]
|
|
$rd PING
|
|
$rd close
|
|
}
|
|
|
|
test {TLS: Working with an encrypted keyfile} {
|
|
# Create an encrypted version
|
|
set keyfile [lindex [r config get tls-key-file] 1]
|
|
set keyfile_encrypted "$keyfile.encrypted"
|
|
exec -ignorestderr openssl rsa -in $keyfile -out $keyfile_encrypted -aes256 -passout pass:1234 2>/dev/null
|
|
|
|
# Using it without a password fails
|
|
catch {r config set tls-key-file $keyfile_encrypted} e
|
|
assert_match {*Unable to update TLS*} $e
|
|
|
|
# Now use a password
|
|
r config set tls-key-file-pass 1234
|
|
r config set tls-key-file $keyfile_encrypted
|
|
}
|
|
|
|
test {TLS: Auto-authenticate using tls-auth-clients-user (CN)} {
|
|
# Create a user matching the CN in the client certificate (CN=Client-only)
|
|
r ACL SETUSER {Client-only} on >clientpass allcommands allkeys
|
|
|
|
# Map the client certificate CN to the ACL user name.
|
|
r CONFIG SET tls-auth-clients-user CN
|
|
|
|
# Connect over TLS using the test client certificate (CN=Client-only)
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1]
|
|
catch {$s PING} e
|
|
assert_match {PONG} $e
|
|
assert_equal "Client-only" [$s ACL WHOAMI]
|
|
}
|
|
|
|
foreach user_type {"non-existent" "disabled"} {
|
|
test "TLS: $user_type user cannot auto-authenticate via certificate" {
|
|
if {$user_type eq "non-existent"} {
|
|
# Ensure the Client-only user does not exist so auto-auth will fail
|
|
catch {r ACL DELUSER {Client-only}}
|
|
} else {
|
|
r ACL SETUSER {Client-only} on >clientpass allcommands allkeys
|
|
r ACL SETUSER {Client-only} off ;# Disable the user
|
|
}
|
|
r ACL LOG RESET
|
|
r CONFIG SET tls-auth-clients-user CN
|
|
|
|
# Capture the current value of acl_access_denied_tls_cert from INFO stats
|
|
set info_before [r INFO stats]
|
|
regexp {acl_access_denied_tls_cert:(\d+)} $info_before -> before
|
|
|
|
# Connect over TLS using the test client certificate (CN=Client-only)
|
|
# Since there is no matching ACL user or user is disabled, auto-auth should fail
|
|
# and the connection should remain authenticated as the default user
|
|
set s [redis [srv 0 host] [srv 0 port] 0 1]
|
|
assert_equal "default" [$s ACL WHOAMI]
|
|
|
|
# The ACL LOG should contain a single entry with reason "tls-cert"
|
|
# and username "Client-only"
|
|
set log [r ACL LOG]
|
|
assert_equal 1 [llength $log]
|
|
set entry [lindex $log 0]
|
|
assert_equal "tls-cert" [dict get $entry reason]
|
|
assert_equal "Client-only" [dict get $entry username]
|
|
|
|
# INFO stats should report that acl_access_denied_tls_cert increased by 1
|
|
set info_after [r INFO stats]
|
|
regexp {acl_access_denied_tls_cert:(\d+)} $info_after -> after
|
|
assert {$after == $before + 1}
|
|
|
|
# Verify fallback to password auth works after cert auth fails
|
|
r ACL SETUSER testuser on >testpass +@all ~*
|
|
$s AUTH testuser testpass
|
|
assert_equal "testuser" [$s ACL WHOAMI]
|
|
assert_equal "PONG" [$s PING]
|
|
|
|
# Clean up
|
|
r ACL DELUSER testuser
|
|
catch {r ACL DELUSER {Client-only}}
|
|
}
|
|
}
|
|
}
|
|
}
|