mirror of
https://github.com/redis/redis.git
synced 2026-10-28 08:25:04 +08:00
unstable
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
3e6f2b1a45 |
TLS: Session caching configuration support. (#7420)
* TLS: Session caching configuration support. * TLS: Remove redundant config initialization. |
||
|
|
bb3d45a386 | TLS: Update documentation. | ||
|
|
61733ded14 |
TLS: Configuration options.
Add configuration options for TLS protocol versions, ciphers/cipher suites selection, etc. |
||
|
|
6b6294807c | TLS: Implement support for write barrier. | ||
|
|
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 |
||
|
|
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. |