Merge pull request #3640 from akhilmhdh/feat/redis-sentinel-support

Feat/redis sentinel support
This commit is contained in:
Maidul Islam
2025-05-26 18:47:15 -04:00
committed by GitHub
12 changed files with 324 additions and 44 deletions

View File

@@ -15,8 +15,8 @@ import { mockSmtpServer } from "./mocks/smtp";
import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { buildRedisFromConfig } from "@app/lib/config/redis";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
@@ -30,7 +30,7 @@ export default {
dbRootCert: envConfig.DB_ROOT_CERT
});
const redis = new Redis(envConfig.REDIS_URL);
const redis = buildRedisFromConfig(envConfig);
await redis.flushdb("SYNC");
try {
@@ -55,8 +55,8 @@ export default {
});
const smtp = mockSmtpServer();
const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
const keyStore = keyStoreFactory(envConfig.REDIS_URL);
const queue = queueServiceFactory(envConfig, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
const keyStore = keyStoreFactory(envConfig);
const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize();

View File

@@ -1,5 +1,4 @@
import { Redis } from "ioredis";
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
@@ -68,8 +67,8 @@ type TWaitTillReady = {
jitter?: number;
};
export const keyStoreFactory = (redisUrl: string) => {
const redis = new Redis(redisUrl);
export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys) => {
const redis = buildRedisFromConfig(redisConfigKeys);
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
const setItem = async (key: string, value: string | number | Buffer, prefix?: string) =>

View File

@@ -30,7 +30,19 @@ const envSchema = z
.enum(["true", "false"])
.default("false")
.transform((el) => el === "true"),
REDIS_URL: zpStr(z.string()),
REDIS_URL: zpStr(z.string().optional()),
REDIS_SENTINEL_HOSTS: zpStr(
z
.string()
.optional()
.describe("Comma-separated list of Sentinel host:port pairs. Eg: 192.168.65.254:26379,192.168.65.254:26380")
),
REDIS_SENTINEL_MASTER_NAME: zpStr(
z.string().optional().default("mymaster").describe("The name of the Redis master set monitored by Sentinel")
),
REDIS_SENTINEL_ENABLE_TLS: zodStrBool.optional().describe("Whether to use TLS/SSL for Redis Sentinel connection"),
REDIS_SENTINEL_USERNAME: zpStr(z.string().optional().describe("Authentication username for Redis Sentinel")),
REDIS_SENTINEL_PASSWORD: zpStr(z.string().optional().describe("Authentication password for Redis Sentinel")),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
@@ -259,26 +271,34 @@ const envSchema = z
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
)
.refine(
(data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS),
"Either REDIS_URL or REDIS_SENTINEL_HOSTS must be defined."
)
.transform((data) => ({
...data,
DB_READ_REPLICAS: data.DB_READ_REPLICAS
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
: undefined,
isCloud: Boolean(data.LICENSE_SERVER_KEY),
isSmtpConfigured: Boolean(data.SMTP_HOST),
isRedisConfigured: Boolean(data.REDIS_URL),
isRedisConfigured: Boolean(data.REDIS_URL || data.REDIS_SENTINEL_HOSTS),
isDevelopmentMode: data.NODE_ENV === "development",
isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",")
.map((el) => {
const [host, port] = el.trim().split(":");
return { host: host.trim(), port: Number(port.trim()) };
}),
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
}));

View File

@@ -0,0 +1,24 @@
import { Redis } from "ioredis";
export type TRedisConfigKeys = Partial<{
REDIS_URL: string;
REDIS_SENTINEL_HOSTS: { host: string; port: number }[];
REDIS_SENTINEL_MASTER_NAME: string;
REDIS_SENTINEL_ENABLE_TLS: boolean;
REDIS_SENTINEL_USERNAME: string;
REDIS_SENTINEL_PASSWORD: string;
}>;
export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => {
if (cfg.REDIS_URL) return new Redis(cfg.REDIS_URL, { maxRetriesPerRequest: null });
return new Redis({
// refine at tope will catch this case
sentinels: cfg.REDIS_SENTINEL_HOSTS!,
name: cfg.REDIS_SENTINEL_MASTER_NAME!,
maxRetriesPerRequest: null,
sentinelUsername: cfg.REDIS_SENTINEL_USERNAME,
sentinelPassword: cfg.REDIS_SENTINEL_PASSWORD,
enableTLSForSentinelMode: cfg.REDIS_SENTINEL_ENABLE_TLS
});
};

