14 Commits
Author SHA1 Message Date
hristostaykov-del 7716edb651 Verify the TLS server identity by default in redis-cli, redis-benchmark and Sentinel (#16033)
Followup #15492

## Verify the TLS server identity by default in redis-cli,
redis-benchmark and Sentinel - Breaking change for TLS users of these
tools - see "Impact on existing deployments"

## What this PR contains

1. **Verify the TLS server identity by default** in the three tools (the
change).
2. **Reject IP addresses in `tls-expected-peer-name`.** The server
option is
defined as DNS names only; an IP address configured there matched
nothing
and rejected every peer, so it is now refused when the configuration is
   loaded or set.
3. **Share the TLS peer identity primitive.** `tls.c`, `cli_common.c`
and
`sentinel.c` each had a copy of the OpenSSL calls that bind an expected
identity; they are consolidated into `tlshelpers.h`. No behavior change.
4. **Sentinel: retry the Pub/Sub link when TLS setup fails.**
Pre-existing: a
   failed TLS setup left the Pub/Sub link dead until restart.
5. **Add a positive control to the cluster-bus impersonation test**, so
its
   log-absence assertion cannot pass vacuously.

## Background

A TLS client checks two things about a server certificate: that a
trusted CA
issued it (the "chain"), and that it was issued for the server being
contacted (the "identity": the host name or IP in the certificate's
Subject
Alternative Name). `redis-cli`, `redis-benchmark` and Sentinel's
connections
to the instances it monitors checked only the chain. So anyone holding
any
certificate from the same CA — under a shared or public CA — could
impersonate
a Redis server to them and receive the `AUTH` credentials they send.
Sentinel
makes these connections on its own, continuously, for every master,
replica
and sentinel it monitors.

## What changes

The three tools now also verify the identity, as a browser or `curl`
would.
The identity they expect is, in order of preference:

1. **An explicitly configured name**: `--tls-expected-peer-name` for the
tools (DNS names, or a single IP address), `tls-expected-peer-name` for
   Sentinel (DNS names only, as for the rest of the server). Meant for
   deployments where every node's certificate carries one shared name,
including cluster mode, where node addresses come from the cluster
itself.
2. **The name given to `--sni`**, which is now both sent to the server
and
   verified (before, it was only sent).
3. **The host that was dialed**: an IP is matched against the IP entries
of
   the certificate, a DNS name against its DNS entries.

A dialed DNS name is also sent as SNI by default. Redis itself serves
one
certificate and ignores SNI; this is for a TLS proxy or load balancer in
front
of it that routes by SNI or presents the certificate for that name.

Two opt-outs: `--no-verify-hostname` keeps the chain check but skips the
identity check; `--insecure` skips both, as before. Neither may be
combined
with an explicit expected name, as it could not be honored. A rejected
certificate produces a message naming the identity that was expected and
these remedies, instead of OpenSSL's `certificate verify failed`.

## Sentinel

New option `sentinel tls-verify-hostnames yes|no` (default `yes`;
changeable
at runtime with `SENTINEL CONFIG SET`, applying to links made
afterwards).
Sentinel verifies the address it knows an instance by, and sends it as
the
server name when it is a DNS name. The option governs only that address
check: when `tls-expected-peer-name` is configured, that name is
verified
instead, on every link, whatever the option says. A mismatch is logged
as a
WARNING event, `-tls-identity-mismatch`, that names the expected
identity and
the remedies; a link failing at DEBUG level only would otherwise leave a
Sentinel that lost every link with nothing visible in its log.

## Impact on existing deployments

This is a breaking change for TLS users of these tools, to be listed as
such
in the release notes. Affected are `redis-cli` and `redis-benchmark` run
with
`--tls`, and Sentinel with `tls-replication yes`; plaintext connections
are
untouched.

Certificates must now carry the identity these tools dial. Most Redis
deployments connect by IP — Sentinel always does unless
`resolve-hostnames`
is enabled — and an IP is matched only against the certificate's `IP
Address`
SAN entries: DNS entries are not consulted and there is no fallback to
the
Common Name. A certificate whose SAN lists only DNS names, or that has
no SAN
and relies on its Common Name, is therefore rejected when the tool
connects by
IP, even though it is accepted when the tool connects by a DNS name it
carries. Operators must reissue certificates with the IP in the SAN, pin
a
shared DNS name with `tls-expected-peer-name`, or set the opt-out
`--no-verify-hostname` for `redis-cli` and `redis-benchmark`, 
`sentinel tls-verify-hostnames no` for Sentinel.

`tls-expected-peer-name` now refuses IP addresses, from the config file
and
from `CONFIG SET`. This also affects replication, the cluster bus and
`MIGRATE`. Such a value never matched a certificate, so any deployment
that
had one was already failing every connection with only `certificate
verify failed`
to show for it; it now fails to load with a message that says why.

The test certificates change accordingly: `server.crt` now carries
`127.0.0.1`, `::1` and `localhost`, so `utils/gen-test-certs.sh` must be
re-run; `redis.crt` keeps no identity on purpose, as the fixture for the
negative tests.

## Unchanged on purpose

Connections between servers (replication, cluster bus, `MIGRATE`) keep
the
existing behaviour: chain check by default, identity only with
`tls-expected-peer-name`. Their peer addresses are learned from other
nodes,
so pinning a configured name fits them better than trusting the dialed
address. The only change on that path is the IP rejection above. The
bundled
hiredis library is also untouched; these tools do not use its TLS
context
API.

## Also fixed

As the new failure mode exposed them:

- `redis-benchmark` looped forever on a fatal TLS error on a
non-blocking
  connection (the error was reported as "try again").
- `redis-cli` printed a TLS failure twice because its silent probe
connection
  did not honor the quiet flag.
- Sentinel never retried its Pub/Sub link after a TLS setup failure: the
dead
  context stayed in place and counted as connected, so the instance lost
Hello traffic until restart. The command link already closed its context
in
  that case; the Pub/Sub link now does the same.

## Build

The check uses OpenSSL's `X509_VERIFY_PARAM` API (OpenSSL 1.0.2+). The
availability test and the one function that binds an expected identity
to a
connection (`tlsVerifyParamAddIdentity()`) live in the new
`src/tlshelpers.h`,
shared by `tls.c`, `sentinel.c` and the tools, so OpenSSL's subtleties —
IP
versus DNS matching, explicit lengths for BoringSSL, wildcard flags —
are in
one place. Building against an older OpenSSL fails unless
`TLS_NO_PEER_NAME_VERIFICATION` is defined, which compiles the check
out; the
tools then verify the chain only and say so once, and an explicit
expected
name is refused rather than ignored.

## Tests

- `tests/unit/tls.tcl`: the default by IP and by DNS name, `--sni`,
expected-name precedence, rejection of a trusted certificate lacking the
address, both opt-outs, invalid and conflicting options, the check
running
before `AUTH` is sent, `redis-benchmark`, and the config-time rejection
of
  IP addresses in `tls-expected-peer-name`.
- `tests/sentinel/tests/17-tls-verify-hostnames.tcl`: the default,
rejection
and its WARNING event, the runtime opt-out and its persistence, and the
  `tls-expected-peer-name` override.
- `tests/unit/cluster/tls-peer-impersonation.tcl`: a positive control
that
replays the forged FAIL through a certificate carrying the cluster SAN
and
  sees it processed, so the negative test's log-absence assertion is
  meaningful.
2026-10-26 14:40:46 +02:00
AditSingh10 8bf7cb9e28 Retire the legacy cluster test runner
Nothing sources the old harness now that all 28 tests run under the normal
framework, so remove it and everything that pointed at it:

- tests/cluster/ and the runtest-cluster entry point.
- the test-cluster target in src/Makefile, the runtest-cluster call in
  utils/releasetools/03_test_release.sh, and the runtest-cluster lines in
  TLS.md.
- the "cluster tests" steps and the test-macos-latest-cluster job in
  .github/workflows/daily.yml. Those jobs already run ./runtest, which now
  covers unit/cluster because it is listed in test_dirs.

Add a --cluster option to runtest, ported from valkey-io/valkey#1052, so a run
can be limited to tests/unit/cluster without naming each file. The cluster
tests still run as part of a plain ./runtest; this only makes it possible to
run them on their own.

test-ubuntu-io-threads and the client-compression job run ./runtest with a
positive --tags filter that skips the cluster tests, so they were the two jobs
where deleting the step lost coverage. I tried to keep it there with
./runtest --cluster, but the cluster directory holds 46 files where
runtest-cluster only ever ran 28, and several of the 18 extra ones are not
stable in those two configurations: across four dispatched Daily runs, three
different tests failed in unit/cluster/misc.tcl and
unit/cluster/atomic-slot-migration.tcl, each passing in isolation. The legacy
runner also ran the suite sequentially against one shared pool, where ./runtest
runs 16 clients each starting their own cluster, so the load is not comparable.
Rather than have this PR introduce that flakiness, those two steps are dropped
along with the rest, and stabilising those files under io-threads and
compression is left as separate work.

Since the cluster cases now run inside ./runtest, a 'cluster' entry in
skiptests would only have reached those two steps and left every other job
running them, so it is gone.

Kept on purpose: tests/instances.tcl, still the sentinel harness, and
tests/support/cluster.tcl, which cluster_util.tcl loads for shared cluster
client helpers.
2026-09-24 19:30:04 -05:00
hristostaykov-delandOzan Tezcan b78a80bd1f Make an unauthenticated cluster bus port an explicit choice (#15722)
## Make an unauthenticated cluster bus port an explicit choice

### Problem

The cluster bus protocol has no authentication of its own: any host that
can reach a node's cluster bus port can speak the protocol and so
threaten the whole cluster. What authenticates the bus is `tls-cluster`,
which is disabled by default — so until it is enabled, the only thing
standing between the cluster and an arbitrary host is whatever the
operator has done to keep the cluster bus port inaccessible to untrusted
hosts (firewall, VPN, private network), and nothing in Redis says so.

### What this PR does

Introduces `cluster-bus-port-protected-mode`, a boolean defaulting to
**yes**. A cluster node now refuses to start unless its bus port is
authenticated; running an unauthenticated one becomes something the
operator opts into, in the spirit of `protected-mode`.

| `cluster-enabled` | `cluster-bus-port-protected-mode` | `tls-cluster`
| Result |
|---|---|---|---|
| no | yes (default) | no | starts normally — no cluster bus port exists
|
| yes | yes (default) | yes | starts, unchanged from today |
| yes | yes (default) | no | **refuses to start**, with an error naming
both ways out |
| yes | no | no | starts, and logs a warning that the bus port is
unauthenticated |

The refusal message:

> cluster-bus-port-protected-mode is enabled but tls-cluster is
disabled, which leaves the cluster bus port of this node
unauthenticated: any host able to reach the port can join the cluster
and speak the cluster protocol, and threaten the whole cluster. Either
set 'tls-cluster yes', which makes every cluster bus peer present a
certificate verified against your CA, or set
'cluster-bus-port-protected-mode no' to acknowledge that the cluster bus
port is reachable by trusted hosts only, for instance because a firewall
blocks it.

### ⚠️ Breaking change

Non-TLS cluster deployments will not start after upgrading until they
add `cluster-bus-port-protected-mode no`. This is deliberate and
unavoidable: a default that is safe for new deployments cannot also be
silent for existing ones. The failure is loud, immediate (before any
listener is opened or pid file written), and the message states both
remedies.

**Upgrade path for a cluster on a trusted network:** add
`cluster-bus-port-protected-mode no` to `redis.conf`, or pass
`--cluster-bus-port-protected-mode no`. No data or topology
implications.

### Design notes

Two decisions worth a reviewer's attention:

**Protection is gated on cluster mode.** Only a cluster node opens a bus
port, so the enforced invariant is `cluster_enabled &&
cluster_bus_port_protected_mode ⟹ tls_cluster`. A standalone instance is
unaffected regardless of `tls-cluster` — otherwise the new default would
refuse to start every non-TLS Redis in existence, not just clusters. The
gate is safe to rely on because `cluster-enabled` is `IMMUTABLE_CONFIG`
(no assignment to `server.cluster_enabled` exists outside the config
system), so it cannot flip at runtime; there is a test pinning that.

Testing `tls-cluster` alone is sound because it does not depend on
`tls-auth-clients`, which governs the client port only: the bus has
dedicated dial/accept paths, and `clusterAcceptHandler()` hardcodes
`TLS_CLIENT_AUTH_YES`, so a peer certificate verified against the CA —
which `tls-cluster` makes mandatory — is required in both directions
regardless of how client authentication is configured. A test pins this
by running a cluster with `tls-auth-clients no` and requiring the two
ports to disagree: a certificate-less peer is served on the client port
and refused on the bus.

**Runtime enforcement lives in the `apply` callbacks, not the setters.**
`configSetCommand()` stores every value before running the first apply
callback, so an apply-time check judges the pair of values the command
*produces*. That makes `CONFIG SET tls-cluster no
cluster-bus-port-protected-mode no` succeed in either argument order,
while `CONFIG SET tls-cluster no` alone is refused. A setter-time check
would have rejected the legitimate atomic waiver depending on argument
order. A refused set is also a true no-op, which takes more than putting
the check first: `configSetCommand()` restores the previous value and
calls every apply callback of the command again, so `applyTlsCluster()`
additionally short-circuits when `tls-cluster` did not effectively move
— otherwise a refusal would rebuild the `SSL_CTX` and, through
`clusterNotifyTopologyChanged()`, rebuild the SSL_CTX, discarding the
TLS session cache, and report a NODE topology change to modules for a
change that never happened. Same shadow-previous-value pattern as
`clusterUpdateMyselfIp()`.

The option is kept `MODIFIABLE_CONFIG`, like `protected-mode`. Making it
immutable would additionally have removed the existing ability to
`CONFIG SET tls-cluster no` without a restart, which would be a second
breaking change.

**Startup check placement.** It runs in the post-load block of
`loadServerConfigFromString()` (the `config_load_depth == 0` guard), so
it observes the *final* configuration: main config file, `include`-d
files and command-line arguments alike. Covered by the "protection is
judged after the whole configuration is loaded" test, which drives
`src/redis-server` against a hand-written `include` pair in both
directions — `start_server` cannot express this, as it generates the
config file itself.

**Logging.** A node whose bus port is unauthenticated because protection
was waived says so once at startup, and again whenever the bus loses its
authentication at runtime (any such transition necessarily changes
`tls-cluster`, so the `tls-cluster` apply callback is the single place
that needs to report it). This mirrors how Redis warns about
`protected-mode` / a passwordless default user.

### Tests

New `tests/unit/cluster/cluster-bus-port-protected-mode.tcl`, 10 tests
(4 of them TLS-only), green in
both non-TLS and `--tls` runs:

- the shipped default is `yes` and refuses an unauthenticated bus — run
with no config file at all, so it observes the compiled-in default
rather than anything the suite configures
- protection is judged only once the whole configuration is loaded: both
`cluster-enabled` and the waiver are honoured when they reach the server
through an `include`, which an included file's early end would otherwise
hide
- protection does not apply outside cluster mode, both options stay
freely settable there, and `cluster-enabled` is still immutable so the
cluster-mode gate cannot be opened at runtime
- waiving protection starts a cluster node with an unauthenticated bus
- the waived requirement is reported in the log
- `CONFIG SET cluster-bus-port-protected-mode yes` is refused while the
bus is unauthenticated
- with `tls-cluster` on, a cluster node starts with its bus
authenticated and logs no advisory
- `CONFIG SET tls-cluster no` is refused while protected mode is enabled
- a single `CONFIG SET` can un-authenticate the bus in either argument
order, and put it back
- each option can also be changed on its own, in the safe order —
waiving protection before disabling `tls-cluster`, and authenticating
the bus before enabling protection, since the check judges only the
state each call produces

New `tests/unit/cluster/cluster-bus-tls.tcl`, 3 TLS-only tests. These
cover what `tls-cluster`
verifies rather than the new option — the properties protected mode
relies on when it treats
`tls-cluster` alone as an authenticated bus port, which nothing pinned
before:

- a peer whose certificate does not chain to the CA is rejected on
accept, on the same port that accepts a CA-signed one — exactly one
rejection, so neither "everything is refused" nor "nothing is" can pass
- a peer whose server certificate does not chain to the CA is refused
when dialled, and never admitted to the cluster
- `tls-auth-clients no` does not relax the cluster bus: a
certificate-less peer is served on the client port, and refused on the
bus with `peer did not return a certificate`

`tests/unit/cluster/cluster-topology-change.tcl` gains two TLS-only
tests for the
`applyTlsCluster()` short-circuit described above, since it already
loads the module that
counts the notifications:

- a **rolled back** `CONFIG SET tls-cluster no tls-cert-file
/nonexistent` reports no NODE change — the trigger that predates this
PR, with `cluster-bus-port-protected-mode` explicitly out of the picture
- a **refused** `CONFIG SET tls-cluster no` reports no NODE change — the
trigger this PR adds

Both also assert that the tracked value is not left out of step with
`tls-cluster`, by driving
a real change afterwards and requiring it to still be reported. Both
were verified to fail
without the short-circuit (`Expected '11' to be equal to '12'`), so they
are not vacuous.

### Test infrastructure and tooling

The new default means anything that starts a cluster node with an
unauthenticated bus has to waive protection:

- `tests/assets/default.conf` — waived suite-wide; non-TLS runs have no
TLS to offer, so without this *every* cluster test fails to start. The
tests that cover the protection itself enable it explicitly. Chosen over
per-test overrides because the alternative silently breaks any future
test that adds `cluster-enabled yes`.
- `tests/cluster/run.tcl`, `.github/workflows/external.yml`,
`utils/create-cluster/create-cluster` — the three places that start
cluster nodes without going through `default.conf` (`git grep
cluster-enabled` confirms there are no others). `create-cluster` gets a
`BUS_PROTECTED_MODE` variable, overridable from `config.sh` like the
existing `PROTECTED_MODE`.

### Documentation

`redis.conf` gains a documented block in the **REDIS CLUSTER** section
explaining what the bus port exposes, what `tls-cluster` authenticates,
and when waiving is acceptable. `tls-auth-clients` gains a paragraph
recording that it governs the client port only and cannot weaken the
cluster bus, since that is the property the startup check relies on. The
**TLS** section's `tls-cluster` note is also rewritten — it described
the option as a way to encrypt the bus, and stated a default that a
cluster node no longer has, neither of which reflects what the option is
for.

`TLS.md` gains a **Cluster bus protection** section describing what the
cluster bus port exposes, what `tls-cluster` verifies in each direction,
that it is independent of `tls-auth-clients`, and the new default.
Previously the guide stated only that `--tls-cluster yes` makes Redis
Cluster use TLS across nodes.

The **Getting Started** section also now describes `tls-auth-clients`,
which the guide did not cover before although the `redis-cli` example
depends on it: clients on a TLS port are required to present a
certificate by default, `no` accepts none and `optional` validates one
only when offered, both apply to the client port only, and
`tls-auth-clients-user` maps a validated certificate's Common Name to a
Redis ACL user.

---------

Co-authored-by: Ozan Tezcan <ozantezcan@gmail.com>
2026-09-15 14:57:08 +03:00
hristostaykov-del e1d7d50f9c Document and warn about leading dots in tls-expected-peer-name (#15684)
Followup #15492

### Summary

A `tls-expected-peer-name` entry that begins with a dot (e.g.
`.example.com`) is not a host name — OpenSSL treats it as a *parent
domain* and
accepts any subdomain at any depth. That behaviour was undocumented.
This PR
documents it and logs a warning when such an entry is configured. There
is no
behaviour change and no new configuration option.

### Motivation

Verified against OpenSSL 3.0.13 with the flags Redis sets
(`X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS`), one SAN per test certificate:

| configured value | cert SAN `example.com` | `node.example.com` |
`a.b.example.com` |
|---|---|---|---|
| `example.com`    | match    | no match | no match |
| `.example.com`   | no match | match    | match    |
| `.com`           | match    | match    | match    |

Two properties are easy to miss: a leading-dot entry matches at
unlimited depth and
does **not** match the domain itself, and the scope follows from the
suffix alone —
`.com` accepts every name in that TLD, which under a shared or public CA
is close to
accepting any CA-signed certificate.

The pattern is legitimate and someone may want it, but a leading dot is
also easy to
introduce unintentionally. Documentation alone only helps a reader; a
log
line is also visible to someone who inherited the configuration.

### What this PR does

* Logs a single `LL_WARNING` naming how many entries begin with a dot:

tls-expected-peer-name has 2 entries beginning with a dot, which OpenSSL
matches as a parent domain: such an entry accepts any subdomain at any
depth
(for example ".example.com" also accepts a.b.example.com) and does not
accept
the domain itself. Use explicit host names if that is not intended.

It is emitted from the post-load config checks in
`loadServerConfigFromString()`
at startup, and from a new apply callback on `CONFIG SET`. The startup
location is
the same block that already carries config advisories (e.g. the
cluster-mode
`dbnum` warning); running after every directive is parsed makes the
message
independent of where the directive appears and ensures it reaches the
configured
  logfile. One line is emitted regardless of how many entries qualify.
* Documents the semantics in `TLS.md` and `redis.conf`, including the
`.com`
  breadth, and recommends explicit host names, a deeper parent
(`.dc1.example.com`, which still matches `node.dc1.example.com`), or
several
  space-separated names.
* Clarifies an adjacent point in the same docs: wildcards are matched
from the
**certificate**, not from this option. A certificate whose SAN is
`*.example.com`
matches a concrete configured name such as `node.example.com`;
configuring
`*.example.com` as the expected name does not perform wildcard matching.
The
  previous wording could be read as endorsing the latter.

### Backward compatibility

* No change to matching semantics, validation, or the accepted set of
values — a
  configuration that works today continues to work identically.
* The only observable difference is one additional `LL_WARNING` line
when a
  parent-domain entry is configured.
* The new apply callback only emits the advisory; it deliberately does
not
reconfigure TLS, since the option is not an input to the `SSL_CTX` and
is applied
  per connection.
* `src/tls.c` is untouched, so nothing changes for the TLS module build.
2026-08-28 10:39:35 +03:00
AliasJeff 78432ed8e4 Add tls-groups to control TLS named groups (#15556)
## Summary

- add a tls-groups configuration for TLS named group selection
- apply the option when creating or reconfiguring the OpenSSL TLS
context
- add matching --tls-groups support for redis-cli and redis-benchmark
when supported by the build

## Why

Fixes [#15502](https://github.com/redis/redis/issues/15502)

Redis already exposes TLS protocol, cipher, and ciphersuite
configuration, but there is no equivalent option for the OpenSSL
supported groups list. Some deployments need to control this list as
part of their TLS policy.

The implementation uses SSL_CTX_set1_groups_list when available and
falls back to SSL_CTX_set1_curves_list for older OpenSSL headers. If
neither API is available, Redis fails the TLS build by default so the
option is not silently ignored. Defining TLS_NO_GROUPS compiles the
feature out.

For redis-cli and redis-benchmark, --tls-groups follows the
--tls-ciphersuites behavior: the option is only shown and accepted when
the build supports OpenSSL named group configuration.

---------

Signed-off-by: AliasJeff <zhexunchen@gmail.com>
2026-08-20 22:45:16 +08:00
debing.sunandYuan Wang 3acc0c49cf update TLS.md to reflect current TLS support (#15497)
## Description

`TLS.md` has drifted out of date. This PR updates it to match the
current state of the code:

1. **Multi-threading I/O.** The doc claimed multi-threaded I/O is not
supported
   for TLS. This has been supported since Redis 8.0(async I/O threads
introduced in #13695)

2. **To-Do List removed.** Both remaining items were completed long ago:
   - redis-benchmark TLS support — done in #7959
   - redis-cli `--slave` / `--rdb` support — done in #7456

---------

Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2026-07-31 19:00:14 +08:00
hristostaykov-delanddebing.sun 58d0fb9c62 Add TLS peer certificate name (SAN) verification for server-to-server connections (#15492)
## Add TLS peer certificate name (SAN) verification for server-to-server
connections

### Summary

Adds an opt-in `tls-expected-peer-name` configuration that verifies the
**identity**
of the peer certificate, not just that it chains to the trusted CA on
Redis's
server-to-server TLS connections (replication, cluster bus, `MIGRATE`).

### Motivation

Today Redis TLS validates only that a peer certificate chains to
`tls-ca-cert-file`/`tls-ca-cert-dir`; it never checks that the
certificate belongs to
the specific peer being contacted (`connTLSConnect` sets SNI but never
calls
`SSL_set1_host`).

- Under a **dedicated per-cluster CA** (the recommended model) this is
fine - every
  cert the CA signs belongs to one of your nodes.
- Under a **shared, organizational, or public CA** it is not: any holder
of a
CA-signed certificate can impersonate a master/peer. On replication this
lets a
MITM capture the `AUTH <masteruser> <masterauth>` credentials a replica
sends. On
the cluster bus which has **no per-message authentication** (a message's
sender
is identified only by the public node ID in its header). Such a
certificate can
open an inbound bus connection and forge messages impersonating a real
member (e.g.
a `FAIL` marking a healthy node dead), with no handshake and no return
link
  required.

### What this PR does

Introduces `tls-expected-peer-name` (string, default unset, `CONFIG
SET`-able). When
set, the peer certificate must additionally carry one of the configured
names in its
SAN (CN only as a fallback), verified as part of chain validation. The
expected name
comes from **local configuration only**, never from the dialed address
or any data
received over the wire, which is what makes it a valid trust anchor.

Enforced in **both directions**, sharing a single helper
(`tlsSetVerifyName`):

- **Outbound** - in `connTLSConnect`, the peer's *server* certificate is
verified when
a node dials a master, cluster peer, or `MIGRATE` target. A single choke
point
  covers all three.
- **Inbound cluster bus** - a new `set_verify_name` connection-type
method
  (`connSetVerifyName` wrapper; no-op for non-TLS) is invoked from
`clusterAcceptHandler` before the handshake, verifying the *connecting*
peer's
*client* certificate. This is what actually blocks the node-ID
impersonation/forgery
above, since the victim never dials the attacker and so outbound
verification cannot
  help.

A space-separated list of names is supported (first via
`X509_VERIFY_PARAM_set1_host`,
rest via `X509_VERIFY_PARAM_add1_host` - the `X509_VERIFY_PARAM_*` host
API rather than
the `SSL_set1_host`/`SSL_add1_host` wrappers, so the feature is
available on every
OpenSSL that provides the host-checking machinery, not just 1.1.0+);
partial-label
wildcards are rejected (`X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS`). The
value is tokenized
per connection; the config layer validates it (rejecting an empty value
only as the
"disable" case, and rejecting whitespace-only or any non-space
whitespace, which could
never match a real hostname and would otherwise be handed to OpenSSL
verbatim).

### OpenSSL version requirement and the `TLS_NO_PEER_NAME_VERIFICATION`
build flag

Name verification uses the OpenSSL `X509_VERIFY_PARAM` host API,
available since
**OpenSSL 1.0.2**. To make this explicit rather than a silent
degradation:

- Building the TLS support against **OpenSSL < 1.0.2 is a hard compile
error** by
default, with a message pointing to the opt-out flag. This prevents a
build from
  quietly shipping without the check.
- Defining **`TLS_NO_PEER_NAME_VERIFICATION`** (e.g.
`make BUILD_TLS=yes CFLAGS=-DTLS_NO_PEER_NAME_VERIFICATION`) compiles
the feature out.
It is both the required opt-out for building against an older OpenSSL
**and** a way to
  disable the feature on any OpenSSL version. When compiled out, setting
`tls-expected-peer-name` is accepted but each affected connection logs a
warning and
proceeds without the name check (CA chain validation still applies).
Documented in
  `TLS.md`.

### Operational note (important for reviewers/operators)

Because a node's outbound (client) certificate is now verified by its
peers on accept,
the identity SAN must be present on **whatever certificate the node
presents in both
roles** i.e. on `tls-client-cert-file` as well as `tls-cert-file` (when
no separate
client cert is set, `tls-cert-file` is used for both and needs the SAN
once). If a
separate SAN-less client cert is configured, peers will reject the
node's inbound bus
connections and the cluster will not form. This is documented in
`TLS.md`/`redis.conf`.

### Backward compatibility

- Default unset ⇒ behavior identical to today; no existing deployment is
affected.
- The SAN check runs only when all of the following hold:
`tls-expected-peer-name` is
configured, peer verification is active, and a certificate is presented
- it is a
refinement layered on top of the existing CA verification, not a
replacement.
- We do **not** change the cluster bus's existing TLS enforcement: it
already mandates
mutual TLS (client certificates are required), and that is unchanged.
The accept-side
  SAN check is only added on top, and only when a name is configured.
- The generic client (data-port) listener is intentionally **not**
affected; client
  identity/authorization there remains `AUTH`/ACL (and optionally
  `tls-auth-clients-user`).
- Build-time: TLS support now requires OpenSSL >= 1.0.2 by default (a
build against an
older OpenSSL fails with a clear error). This only affects building
against old
  OpenSSL versions (1.0.1), and such builds remain possible via
  `TLS_NO_PEER_NAME_VERIFICATION`.

### Testing

- `tests/unit/tls.tcl`: functional coverage over replication - matching
SAN,
mismatching SAN (rejected), multiple listed names (2nd matches), runtime
`CONFIG SET`,
enforcement on `MIGRATE` (blocking connect), set-and-clear, and
config-time rejection
  of whitespace-only / embedded-whitespace values.
- `tests/unit/cluster/tls-peer-impersonation.tcl`: end-to-end cluster
test that (a)
confirms a cluster still forms with the check active in both directions
and (b)
forges a `FAIL` bus packet from a CA-signed-but-SAN-less certificate and
asserts it
is rejected (the victim logs the accept-side rejection, the packet is
not processed,
  the target is not flagged failed, and the victim stays responsive).
- `utils/gen-test-certs.sh`: adds `san.{crt,key}` carrying
  `subjectAltName = DNS:redis.local, DNS:cluster.local`.
- Verified passing with both `BUILD_TLS=yes` and `BUILD_TLS=module`
(`--tls` and
  `--tls-module`); existing TLS and cluster tests unaffected.
- Verified the `TLS_NO_PEER_NAME_VERIFICATION` opt-out builds and runs
(feature compiled
out, warn-and-proceed), and that OpenSSL 1.0.1 still compiles with the
flag (gcc-13 /
  Ubuntu 24.04).

### Possible future work

- A config parameter to control how the certificate name check is set up
at the SSL
  layer e.g. forwarding OpenSSL verification flags such as
`X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT` (today the CN is consulted only
as a fallback
  when the certificate has no DNS SAN entries).
- Certificate revocation checking (CRL/OCSP): Redis performs no
revocation checking
  today, which is something we may want to consider adding.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-07-29 11:23:38 +03:00
Oran Agraandzhenwei pi 4faddf18ca Build TLS as a loadable module
* Support BUILD_TLS=module to be loaded as a module via config file or
  command line. e.g. redis-server --loadmodule redis-tls.so
* Updates to redismodule.h to allow it to be used side by side with
  server.h by defining REDISMODULE_CORE_MODULE
* Changes to server.h, redismodule.h and module.c to avoid repeated
  type declarations (gcc 4.8 doesn't like these)
* Add a mechanism for non-ABI neutral modules (ones who include
  server.h) to refuse loading if they detect not being built together with
  redis (release.c)
* Fix wrong signature of RedisModuleDefragFunc, this could break
  compilation of a module, but not the ABI
* Move initialization of listeners in server.c to be after loading
  the modules
* Config TLS after initialization of listeners
* Init cluster after initialization of listeners
* Add TLS module to CI
* Fix a test suite race conditions:
  Now that the listeners are initialized later, it's not sufficient to
  wait for the PID message in the log, we need to wait for the "Server
  Initialized" message.
* Fix issues with moduleconfigs test as a result from start_server
  waiting for "Server Initialized"
* Fix issues with modules/infra test as a result of an additional module
  present

Notes about Sentinel:
Sentinel can't really rely on the tls module, since it uses hiredis to
initiate connections and depends on OpenSSL (won't be able to use any
other connection modules for that), so it was decided that when TLS is
built as a module, sentinel does not support TLS at all.
This means that it keeps using redis_tls_ctx and redis_tls_client_ctx directly.

Example code of config in redis-tls.so(may be use in the future):
RedisModuleString *tls_cfg = NULL;

void tlsInfo(RedisModuleInfoCtx *ctx, int for_crash_report) {
    UNUSED(for_crash_report);
    RedisModule_InfoAddSection(ctx, "");
    RedisModule_InfoAddFieldLongLong(ctx, "var", 42);
}

int tlsCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
{
    if (argc != 2) return RedisModule_WrongArity(ctx);
    return RedisModule_ReplyWithString(ctx, argv[1]);
}

RedisModuleString *getStringConfigCommand(const char *name, void *privdata) {
    REDISMODULE_NOT_USED(name);
    REDISMODULE_NOT_USED(privdata);
    return tls_cfg;
}

int setStringConfigCommand(const char *name, RedisModuleString *new, void *privdata, RedisModuleString **err) {
    REDISMODULE_NOT_USED(name);
    REDISMODULE_NOT_USED(err);
    REDISMODULE_NOT_USED(privdata);
    if (tls_cfg) RedisModule_FreeString(NULL, tls_cfg);
    RedisModule_RetainString(NULL, new);
    tls_cfg = new;
    return REDISMODULE_OK;
}

int RedisModule_OnLoad(void *ctx, RedisModuleString **argv, int argc)
{
    ....
    if (RedisModule_CreateCommand(ctx,"tls",tlsCommand,"",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_RegisterStringConfig(ctx, "cfg", "", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) {
        if (tls_cfg) {
            RedisModule_FreeString(ctx, tls_cfg);
            tls_cfg = NULL;
        }
        return REDISMODULE_ERR;
    }
    ...
}

Co-authored-by: zhenwei pi <pizhenwei@bytedance.com>
Signed-off-by: zhenwei pi <pizhenwei@bytedance.com>
2022-08-23 12:37:56 +03:00
Yossi Gottlieb 3e6f2b1a45 TLS: Session caching configuration support. (#7420)
* TLS: Session caching configuration support.
* TLS: Remove redundant config initialization.
2020-07-10 11:33:47 +03:00
Yossi Gottlieb bb3d45a386 TLS: Update documentation. 2020-02-05 21:19:03 +02:00
Yossi Gottlieb 61733ded14 TLS: Configuration options.
Add configuration options for TLS protocol versions, ciphers/cipher
suites selection, etc.
2019-10-07 21:07:27 +03:00
Oran Agra 6b6294807c TLS: Implement support for write barrier. 2019-10-07 21:06:30 +03:00
Oran Agra 5a47794606 diskless replication rdb transfer uses pipe, and writes to sockets form the parent process.
misc:
- handle SSL_has_pending by iterating though these in beforeSleep, and setting timeout of 0 to aeProcessEvents
- fix issue with epoll signaling EPOLLHUP and EPOLLERR only to the write handlers. (needed to detect the rdb pipe was closed)
- add key-load-delay config for testing
- trim connShutdown which is no longer needed
- rioFdsetWrite -> rioFdWrite - simplified since there's no longer need to write to multiple FDs
- don't detect rdb child exited (don't call wait3) until we detect the pipe is closed
- Cleanup bad optimization from rio.c, add another one
2019-10-07 21:06:30 +03:00
Yossi Gottlieb b087dd1db6 TLS: Connections refactoring and TLS support.
* Introduce a connection abstraction layer for all socket operations and
integrate it across the code base.
* Provide an optional TLS connections implementation based on OpenSSL.
* Pull a newer version of hiredis with TLS support.
* Tests, redis-cli updates for TLS support.
2019-10-07 21:06:13 +03:00