You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2026/10/16 04:03:06 UTC
(superset) branch master updated: fix(websocket): support Sentinel and full TLS options for Redis/Valkey (#45355)
This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 1aaabd8e435 fix(websocket): support Sentinel and full TLS options for Redis/Valkey (#45355)
1aaabd8e435 is described below
commit 1aaabd8e4352cfcad448b5734831075e22a96b9d
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Thu Oct 15 21:02:59 2026 -0700
fix(websocket): support Sentinel and full TLS options for Redis/Valkey (#45355)
---
superset-websocket/README.md | 25 +++++
superset-websocket/config.example.json | 10 +-
superset-websocket/spec/config.test.ts | 42 +++++++++
superset-websocket/spec/index.test.ts | 161 ++++++++++++++++++++++++++-------
superset-websocket/src/config.ts | 75 +++++++++++++++
superset-websocket/src/index.ts | 93 +++++++++++++++----
6 files changed, 352 insertions(+), 54 deletions(-)
diff --git a/superset-websocket/README.md b/superset-websocket/README.md
index 844becec080..bf1dafc4d52 100644
--- a/superset-websocket/README.md
+++ b/superset-websocket/README.md
@@ -258,6 +258,31 @@ when `REALTIME_CHANNEL_PREFIX=tenant-a:`. Set a per-deployment prefix to keep
deployments that share one Redis/Valkey from cross-delivering realtime nudges
(Redis pub/sub is not scoped by DB number).
+### Redis / Valkey connection modes
+
+The `redis` object (or the `REDIS_*` environment variables) supports the same
+modes as the Python coordination backend: a standalone server, or Redis/Sentinel,
+each optionally over TLS. Redis Cluster is not supported by either side.
+
+| Setting | Environment variable | Notes |
+| -------------------------------- | -------------------------- | --------------------------------------------------------------------- |
+| `host`, `port`, `db` | `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB` | Standalone server (ignored in Sentinel mode) |
+| `username`, `password` | `REDIS_USERNAME`, `REDIS_PASSWORD` | Authenticate against the (resolved) master |
+| `ssl` | `REDIS_SSL` | Enable TLS to the server |
+| `sslCaCerts` | `REDIS_SSL_CA_CERTS` | Path to a PEM CA bundle, trusted **in addition to** the public CAs |
+| `sslCertfile`, `sslKeyfile` | `REDIS_SSL_CERTFILE`, `REDIS_SSL_KEYFILE` | Client certificate and key (mutual TLS) |
+| `sslCertReqs` | `REDIS_SSL_CERT_REQS` | `required` (default), `optional` or `none`; only `none` skips verification |
+| `validateHostname` | `REDIS_VALIDATE_HOSTNAME` | Check the server certificate's hostname (default `true`) |
+| `sentinels` | `REDIS_SENTINELS` | Enables Sentinel mode. JSON: `[{"host": "s1", "port": 26379}]`; env: `s1:26379,s2:26379` |
+| `sentinelMaster` | `REDIS_SENTINEL_MASTER` | Master name; required when `sentinels` is set |
+| `sentinelPassword` | `REDIS_SENTINEL_PASSWORD` | Password for the sentinel nodes themselves |
+
+With no `sslCaCerts`, the server certificate is validated against the public CA
+roots bundled with Node, so a certificate from a public CA works with just
+`ssl: true`. In Sentinel mode TLS applies to the resolved master; the sentinel
+nodes are contacted in plaintext, matching the Python backend. The server follows
+failovers by re-resolving the master through the sentinels.
+
## StatsD monitoring
The application is tracking a couple of metrics with `statsd` using the [hot-shots](https://www.npmjs.com/package/hot-shots) library, such as the number of connected clients and the number of failed attempts to send a message to a client.
diff --git a/superset-websocket/config.example.json b/superset-websocket/config.example.json
index bab9f6c6d59..aeb961f4a7e 100644
--- a/superset-websocket/config.example.json
+++ b/superset-websocket/config.example.json
@@ -13,7 +13,15 @@
"host": "127.0.0.1",
"password": "",
"db": 0,
- "ssl": false
+ "ssl": false,
+ "validateHostname": true,
+ "sslCaCerts": "",
+ "sslCertfile": "",
+ "sslKeyfile": "",
+ "sslCertReqs": "required",
+ "sentinels": [],
+ "sentinelMaster": "",
+ "sentinelPassword": ""
},
"jwtAlgorithms": ["HS256"],
"jwtSecret": "CHANGE-ME",
diff --git a/superset-websocket/spec/config.test.ts b/superset-websocket/spec/config.test.ts
index 40e161da73c..c1de1d77b5b 100644
--- a/superset-websocket/spec/config.test.ts
+++ b/superset-websocket/spec/config.test.ts
@@ -33,6 +33,10 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
expect(config.redis.password).toEqual('some pwd');
expect(config.redis.db).toEqual(10);
expect(config.redis.ssl).toEqual(false);
+ expect(config.redis.validateHostname).toEqual(true);
+ expect(config.redis.sslCertReqs).toEqual('required');
+ expect(config.redis.sslCaCerts).toEqual('');
+ expect(config.redis.sentinels).toEqual([]);
expect(config.statsd.host).toEqual('127.0.0.1');
expect(config.statsd.port).toEqual(8125);
expect(config.statsd.globalTags).toEqual([]);
@@ -45,6 +49,15 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
process.env.REDIS_PASSWORD = 'admin';
process.env.REDIS_DB = '4';
process.env.REDIS_SSL = 'true';
+ process.env.REDIS_VALIDATE_HOSTNAME = 'false';
+ process.env.REDIS_SSL_CA_CERTS = '/certs/ca.pem';
+ process.env.REDIS_SSL_CERTFILE = '/certs/client.pem';
+ process.env.REDIS_SSL_KEYFILE = '/certs/client.key';
+ process.env.REDIS_SSL_CERT_REQS = 'NONE';
+ process.env.REDIS_SENTINELS =
+ 's1:26380, s2 ,[::1]:26381,bad:port,toobig:70000';
+ process.env.REDIS_SENTINEL_MASTER = 'mymaster';
+ process.env.REDIS_SENTINEL_PASSWORD = 'sentinel-pwd';
process.env.STATSD_HOST = '15.15.15.15';
process.env.STATSD_PORT = '8000';
process.env.STATSD_GLOBAL_TAGS = 'tag-1,tag-2';
@@ -59,6 +72,18 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
expect(config.redis.password).toEqual('admin');
expect(config.redis.db).toEqual(4);
expect(config.redis.ssl).toEqual(true);
+ expect(config.redis.validateHostname).toEqual(false);
+ expect(config.redis.sslCaCerts).toEqual('/certs/ca.pem');
+ expect(config.redis.sslCertfile).toEqual('/certs/client.pem');
+ expect(config.redis.sslKeyfile).toEqual('/certs/client.key');
+ expect(config.redis.sslCertReqs).toEqual('none');
+ expect(config.redis.sentinels).toEqual([
+ { host: 's1', port: 26380 },
+ { host: 's2', port: 26379 },
+ { host: '::1', port: 26381 },
+ ]);
+ expect(config.redis.sentinelMaster).toEqual('mymaster');
+ expect(config.redis.sentinelPassword).toEqual('sentinel-pwd');
expect(config.statsd.host).toEqual('15.15.15.15');
expect(config.statsd.port).toEqual(8000);
expect(config.statsd.globalTags).toEqual(['tag-1', 'tag-2']);
@@ -71,6 +96,14 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
delete process.env.REDIS_PASSWORD;
delete process.env.REDIS_DB;
delete process.env.REDIS_SSL;
+ delete process.env.REDIS_VALIDATE_HOSTNAME;
+ delete process.env.REDIS_SSL_CA_CERTS;
+ delete process.env.REDIS_SSL_CERTFILE;
+ delete process.env.REDIS_SSL_KEYFILE;
+ delete process.env.REDIS_SSL_CERT_REQS;
+ delete process.env.REDIS_SENTINELS;
+ delete process.env.REDIS_SENTINEL_MASTER;
+ delete process.env.REDIS_SENTINEL_PASSWORD;
delete process.env.STATSD_HOST;
delete process.env.STATSD_PORT;
delete process.env.STATSD_GLOBAL_TAGS;
@@ -104,3 +137,12 @@ test('buildConfig() falls back to the default for a malformed numeric override',
delete process.env.SOCKET_RESPONSE_TIMEOUT_MS;
delete process.env.MAX_CONNECTIONS_PER_CHANNEL;
});
+
+test('buildConfig() falls back to the default for an invalid REDIS_SSL_CERT_REQS', () => {
+ process.env.REDIS_SSL_CERT_REQS = 'sometimes';
+
+ // A typo must not silently weaken verification.
+ expect(buildConfig().redis.sslCertReqs).toEqual('required');
+
+ delete process.env.REDIS_SSL_CERT_REQS;
+});
diff --git a/superset-websocket/spec/index.test.ts b/superset-websocket/spec/index.test.ts
index f45c878123d..db376996638 100644
--- a/superset-websocket/spec/index.test.ts
+++ b/superset-websocket/spec/index.test.ts
@@ -30,8 +30,13 @@ import {
} from 'vitest';
import * as http from 'http';
import * as net from 'net';
+import { mkdtempSync, rmSync, writeFileSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+import { ConnectionOptions, rootCertificates } from 'tls';
import { WebSocket } from 'ws';
import * as server from '../src/index';
+import type { RedisConfig } from '../src/config';
import { statsd } from '../src/index';
const { mockSubscribe, mockOn } = vi.hoisted(() => {
@@ -226,31 +231,44 @@ describe('server', () => {
});
});
- describe('redisUrlFromConfig', () => {
- test('it builds a valid Redis URL from defaults', () => {
- expect(
- server.buildRedisOpts({
- port: 6379,
- host: '127.0.0.1',
- username: 'test-user',
- password: '',
- db: 0,
- ssl: false,
- validateHostname: false,
- }),
- ).toEqual({ db: 0, host: '127.0.0.1', port: 6379 });
+ describe('buildRedisOpts', () => {
+ const redisConfig = (
+ overrides: Partial<RedisConfig> = {},
+ ): RedisConfig => ({
+ port: 6379,
+ host: '127.0.0.1',
+ username: 'cool-user',
+ password: '',
+ db: 0,
+ ssl: false,
+ validateHostname: false,
+ sslCaCerts: '',
+ sslCertfile: '',
+ sslKeyfile: '',
+ sslCertReqs: 'required',
+ sentinels: [],
+ sentinelMaster: '',
+ sentinelPassword: '',
+ ...overrides,
+ });
+
+ test('it builds standalone options from defaults', () => {
+ expect(server.buildRedisOpts(redisConfig())).toEqual({
+ db: 0,
+ host: '127.0.0.1',
+ port: 6379,
+ });
});
- test('it builds a valid Redis URL with a password', () => {
+ test('it builds standalone options with a password', () => {
expect(
- server.buildRedisOpts({
- port: 6380,
- host: 'redis.local',
- username: 'cool-user',
- password: 'foo',
- db: 1,
- ssl: false,
- validateHostname: false,
- }),
+ server.buildRedisOpts(
+ redisConfig({
+ port: 6380,
+ host: 'redis.local',
+ password: 'foo',
+ db: 1,
+ }),
+ ),
).toEqual({
db: 1,
host: 'redis.local',
@@ -259,24 +277,97 @@ describe('server', () => {
username: 'cool-user',
});
});
- test('it builds a valid Redis URL with SSL', () => {
- expect(
- server.buildRedisOpts({
- port: 6379,
- host: '127.0.0.1',
- password: '',
- username: 'cool-user',
- db: 0,
- ssl: true,
- validateHostname: false,
- }),
- ).toEqual({
+ test('it relies on the default trust store for TLS', () => {
+ expect(server.buildRedisOpts(redisConfig({ ssl: true }))).toEqual({
db: 0,
host: '127.0.0.1',
port: 6379,
tls: { checkServerIdentity: expect.anything() },
});
});
+ test('it disables verification only for ssl cert reqs "none"', () => {
+ const tlsFor = (sslCertReqs: RedisConfig['sslCertReqs']) =>
+ server.buildRedisOpts(redisConfig({ ssl: true, sslCertReqs })).tls;
+ expect(tlsFor('none')).toMatchObject({ rejectUnauthorized: false });
+ expect(tlsFor('required')).not.toHaveProperty('rejectUnauthorized');
+ expect(tlsFor('optional')).not.toHaveProperty('rejectUnauthorized');
+ });
+ test('it trusts a custom CA alongside the public roots, and loads client certs', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'ws-redis-tls-'));
+ try {
+ const write = (name: string, body: string) => {
+ const path = join(dir, name);
+ writeFileSync(path, body);
+ return path;
+ };
+ const opts = server.buildRedisOpts(
+ redisConfig({
+ ssl: true,
+ sslCaCerts: write('ca.pem', 'CUSTOM-CA'),
+ sslCertfile: write('client.pem', 'CLIENT-CERT'),
+ sslKeyfile: write('client.key', 'CLIENT-KEY'),
+ }),
+ );
+ const tls = opts.tls as ConnectionOptions;
+ expect(tls.ca).toEqual([...rootCertificates, 'CUSTOM-CA']);
+ expect(tls.cert).toEqual('CLIENT-CERT');
+ expect(tls.key).toEqual('CLIENT-KEY');
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+ test('it fails fast when a configured cert file is missing', () => {
+ expect(() =>
+ server.buildRedisOpts(
+ redisConfig({ ssl: true, sslCaCerts: '/nonexistent/ca.pem' }),
+ ),
+ ).toThrow(/ca\.pem/);
+ });
+ test('it builds Sentinel options in place of host/port', () => {
+ expect(
+ server.buildRedisOpts(
+ redisConfig({
+ password: 'master-pwd',
+ sentinels: [
+ { host: 's1', port: 26379 },
+ { host: 's2', port: 26380 },
+ ],
+ sentinelMaster: 'mymaster',
+ sentinelPassword: 'sentinel-pwd',
+ }),
+ ),
+ ).toEqual({
+ db: 0,
+ sentinels: [
+ { host: 's1', port: 26379 },
+ { host: 's2', port: 26380 },
+ ],
+ name: 'mymaster',
+ failoverDetector: true,
+ sentinelPassword: 'sentinel-pwd',
+ username: 'cool-user',
+ password: 'master-pwd',
+ });
+ });
+ test('it enables TLS for the Sentinel-resolved master', () => {
+ const opts = server.buildRedisOpts(
+ redisConfig({
+ ssl: true,
+ sentinels: [{ host: 's1', port: 26379 }],
+ sentinelMaster: 'mymaster',
+ }),
+ );
+ expect(opts.tls).toEqual({ checkServerIdentity: expect.anything() });
+ // Without this flag ioredis connects to the master in plaintext.
+ expect(opts.enableTLSForSentinelMode).toBe(true);
+ });
+ test('it requires a master name in Sentinel mode', () => {
+ expect(() =>
+ server.buildRedisOpts(
+ redisConfig({ sentinels: [{ host: 's1', port: 26379 }] }),
+ ),
+ ).toThrow(/sentinelMaster/);
+ });
});
describe('subscribeToChannels', () => {
diff --git a/superset-websocket/src/config.ts b/superset-websocket/src/config.ts
index d9146b9b61d..c6c59618ca3 100644
--- a/superset-websocket/src/config.ts
+++ b/superset-websocket/src/config.ts
@@ -21,6 +21,14 @@ import { readFileSync } from 'fs';
import { merge as _merge } from 'lodash-es';
import { resolve } from 'path';
+export interface SentinelNode {
+ host: string;
+ port: number;
+}
+
+/** Mirrors the Python ``CACHE_REDIS_SSL_CERT_REQS`` values. */
+export type SslCertReqs = 'required' | 'optional' | 'none';
+
export interface RedisConfig {
port: number;
host: string;
@@ -29,6 +37,21 @@ export interface RedisConfig {
db: number;
ssl: boolean;
validateHostname: boolean;
+ // Paths to PEM files. The CA bundle is trusted in addition to the system's
+ // public CAs, so a private CA and public CAs can coexist.
+ sslCaCerts: string;
+ sslCertfile: string;
+ sslKeyfile: string;
+ // Only 'none' disables certificate verification: for a TLS client Python's
+ // 'optional' is equivalent to 'required', so both verify the server.
+ sslCertReqs: SslCertReqs;
+ // A non-empty list selects Sentinel mode: `host`/`port` are ignored and the
+ // master named `sentinelMaster` is resolved through the sentinels.
+ sentinels: SentinelNode[];
+ sentinelMaster: string;
+ // Credentials for the sentinel nodes themselves (`password`/`username`
+ // authenticate against the resolved master).
+ sentinelPassword: string;
}
type ConfigType = {
@@ -90,6 +113,13 @@ function defaultConfig(): ConfigType {
db: 0,
ssl: false,
validateHostname: true,
+ sslCaCerts: '',
+ sslCertfile: '',
+ sslKeyfile: '',
+ sslCertReqs: 'required',
+ sentinels: [],
+ sentinelMaster: '',
+ sentinelPassword: '',
},
};
}
@@ -134,6 +164,41 @@ const toStringArray = (s: string) =>
.map(entry => entry.trim())
.filter(entry => entry.length > 0);
+// Parse "host:port,host:port" (IPv6 hosts as "[::1]:26379"). A missing port
+// defaults to the Sentinel port; an unparseable entry is skipped with a warning.
+function toSentinelNodes(s: string): SentinelNode[] {
+ return toStringArray(s).flatMap(entry => {
+ const sep = entry.lastIndexOf(':');
+ const hasPort = sep > 0 && !entry.endsWith(']');
+ const host = (hasPort ? entry.slice(0, sep) : entry).replace(
+ /^\[|\]$/g,
+ '',
+ );
+ const port = hasPort ? Number(entry.slice(sep + 1)) : 26379;
+ if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
+ console.warn(`Ignoring invalid sentinel address "${entry}".`);
+ return [];
+ }
+ return [{ host, port }];
+ });
+}
+
+function toSslCertReqs(val: string, fallback: SslCertReqs): SslCertReqs {
+ const normalized = val.trim().toLowerCase();
+ if (
+ normalized === 'required' ||
+ normalized === 'optional' ||
+ normalized === 'none'
+ ) {
+ return normalized;
+ }
+ console.warn(
+ `Invalid REDIS_SSL_CERT_REQS "${val}"; expected required, optional or ` +
+ `none. Falling back to ${fallback}.`,
+ );
+ return fallback;
+}
+
function applyEnvOverrides(config: ConfigType): ConfigType {
// Every numeric override goes through `toNonNegativeNumber` so a typo'd value
// falls back to the default with a warning rather than silently becoming NaN
@@ -186,6 +251,16 @@ function applyEnvOverrides(config: ConfigType): ConfigType {
REDIS_DB: val =>
(config.redis.db = toNonNegativeNumber(val, config.redis.db)),
REDIS_SSL: val => (config.redis.ssl = toBoolean(val)),
+ REDIS_VALIDATE_HOSTNAME: val =>
+ (config.redis.validateHostname = toBoolean(val)),
+ REDIS_SSL_CA_CERTS: val => (config.redis.sslCaCerts = val),
+ REDIS_SSL_CERTFILE: val => (config.redis.sslCertfile = val),
+ REDIS_SSL_KEYFILE: val => (config.redis.sslKeyfile = val),
+ REDIS_SSL_CERT_REQS: val =>
+ (config.redis.sslCertReqs = toSslCertReqs(val, config.redis.sslCertReqs)),
+ REDIS_SENTINELS: val => (config.redis.sentinels = toSentinelNodes(val)),
+ REDIS_SENTINEL_MASTER: val => (config.redis.sentinelMaster = val),
+ REDIS_SENTINEL_PASSWORD: val => (config.redis.sentinelPassword = val),
STATSD_HOST: val => (config.statsd.host = val),
STATSD_PORT: val =>
(config.statsd.port = toNonNegativeNumber(val, config.statsd.port)),
diff --git a/superset-websocket/src/index.ts b/superset-websocket/src/index.ts
index d72f12d4833..289e68cc035 100644
--- a/superset-websocket/src/index.ts
+++ b/superset-websocket/src/index.ts
@@ -18,6 +18,7 @@
*/
import * as http from 'http';
import * as net from 'net';
+import { readFileSync } from 'fs';
import { inspect } from 'util';
import { WebSocket, WebSocketServer } from 'ws';
import { randomUUID } from 'crypto';
@@ -31,7 +32,12 @@ import StatsD from 'hot-shots';
import { createLogger } from './logger.js';
import { buildConfig, RedisConfig } from './config.js';
-import { checkServerIdentity, PeerCertificate } from 'tls';
+import {
+ checkServerIdentity,
+ ConnectionOptions,
+ PeerCertificate,
+ rootCertificates,
+} from 'tls';
const REALTIME_JWT_AUDIENCE = 'superset-websocket';
const REALTIME_JWT_ISSUER = 'superset';
@@ -146,12 +152,69 @@ if (startServer) {
validateConfiguredJwtSecret(opts.previousJwtSecret, 'previous JWT secret');
}
-export const buildRedisOpts = (baseConfig: RedisConfig) => {
- const redisOpts: RedisOptions = {
- port: baseConfig.port,
- host: baseConfig.host,
- db: baseConfig.db,
+const readPem = (path: string): string => readFileSync(path, 'utf8');
+
+const buildTlsOptions = (baseConfig: RedisConfig): ConnectionOptions => {
+ const tlsOpts: ConnectionOptions = {
+ checkServerIdentity: (
+ hostname: string,
+ cert: PeerCertificate,
+ ): Error | undefined => {
+ // Note, the cert chain will have been verified already. the role of this method is to
+ // validate that at least one of the SAN's (or subject) of the server's cert matches the provided hostname
+ if (baseConfig.validateHostname) {
+ return checkServerIdentity(hostname, cert);
+ }
+ },
};
+ if (baseConfig.sslCertReqs === 'none') {
+ tlsOpts.rejectUnauthorized = false;
+ }
+ if (baseConfig.sslCaCerts) {
+ // Supplying `ca` replaces Node's default trust store, so the public roots
+ // are re-added to keep public and private CAs usable together.
+ tlsOpts.ca = [...rootCertificates, readPem(baseConfig.sslCaCerts)];
+ }
+ if (baseConfig.sslCertfile) {
+ tlsOpts.cert = readPem(baseConfig.sslCertfile);
+ }
+ if (baseConfig.sslKeyfile) {
+ tlsOpts.key = readPem(baseConfig.sslKeyfile);
+ }
+ return tlsOpts;
+};
+
+/**
+ * Translate the `redis` config into ioredis options, covering the same modes as
+ * the Python coordination backend: standalone or Sentinel (`sentinels` set),
+ * each optionally over TLS. In Sentinel mode TLS applies to the resolved master
+ * only; the sentinel nodes are reached in plaintext, as on the Python side.
+ */
+export const buildRedisOpts = (baseConfig: RedisConfig) => {
+ const redisOpts: RedisOptions = { db: baseConfig.db };
+
+ if (baseConfig.sentinels.length > 0) {
+ if (!baseConfig.sentinelMaster) {
+ throw new Error(
+ 'redis.sentinelMaster (REDIS_SENTINEL_MASTER) is required when ' +
+ 'redis.sentinels (REDIS_SENTINELS) is set',
+ );
+ }
+ redisOpts.sentinels = baseConfig.sentinels.map(({ host, port }) => ({
+ host,
+ port,
+ }));
+ redisOpts.name = baseConfig.sentinelMaster;
+ // Drop the connection to a master that has been demoted by a failover so
+ // the subscriber re-resolves the new master and keeps receiving messages.
+ redisOpts.failoverDetector = true;
+ if (baseConfig.sentinelPassword !== '') {
+ redisOpts.sentinelPassword = baseConfig.sentinelPassword;
+ }
+ } else {
+ redisOpts.host = baseConfig.host;
+ redisOpts.port = baseConfig.port;
+ }
const passwd = baseConfig.password;
if (passwd !== '') {
@@ -160,18 +223,12 @@ export const buildRedisOpts = (baseConfig: RedisConfig) => {
}
if (baseConfig.ssl) {
- redisOpts.tls = {
- checkServerIdentity: (
- hostname: string,
- cert: PeerCertificate,
- ): Error | undefined => {
- // Note, the cert chain will have been verified already. the role of this method is to
- // validate that at least one of the SAN's (or subject) of the server's cert matches the provided hostname
- if (baseConfig.validateHostname) {
- return checkServerIdentity(hostname, cert);
- }
- },
- };
+ redisOpts.tls = buildTlsOptions(baseConfig);
+ // ioredis ignores `tls` for the Sentinel-resolved master (connecting in
+ // plaintext) unless this flag is set.
+ if (redisOpts.sentinels) {
+ redisOpts.enableTLSForSentinelMode = true;
+ }
}
return redisOpts;