View File

@@ -1,7 +1,6 @@
import "./lib/telemetry/instrumentation";
import dotenv from "dotenv";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
@@ -9,6 +8,7 @@ import { runMigrations } from "./auto-start-migrations";
import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
import { buildRedisFromConfig } from "./lib/config/redis";
import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue";
@@ -44,15 +44,15 @@ const run = async () => {
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(envConfig.REDIS_URL, {
const queue = queueServiceFactory(envConfig, {
dbConnectionUrl: envConfig.DB_CONNECTION_URI,
dbRootCert: envConfig.DB_ROOT_CERT
});
await queue.initialize();
const keyStore = keyStoreFactory(envConfig.REDIS_URL);
const redis = new Redis(envConfig.REDIS_URL);
const keyStore = keyStoreFactory(envConfig);
const redis = buildRedisFromConfig(envConfig);
const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize();

View File

@@ -1,5 +1,4 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
import PgBoss, { WorkOptions } from "pg-boss";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
@@ -13,6 +12,7 @@ import {
TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
import { logger } from "@app/lib/logger";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
@@ -265,10 +265,10 @@ export type TQueueJobTypes = {
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
export const queueServiceFactory = (
redisUrl: string,
redisCfg: TRedisConfigKeys,
{ dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string }
) => {
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
const connection = buildRedisFromConfig(redisCfg);
const queueContainer = {} as Record<
QueueName,
Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>

View File

@@ -1,9 +1,9 @@
/* eslint-disable no-console */
import { Redis } from "ioredis";
import { Knex } from "knex";
import { createTransport } from "nodemailer";
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig } from "@app/lib/config/redis";
import { logger } from "@app/lib/logger";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -65,12 +65,15 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
});
console.log("Testing redis connection");
const redis = new Redis(appCfg.REDIS_URL);
const redis = buildRedisFromConfig(appCfg);
const redisPing = await redis?.ping();
if (!redisPing) {
console.error("Redis - Failed to connect");
} else {
console.error("Redis successfully connected");
console.log("Redis successfully connected");
if (appCfg.isRedisSentinelMode) {
console.log("Redis Sentinel Mode");
}
redis.disconnect();
}

View File

@@ -1,14 +1,12 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
import { getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig } from "@app/lib/config/redis";
import { RateLimitError } from "@app/lib/errors";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
const appCfg = getConfig();
const redis = appCfg.isRedisConfigured
? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 })
: null;
const redis = appCfg.isRedisConfigured ? buildRedisFromConfig(appCfg) : null;
return {
errorResponseBuilder: (_, context) => {

View File

@@ -1,5 +1,5 @@
---
title: "Configurations"
title: "Environment Variables"
description: "Read how to configure environment variables for self-hosted Infisical."
---
@@ -34,12 +34,14 @@ Used to configure platform-specific security and operational settings
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
Example values:
- `localhost` (default, same as `127.0.0.1`)
- `0.0.0.0` (all interfaces, accessible externally)
- `192.168.1.100` (specific interface IP)
Example values:
- `localhost` (default, same as `127.0.0.1`)
- `0.0.0.0` (all interfaces, accessible externally)
- `192.168.1.100` (specific interface IP)
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
@@ -86,8 +88,9 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
### PostgreSQL
<Info>
Please note that the database user you create must be granted all privileges on the Infisical database.
This includes the ability to create new schemas, create, update, delete, modify tables and indexes, etc.
Please note that the database user you create must be granted all privileges
on the Infisical database. This includes the ability to create new schemas,
create, update, delete, modify tables and indexes, etc.
</Info>
<ParamField query="DB_CONNECTION_URI" type="string" default="" required>
@@ -119,10 +122,42 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
</ParamField>
### Redis
Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup.
<ParamField query="REDIS_URL" type="string" default="none" required>
Redis connection string.
</ParamField>
<Tabs>
<Tab title="Redis standalone">
<ParamField query="REDIS_URL" type="string" default="none" required>
Redis connection string.
</ParamField>
</Tab>
<Tab title="Redis Sentinel">
<ParamField
query="REDIS_SENTINEL_HOSTS"
type="string"
default="none"
required
>
Comma-separated list of Sentinel host:port pairs. ```
192.168.65.254:26379,192.168.65.254:26380 ```
</ParamField>
<ParamField
query="REDIS_SENTINEL_MASTER_NAME"
type="string"
default="mymaster"
>
The name of the Redis master set monitored by Sentinel
</ParamField>
<ParamField query="REDIS_SENTINEL_ENABLE_TLS" type="bool" default="false">
Whether to use TLS/SSL for Redis Sentinel connection
</ParamField>
<ParamField query="REDIS_SENTINEL_USERNAME" type="string" default="none">
Authentication username for Redis Sentinel
</ParamField>
<ParamField query="REDIS_SENTINEL_PASSWORD" type="string" default="none">
Authentication password for Redis Sentinel
</ParamField>
</Tab>
</Tabs>
## Email Service
@@ -640,13 +675,27 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
The App ID of your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_GIT_APP_SLUG" type="string" default="none" optional>
The slug of your GitHub App.
</ParamField>
{" "}
<ParamField query="SECRET_SCANNING_PRIVATE_KEY" type="string" default="none" optional>
A private key for your GitHub App.
</ParamField>
<ParamField
query="SECRET_SCANNING_GIT_APP_SLUG"
type="string"
default="none"
optional
>
The slug of your GitHub App.
</ParamField>
{" "}
<ParamField
query="SECRET_SCANNING_PRIVATE_KEY"
type="string"
default="none"
optional
>
A private key for your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_WEBHOOK_SECRET" type="string" default="none" optional>
The webhook secret of your GitHub App.

View File

@@ -0,0 +1 @@
HOST_IP=

View File

@@ -0,0 +1,20 @@
## Sink for Redis Sentinel
1. Create a `.env` from `.env.example`
2. The HOST_IP value must be your host ip, so that redis inside the containers can connect to it and switch over as needed.
To test Sentinel is working correctly
1. Run
```
docker exec -it sentinel-2 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
```
2. Run
```
docker-compose -f docker-compose.sentinel.yml stop redis-master
```
3. Again running step 1 should show the other replica port.

View File

@@ -0,0 +1,166 @@
version: "3.8"
services:
redis-master:
image: redis:latest
container_name: redis-master
hostname: redis-master
ports:
- "6380:6379"
volumes:
- ./data/master:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6380",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.3
redis-slave-1:
image: redis:latest
container_name: redis-slave-1
hostname: redis-slave-1
depends_on:
- redis-master
ports:
- "6381:6379"
volumes:
- ./data/slave1:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--replicaof",
"redis-master",
"6379",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6381",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.4
redis-slave-2:
image: redis:latest
container_name: redis-slave-2
hostname: redis-slave-2
depends_on:
- redis-master
ports:
- "6382:6379"
volumes:
- ./data/slave2:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--replicaof",
"redis-master",
"6379",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6382",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.5
sentinel-1:
image: redis:latest
container_name: sentinel-1
hostname: sentinel-1
depends_on:
- redis-master
ports:
- "26379:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.6
sentinel-2:
image: redis:latest
container_name: sentinel-2
hostname: sentinel-2
depends_on:
- redis-master
ports:
- "26380:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.7
sentinel-3:
image: redis:latest
container_name: sentinel-3
hostname: sentinel-3
depends_on:
- redis-master
ports:
- "26381:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.8
redisinsight:
image: redis/redisinsight:latest
container_name: redisinsight
ports:
- "5540:5540"
networks:
redis-net:
ipv4_address: 172.21.0.9
networks:
redis-net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16