diff --git a/.infisicalignore b/.infisicalignore
index 66e2fb635..b935763c8 100644
--- a/.infisicalignore
+++ b/.infisicalignore
@@ -51,3 +51,4 @@ docs/integrations/app-connections/bitbucket.mdx:generic-api-key:123
docs/integrations/app-connections/railway.mdx:generic-api-key:156
.github/workflows/validate-db-schemas.yml:generic-api-key:21
k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8
+docs/integrations/app-connections/redis.mdx:generic-api-key:80
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
index 5f8dea5d7..8d1702850 100644
--- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
@@ -9,6 +9,7 @@ import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rota
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
+import { registerRedisCredentialsRotationRouter } from "./redis-credentials-rotation-router";
export * from "./secret-rotation-v2-router";
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
- [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
+ [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
+ [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
};
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts
new file mode 100644
index 000000000..b83cec52c
--- /dev/null
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts
@@ -0,0 +1,19 @@
+import {
+ CreateRedisCredentialsRotationSchema,
+ RedisCredentialsRotationGeneratedCredentialsSchema,
+ RedisCredentialsRotationSchema,
+ UpdateRedisCredentialsRotationSchema
+} from "@app/ee/services/secret-rotation-v2/redis-credentials";
+import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
+
+import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
+
+export const registerRedisCredentialsRotationRouter = async (server: FastifyZodProvider) =>
+ registerSecretRotationEndpoints({
+ type: SecretRotation.RedisCredentials,
+ server,
+ responseSchema: RedisCredentialsRotationSchema,
+ createSchema: CreateRedisCredentialsRotationSchema,
+ updateSchema: UpdateRedisCredentialsRotationSchema,
+ generatedCredentialsSchema: RedisCredentialsRotationGeneratedCredentialsSchema
+ });
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
index 7db99c8c4..6ea6497e4 100644
--- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
@@ -10,6 +10,7 @@ import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
+import { RedisCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
@@ -25,7 +26,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
- OktaClientSecretRotationListItemSchema
+ OktaClientSecretRotationListItemSchema,
+ RedisCredentialsRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts
new file mode 100644
index 000000000..2d90beab3
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts
@@ -0,0 +1,4 @@
+export * from "./redis-credentials-rotation-constants";
+export * from "./redis-credentials-rotation-fns";
+export * from "./redis-credentials-rotation-schemas";
+export * from "./redis-credentials-rotation-types";
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts
new file mode 100644
index 000000000..1cb14a922
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts
@@ -0,0 +1,15 @@
+import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
+import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+
+export const REDIS_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
+ name: "Redis Credentials",
+ type: SecretRotation.RedisCredentials,
+ connection: AppConnection.Redis,
+ template: {
+ secretsMapping: {
+ username: "REDIS_USERNAME",
+ password: "REDIS_PASSWORD"
+ }
+ }
+};
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts
new file mode 100644
index 000000000..154b4359f
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts
@@ -0,0 +1,194 @@
+/* eslint-disable no-await-in-loop */
+import Redis from "ioredis";
+
+import {
+ TRotationFactory,
+ TRotationFactoryGetSecretsPayload,
+ TRotationFactoryIssueCredentials,
+ TRotationFactoryRevokeCredentials,
+ TRotationFactoryRotateCredentials
+} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
+import { BadRequestError } from "@app/lib/errors";
+
+import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
+import {
+ TRedisCredentialsRotationGeneratedCredentials,
+ TRedisCredentialsRotationWithConnection
+} from "./redis-credentials-rotation-types";
+import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
+
+const redactPasswords = (e: unknown, credentials: TRedisCredentialsRotationGeneratedCredentials) => {
+ const error = e as Error;
+
+ if (!error?.message) return "Unknown error";
+
+ let redactedMessage = error.message;
+
+ credentials.forEach(({ password }) => {
+ redactedMessage = redactedMessage.replaceAll(password, "*******************");
+ });
+
+ return redactedMessage;
+};
+
+export const redisCredentialsRotationFactory: TRotationFactory<
+ TRedisCredentialsRotationWithConnection,
+ TRedisCredentialsRotationGeneratedCredentials
+> = (secretRotation) => {
+ const { connection, secretsMapping, parameters } = secretRotation;
+
+ const $getClient = async () => {
+ const [hostIp] = await verifyHostInputValidity(connection.credentials.host);
+
+ let conn: Redis | null = null;
+ try {
+ conn = new Redis({
+ username: connection.credentials.username,
+ host: hostIp,
+ port: connection.credentials.port,
+ password: connection.credentials.password,
+ ...(connection.credentials.sslEnabled && {
+ tls: {
+ rejectUnauthorized: connection.credentials.sslRejectUnauthorized,
+ ca: connection.credentials.sslCertificate
+ }
+ })
+ });
+
+ let result: string;
+ if (connection.credentials.password) {
+ result = await conn.auth(connection.credentials.username, connection.credentials.password, () => {});
+ } else {
+ result = await conn.auth(connection.credentials.username, () => {});
+ }
+
+ if (result !== "OK") {
+ throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
+ }
+
+ return conn;
+ } catch (err) {
+ if (conn) await conn.quit();
+
+ throw err;
+ }
+ };
+
+ /**
+ * Creates a new user and password for the redis user using ACL
+ */
+ const $rotateAclUser = async () => {
+ let client: Redis | null = null;
+
+ const username = generatePassword({
+ length: 32,
+ required: {
+ symbols: 0,
+ digits: 5,
+ uppercase: 5,
+ lowercase: 5
+ }
+ });
+
+ const password = generatePassword(parameters.passwordRequirements || DEFAULT_PASSWORD_REQUIREMENTS);
+
+ try {
+ client = await $getClient();
+
+ // important: permissionScope is user input so we need to sanitize it, which we do by splitting the permission scope into parts and then passing them to the ACL command as separate arguments
+ const permissionParts = parameters.permissionScope.split(" ");
+ await client.call("ACL", "SETUSER", username, `>${password}`, "on", ...permissionParts);
+
+ return {
+ username,
+ password
+ };
+ } catch (error: unknown) {
+ throw new BadRequestError({
+ message: `Unable to rotate credentials: ${redactPasswords(error, [{ username, password }])}`
+ });
+ } finally {
+ if (client) await client.quit();
+ }
+ };
+
+ /**
+ * Revokes a ACL password from the Redis server using its username and password.
+ */
+ const revokeCredential = async (username: string) => {
+ let client: Redis | null = null;
+
+ try {
+ client = await $getClient();
+ await client.call("ACL", "DELUSER", username);
+ } catch (error: unknown) {
+ throw new BadRequestError({
+ message: `Unable to revoke credential: ${redactPasswords(error, [{ username, password: username }])}`
+ });
+ } finally {
+ if (client) await client.quit();
+ }
+ };
+
+ /**
+ * Issues a new set of credentials.
+ */
+ const issueCredentials: TRotationFactoryIssueCredentials = async (
+ callback
+ ) => {
+ const credentials = await $rotateAclUser();
+
+ return callback(credentials);
+ };
+
+ /**
+ * Revokes a list of credentials.
+ */
+ const revokeCredentials: TRotationFactoryRevokeCredentials = async (
+ credentials,
+ callback
+ ) => {
+ if (!credentials?.length) return callback();
+
+ for (const { username } of credentials) {
+ await revokeCredential(username);
+ // eslint-disable-next-line no-promise-executor-return
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ return callback();
+ };
+
+ /**
+ * Rotates credentials by issuing new ones and revoking the old.
+ */
+ const rotateCredentials: TRotationFactoryRotateCredentials = async (
+ oldCredentials,
+ callback
+ ) => {
+ const newCredentials = await $rotateAclUser();
+
+ if (oldCredentials?.username) {
+ await revokeCredential(oldCredentials.username);
+ }
+
+ return callback(newCredentials);
+ };
+
+ /**
+ * Maps the generated credentials into the secret payload format.
+ */
+ const getSecretsPayload: TRotationFactoryGetSecretsPayload = ({
+ username,
+ password
+ }) => [
+ { key: secretsMapping.username, value: username },
+ { key: secretsMapping.password, value: password }
+ ];
+
+ return {
+ issueCredentials,
+ revokeCredentials,
+ rotateCredentials,
+ getSecretsPayload
+ };
+};
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts
new file mode 100644
index 000000000..c4948a26d
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts
@@ -0,0 +1,70 @@
+import { z } from "zod";
+
+import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
+import {
+ BaseCreateSecretRotationSchema,
+ BaseSecretRotationSchema,
+ BaseUpdateSecretRotationSchema
+} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
+import { SecretRotations } from "@app/lib/api-docs";
+import { SecretNameSchema } from "@app/server/lib/schemas";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+
+import { PasswordRequirementsSchema } from "../shared/general";
+
+export const RedisCredentialsRotationGeneratedCredentialsSchema = z
+ .object({
+ username: z.string(),
+ password: z.string()
+ })
+ .array()
+ .min(1)
+ .max(2);
+
+const RedisCredentialsRotationSecretsMappingSchema = z.object({
+ username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.username),
+ password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.password)
+});
+
+export const RedisCredentialsRotationParametersSchema = z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z
+ .string()
+ .trim()
+ .min(1, "Permission scope is required")
+ .describe(SecretRotations.PARAMETERS.REDIS_CREDENTIALS.permissionScope)
+});
+
+export const RedisCredentialsRotationTemplateSchema = z.object({
+ secretsMapping: z.object({
+ username: z.string(),
+ password: z.string()
+ })
+});
+
+export const RedisCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.RedisCredentials).extend({
+ type: z.literal(SecretRotation.RedisCredentials),
+ parameters: RedisCredentialsRotationParametersSchema,
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema
+});
+
+export const CreateRedisCredentialsRotationSchema = BaseCreateSecretRotationSchema(
+ SecretRotation.RedisCredentials
+).extend({
+ parameters: RedisCredentialsRotationParametersSchema,
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema
+});
+
+export const UpdateRedisCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
+ SecretRotation.RedisCredentials
+).extend({
+ parameters: RedisCredentialsRotationParametersSchema.optional(),
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema.optional()
+});
+
+export const RedisCredentialsRotationListItemSchema = z.object({
+ name: z.literal("Redis Credentials"),
+ connection: z.literal(AppConnection.Redis),
+ type: z.literal(SecretRotation.RedisCredentials),
+ template: RedisCredentialsRotationTemplateSchema
+});
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts
new file mode 100644
index 000000000..46f217d61
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts
@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+import { TRedisConnection } from "@app/services/app-connection/redis";
+
+import {
+ CreateRedisCredentialsRotationSchema,
+ RedisCredentialsRotationGeneratedCredentialsSchema,
+ RedisCredentialsRotationListItemSchema,
+ RedisCredentialsRotationSchema
+} from "./redis-credentials-rotation-schemas";
+
+export type TRedisCredentialsRotation = z.infer;
+
+export type TRedisCredentialsRotationInput = z.infer;
+
+export type TRedisCredentialsRotationListItem = z.infer;
+
+export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
+ connection: TRedisConnection;
+};
+
+export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
+ typeof RedisCredentialsRotationGeneratedCredentialsSchema
+>;
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
index cf0fe578a..661a2399a 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
@@ -7,7 +7,8 @@ export enum SecretRotation {
AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password",
- OktaClientSecret = "okta-client-secret"
+ OktaClientSecret = "okta-client-secret",
+ RedisCredentials = "redis-credentials"
}
export enum SecretRotationStatus {
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
index 4d8cea6a3..e4e6a8531 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
@@ -14,6 +14,7 @@ import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
+import { REDIS_CREDENTIALS_ROTATION_LIST_OPTION } from "./redis-credentials";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
import { TSecretRotationV2ServiceFactory, TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
@@ -35,7 +36,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record {
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
index d9a771101..2087fa195 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = {
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
- [SecretRotation.OktaClientSecret]: "Okta Client Secret"
+ [SecretRotation.OktaClientSecret]: "Okta Client Secret",
+ [SecretRotation.RedisCredentials]: "Redis Credentials"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record = {
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record {
diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts
index 70804d173..11d9ce5e6 100644
--- a/backend/src/server/routes/v1/app-connection-routers/index.ts
+++ b/backend/src/server/routes/v1/app-connection-routers/index.ts
@@ -31,6 +31,7 @@ import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
+import { registerRedisConnectionRouter } from "./redis-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
@@ -80,5 +81,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record {
+ registerAppConnectionEndpoints({
+ app: AppConnection.Redis,
+ server,
+ sanitizedResponseSchema: SanitizedRedisConnectionSchema,
+ createSchema: CreateRedisConnectionSchema,
+ updateSchema: UpdateRedisConnectionSchema
+ });
+};
diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts
index 76dcdd5f0..996cd872a 100644
--- a/backend/src/services/app-connection/app-connection-enums.ts
+++ b/backend/src/services/app-connection/app-connection-enums.ts
@@ -36,7 +36,8 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
- Okta = "okta"
+ Okta = "okta",
+ Redis = "redis"
}
export enum AWSRegion {
diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts
index 8c8f01af7..a86fa7420 100644
--- a/backend/src/services/app-connection/app-connection-fns.ts
+++ b/backend/src/services/app-connection/app-connection-fns.ts
@@ -111,6 +111,7 @@ import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } fr
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
+import { getRedisConnectionListItem, RedisConnectionMethod, validateRedisConnectionCredentials } from "./redis";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
@@ -196,7 +197,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(),
- getOktaConnectionListItem()
+ getOktaConnectionListItem(),
+ getRedisConnectionListItem()
]
.filter((option) => {
switch (projectType) {
@@ -322,7 +324,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
- [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
+ [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
+ [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -369,6 +372,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
+ case RedisConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken:
@@ -456,7 +460,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
- [AppConnection.Okta]: platformManagedCredentialsNotSupported
+ [AppConnection.Okta]: platformManagedCredentialsNotSupported,
+ [AppConnection.Redis]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts
index a2ce02669..e3235d2f7 100644
--- a/backend/src/services/app-connection/app-connection-maps.ts
+++ b/backend/src/services/app-connection/app-connection-maps.ts
@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record = {
[AppConnection.Supabase]: "Supabase",
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
[AppConnection.Netlify]: "Netlify",
- [AppConnection.Okta]: "Okta"
+ [AppConnection.Okta]: "Okta",
+ [AppConnection.Redis]: "Redis"
};
export const APP_CONNECTION_PLAN_MAP: Record = {
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>;
@@ -306,6 +313,7 @@ export type TAppConnectionInput = { id: string } & (
| TDigitalOceanConnectionInput
| TNetlifyConnectionInput
| TOktaConnectionInput
+ | TRedisConnectionInput
);
export type TSqlConnectionInput =
@@ -368,7 +376,8 @@ export type TAppConnectionConfig =
| TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig
| TNetlifyConnectionConfig
- | TOktaConnectionConfig;
+ | TOktaConnectionConfig
+ | TRedisConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -408,7 +417,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema
| TValidateNetlifyConnectionCredentialsSchema
- | TValidateOktaConnectionCredentialsSchema;
+ | TValidateOktaConnectionCredentialsSchema
+ | TValidateRedisConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
diff --git a/backend/src/services/app-connection/redis/index.ts b/backend/src/services/app-connection/redis/index.ts
new file mode 100644
index 000000000..76b071958
--- /dev/null
+++ b/backend/src/services/app-connection/redis/index.ts
@@ -0,0 +1,4 @@
+export * from "./redis-connection-enums";
+export * from "./redis-connection-fns";
+export * from "./redis-connection-schemas";
+export * from "./redis-connection-types";
diff --git a/backend/src/services/app-connection/redis/redis-connection-enums.ts b/backend/src/services/app-connection/redis/redis-connection-enums.ts
new file mode 100644
index 000000000..01befeee9
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-enums.ts
@@ -0,0 +1,3 @@
+export enum RedisConnectionMethod {
+ UsernameAndPassword = "username-and-password"
+}
diff --git a/backend/src/services/app-connection/redis/redis-connection-fns.ts b/backend/src/services/app-connection/redis/redis-connection-fns.ts
new file mode 100644
index 000000000..4141d2f9a
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-fns.ts
@@ -0,0 +1,59 @@
+import Redis from "ioredis";
+
+import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
+import { BadRequestError } from "@app/lib/errors";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+
+import { RedisConnectionMethod } from "./redis-connection-enums";
+import { TRedisConnectionConfig } from "./redis-connection-types";
+
+export const getRedisConnectionListItem = () => {
+ return {
+ name: "Redis" as const,
+ app: AppConnection.Redis as const,
+ methods: Object.values(RedisConnectionMethod) as [RedisConnectionMethod.UsernameAndPassword],
+ supportsPlatformManagement: false as const
+ };
+};
+
+export const validateRedisConnectionCredentials = async (config: TRedisConnectionConfig) => {
+ const [hostIp] = await verifyHostInputValidity(config.credentials.host);
+
+ let connection: Redis | null = null;
+ try {
+ connection = new Redis({
+ username: config.credentials.username,
+ host: hostIp,
+ port: config.credentials.port,
+ password: config.credentials.password,
+ ...(config.credentials.sslEnabled && {
+ tls: {
+ rejectUnauthorized: config.credentials.sslRejectUnauthorized,
+ ca: config.credentials.sslCertificate
+ }
+ })
+ });
+
+ let result: string;
+ if (config.credentials.password) {
+ result = await connection.auth(config.credentials.username, config.credentials.password, () => {});
+ } else {
+ result = await connection.auth(config.credentials.username, () => {});
+ }
+
+ if (result !== "OK") {
+ throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
+ }
+
+ return config.credentials;
+ } catch (err) {
+ if (err instanceof BadRequestError) {
+ throw err;
+ }
+ throw new BadRequestError({
+ message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}`
+ });
+ } finally {
+ if (connection) await connection.quit();
+ }
+};
diff --git a/backend/src/services/app-connection/redis/redis-connection-schemas.ts b/backend/src/services/app-connection/redis/redis-connection-schemas.ts
new file mode 100644
index 000000000..f29a2d036
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-schemas.ts
@@ -0,0 +1,87 @@
+import z from "zod";
+
+import { AppConnections } from "@app/lib/api-docs";
+import {
+ BaseAppConnectionSchema,
+ GenericCreateAppConnectionFieldsSchema,
+ GenericUpdateAppConnectionFieldsSchema
+} from "@app/services/app-connection/app-connection-schemas";
+
+import { AppConnection } from "../app-connection-enums";
+import { RedisConnectionMethod } from "./redis-connection-enums";
+
+export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
+ host: z.string().toLowerCase().min(1),
+ port: z.coerce.number(),
+ username: z.string().min(1),
+ password: z.string().min(1).optional(),
+
+ sslRejectUnauthorized: z.boolean(),
+ sslEnabled: z.boolean(),
+ sslCertificate: z
+ .string()
+ .trim()
+ .transform((value) => value || undefined)
+ .optional()
+});
+
+export const RedisConnectionAccessTokenCredentialsSchema = BaseRedisUsernameAndPasswordConnectionSchema;
+
+const BaseRedisConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Redis) });
+
+export const RedisConnectionSchema = BaseRedisConnectionSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: RedisConnectionAccessTokenCredentialsSchema
+});
+
+export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
+ BaseRedisConnectionSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: RedisConnectionAccessTokenCredentialsSchema.pick({
+ host: true,
+ port: true,
+ username: true,
+ sslEnabled: true,
+ sslRejectUnauthorized: true,
+ sslCertificate: true
+ })
+ })
+]);
+
+export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
+ z.object({
+ method: z
+ .literal(RedisConnectionMethod.UsernameAndPassword)
+ .describe(AppConnections.CREATE(AppConnection.Redis).method),
+ credentials: RedisConnectionAccessTokenCredentialsSchema.describe(
+ AppConnections.CREATE(AppConnection.Redis).credentials
+ )
+ })
+]);
+
+export const CreateRedisConnectionSchema = ValidateRedisConnectionCredentialsSchema.and(
+ GenericCreateAppConnectionFieldsSchema(AppConnection.Redis, {
+ supportsPlatformManagedCredentials: false,
+ supportsGateways: false
+ })
+);
+
+export const UpdateRedisConnectionSchema = z
+ .object({
+ credentials: RedisConnectionAccessTokenCredentialsSchema.optional().describe(
+ AppConnections.UPDATE(AppConnection.Redis).credentials
+ )
+ })
+ .and(
+ GenericUpdateAppConnectionFieldsSchema(AppConnection.Redis, {
+ supportsPlatformManagedCredentials: false,
+ supportsGateways: false
+ })
+ );
+
+export const RedisConnectionListItemSchema = z.object({
+ name: z.literal("Redis"),
+ app: z.literal(AppConnection.Redis),
+ methods: z.nativeEnum(RedisConnectionMethod).array(),
+ supportsPlatformManagement: z.literal(false)
+});
diff --git a/backend/src/services/app-connection/redis/redis-connection-types.ts b/backend/src/services/app-connection/redis/redis-connection-types.ts
new file mode 100644
index 000000000..2d1ba7699
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-types.ts
@@ -0,0 +1,22 @@
+import z from "zod";
+
+import { DiscriminativePick } from "@app/lib/types";
+
+import { AppConnection } from "../app-connection-enums";
+import {
+ CreateRedisConnectionSchema,
+ RedisConnectionSchema,
+ ValidateRedisConnectionCredentialsSchema
+} from "./redis-connection-schemas";
+
+export type TRedisConnection = z.infer;
+
+export type TRedisConnectionInput = z.infer & {
+ app: AppConnection.Redis;
+};
+
+export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
+
+export type TRedisConnectionConfig = DiscriminativePick & {
+ orgId: string;
+};
diff --git a/docs/api-reference/endpoints/app-connections/redis/available.mdx b/docs/api-reference/endpoints/app-connections/redis/available.mdx
new file mode 100644
index 000000000..6b0389d3f
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/available.mdx
@@ -0,0 +1,4 @@
+---
+title: "Available"
+openapi: "GET /api/v1/app-connections/redis/available"
+---
diff --git a/docs/api-reference/endpoints/app-connections/redis/create.mdx b/docs/api-reference/endpoints/app-connections/redis/create.mdx
new file mode 100644
index 000000000..b203048d6
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/create.mdx
@@ -0,0 +1,9 @@
+---
+title: "Create"
+openapi: "POST /api/v1/app-connections/redis"
+---
+
+
+ Check out the configuration docs for [Redis Connections](/integrations/app-connections/redis) to learn how to obtain
+ the required credentials.
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/app-connections/redis/delete.mdx b/docs/api-reference/endpoints/app-connections/redis/delete.mdx
new file mode 100644
index 000000000..bf8178a23
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/app-connections/redis/{connectionId}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/redis/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/redis/get-by-id.mdx
new file mode 100644
index 000000000..9879fff7f
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/get-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by ID"
+openapi: "GET /api/v1/app-connections/redis/{connectionId}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/redis/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/redis/get-by-name.mdx
new file mode 100644
index 000000000..42807f221
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/get-by-name.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by Name"
+openapi: "GET /api/v1/app-connections/redis/connection-name/{connectionName}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/redis/list.mdx b/docs/api-reference/endpoints/app-connections/redis/list.mdx
new file mode 100644
index 000000000..913da1679
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/app-connections/redis"
+---
diff --git a/docs/api-reference/endpoints/app-connections/redis/update.mdx b/docs/api-reference/endpoints/app-connections/redis/update.mdx
new file mode 100644
index 000000000..e2414b971
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/redis/update.mdx
@@ -0,0 +1,9 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/app-connections/redis/{connectionId}"
+---
+
+
+ Check out the configuration docs for [Redis Connections](/integrations/app-connections/redis) to learn how to obtain
+ the required credentials.
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/create.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/create.mdx
new file mode 100644
index 000000000..8bce0dc4f
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/create.mdx
@@ -0,0 +1,10 @@
+---
+title: "Create"
+openapi: "POST /api/v2/secret-rotations/redis-credentials"
+---
+
+
+ Check out the configuration docs for [Redis
+ Credentials Rotations](/documentation/platform/secret-rotation/redis-credentials) to learn how to obtain the
+ required parameters.
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/delete.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/delete.mdx
new file mode 100644
index 000000000..28d9e3a6b
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v2/secret-rotations/redis-credentials/{rotationId}"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-id.mdx
new file mode 100644
index 000000000..a3ec932e0
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by ID"
+openapi: "GET /api/v2/secret-rotations/redis-credentials/{rotationId}"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-name.mdx
new file mode 100644
index 000000000..2dc50d581
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-by-name.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by Name"
+openapi: "GET /api/v2/secret-rotations/redis-credentials/rotation-name/{rotationName}"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id.mdx
new file mode 100644
index 000000000..c0002edd6
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get Credentials by ID"
+openapi: "GET /api/v2/secret-rotations/redis-credentials/{rotationId}/generated-credentials"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/list.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/list.mdx
new file mode 100644
index 000000000..bc72d101e
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v2/secret-rotations/redis-credentials"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets.mdx
new file mode 100644
index 000000000..53c2c7651
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets.mdx
@@ -0,0 +1,4 @@
+---
+title: "Rotate Secrets"
+openapi: "POST /api/v2/secret-rotations/redis-credentials/{rotationId}/rotate-secrets"
+---
diff --git a/docs/api-reference/endpoints/secret-rotations/redis-credentials/update.mdx b/docs/api-reference/endpoints/secret-rotations/redis-credentials/update.mdx
new file mode 100644
index 000000000..4817a0f65
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-rotations/redis-credentials/update.mdx
@@ -0,0 +1,10 @@
+---
+title: "Update"
+openapi: "PATCH /api/v2/secret-rotations/redis-credentials/{rotationId}"
+---
+
+
+ Check out the configuration docs for [Redis
+ Credentials Rotations](/documentation/platform/secret-rotation/redis-credentials) to learn how to obtain the
+ required parameters.
+
\ No newline at end of file
diff --git a/docs/docs.json b/docs/docs.json
index d71b16f7f..b2ce93499 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -134,6 +134,7 @@
"integrations/app-connections/oracledb",
"integrations/app-connections/postgres",
"integrations/app-connections/railway",
+ "integrations/app-connections/redis",
"integrations/app-connections/render",
"integrations/app-connections/supabase",
"integrations/app-connections/teamcity",
@@ -442,7 +443,8 @@
"documentation/platform/secret-rotation/mysql-credentials",
"documentation/platform/secret-rotation/okta-client-secret",
"documentation/platform/secret-rotation/oracledb-credentials",
- "documentation/platform/secret-rotation/postgres-credentials"
+ "documentation/platform/secret-rotation/postgres-credentials",
+ "documentation/platform/secret-rotation/redis-credentials"
]
},
{
@@ -1389,6 +1391,19 @@
"api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets",
"api-reference/endpoints/secret-rotations/postgres-credentials/update"
]
+ },
+ {
+ "group": "Redis Credentials",
+ "pages": [
+ "api-reference/endpoints/secret-rotations/redis-credentials/create",
+ "api-reference/endpoints/secret-rotations/redis-credentials/delete",
+ "api-reference/endpoints/secret-rotations/redis-credentials/get-by-id",
+ "api-reference/endpoints/secret-rotations/redis-credentials/get-by-name",
+ "api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id",
+ "api-reference/endpoints/secret-rotations/redis-credentials/list",
+ "api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets",
+ "api-reference/endpoints/secret-rotations/redis-credentials/update"
+ ]
}
]
},
@@ -1866,6 +1881,18 @@
"api-reference/endpoints/app-connections/railway/delete"
]
},
+ {
+ "group": "Redis",
+ "pages": [
+ "api-reference/endpoints/app-connections/redis/list",
+ "api-reference/endpoints/app-connections/redis/available",
+ "api-reference/endpoints/app-connections/redis/get-by-id",
+ "api-reference/endpoints/app-connections/redis/get-by-name",
+ "api-reference/endpoints/app-connections/redis/create",
+ "api-reference/endpoints/app-connections/redis/update",
+ "api-reference/endpoints/app-connections/redis/delete"
+ ]
+ },
{
"group": "Render",
"pages": [
diff --git a/docs/documentation/platform/secret-rotation/redis-credentials.mdx b/docs/documentation/platform/secret-rotation/redis-credentials.mdx
new file mode 100644
index 000000000..577d0e956
--- /dev/null
+++ b/docs/documentation/platform/secret-rotation/redis-credentials.mdx
@@ -0,0 +1,158 @@
+---
+title: "Redis Credentials Rotation"
+description: "Learn how to automatically rotate Redis credentials."
+---
+
+## Prerequisites
+
+1. Create a [Redis Connection](/integrations/app-connections/redis) with the required **Secret Rotation** permissions
+2. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply.
+
+Create a Redis Credentials Rotation in Infisical
+
+
+
+ 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown.
+ 
+
+ 2. Select the **Redis Credentials** option.
+ 
+
+ 3. Select the **Redis Connection** to use and configure the rotation behavior. Then click **Next**.
+ 
+
+ - **Redis Connection** - the connection that will perform the rotation of the configured database user credentials.
+ - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation.
+ - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed.
+ - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation.
+
+ 4. Input the password requirements and permission scope for the Redis users that will be created for the rotation. Then click **Next**.
+ 
+
+ - **Permission Scope** - The scope of the Redis users that will be created for the rotation. This will default to `~* +@all` if not specified.
+ - **Password Requirements** - The requirements for the password of the Redis users that will be created for the rotation.
+
+ 5. Specify the secret names that the active credentials should be mapped to. Then click **Next**.
+ 
+
+ - **Username** - the name of the secret that the active username will be mapped to.
+ - **Password** - the name of the secret that the active password will be mapped to.
+
+ 6. Give your rotation a name and description (optional). Then click **Next**.
+ 
+
+ - **Name** - the name of the secret rotation configuration. Must be slug-friendly.
+ - **Description** (optional) - a description of this rotation configuration.
+
+ 7. Review your configuration, then click **Create Secret Rotation**.
+ 
+
+ 8. Your **Redis Credentials** are now available for use via the mapped secrets.
+ 
+
+
+ To create a Redis Credentials Rotation, make an API request to the [Create Redis
+ Credentials Rotation](/api-reference/endpoints/secret-rotations/redis-credentials/create) API endpoint.
+
+ ### Sample request
+
+ ```bash Request
+ curl --request POST \
+ --url https://us.infisical.com/api/v2/secret-rotations/redis-credentials \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "name": my-redis-rotation",
+ "projectId": "",
+ "description": "",
+ "connectionId": "",
+ "environment": "dev|staging|prod",
+ "secretPath": "",
+ "isAutoRotationEnabled": true,
+ "rotationInterval": 2,
+ "rotateAtUtc": {
+ "hours": 11.5,
+ "minutes": 29.5
+ },
+ "parameters": {
+ "passwordRequirements": {
+ "length": 64,
+ "required": {
+ "digits": 1,
+ "lowercase": 1,
+ "uppercase": 1,
+ "symbols": 1
+ },
+ "allowedSymbols": "@!+"
+ },
+ "permissionScope": "~* +@all"
+ },
+ "secretsMapping": {
+ "username": "REDIS_USERNAME",
+ "password": "REDIS_PASSWORD"
+ }
+ }'
+ ```
+
+ ### Sample response
+
+ ```bash Response
+ {
+ "secretRotation": {
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "name": "my-redis-rotation",
+ "description": "my database credentials rotation",
+ "isAutoRotationEnabled": true,
+ "activeIndex": 0,
+ "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "createdAt": "2023-11-07T05:31:56Z",
+ "updatedAt": "2023-11-07T05:31:56Z",
+ "rotationInterval": 30,
+ "rotationStatus": "success",
+ "lastRotationAttemptedAt": "2023-11-07T05:31:56Z",
+ "lastRotatedAt": "2023-11-07T05:31:56Z",
+ "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "nextRotationAt": "2023-11-07T05:31:56Z",
+ "connection": {
+ "app": "redis",
+ "name": "my-redis-connection",
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
+ },
+ "environment": {
+ "slug": "dev",
+ "name": "Development",
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
+ },
+ "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "folder": {
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "path": "/"
+ },
+ "rotateAtUtc": {
+ "hours": 0,
+ "minutes": 0
+ },
+ "lastRotationMessage": null,
+ "type": "redis-credentials",
+ "parameters": {
+ "passwordRequirements": {
+ "length": 64,
+ "required": {
+ "digits": 1,
+ "lowercase": 1,
+ "uppercase": 1,
+ "symbols": 1
+ },
+ "allowedSymbols": "@!+"
+ },
+ "permissionScope": "~* +@all"
+ },
+ "secretsMapping": {
+ "username": "REDIS_USERNAME",
+ "password": "REDIS_PASSWORD"
+ }
+ }
+ }
+ ```
+
+
diff --git a/docs/images/app-connections/redis/redis-app-connection-form.png b/docs/images/app-connections/redis/redis-app-connection-form.png
new file mode 100644
index 000000000..05db36fd5
Binary files /dev/null and b/docs/images/app-connections/redis/redis-app-connection-form.png differ
diff --git a/docs/images/app-connections/redis/redis-app-connection-generated.png b/docs/images/app-connections/redis/redis-app-connection-generated.png
new file mode 100644
index 000000000..b3eb5af72
Binary files /dev/null and b/docs/images/app-connections/redis/redis-app-connection-generated.png differ
diff --git a/docs/images/app-connections/redis/redis-app-connection-option.png b/docs/images/app-connections/redis/redis-app-connection-option.png
new file mode 100644
index 000000000..46f3dea6d
Binary files /dev/null and b/docs/images/app-connections/redis/redis-app-connection-option.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-configuration.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-configuration.png
new file mode 100644
index 000000000..280a8bed6
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-configuration.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-confirm.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-confirm.png
new file mode 100644
index 000000000..efff5b168
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-confirm.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-created.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-created.png
new file mode 100644
index 000000000..2e6dd994c
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-created.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-details.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-details.png
new file mode 100644
index 000000000..9a7552f7c
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-details.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-parameters.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-parameters.png
new file mode 100644
index 000000000..4d9db7dd4
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-parameters.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-secrets-mapping.png b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-secrets-mapping.png
new file mode 100644
index 000000000..10d02a191
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/redis-credentials-secrets-mapping.png differ
diff --git a/docs/images/secret-rotations-v2/redis-credentials/select-redis-credentials-option.png b/docs/images/secret-rotations-v2/redis-credentials/select-redis-credentials-option.png
new file mode 100644
index 000000000..979a77b43
Binary files /dev/null and b/docs/images/secret-rotations-v2/redis-credentials/select-redis-credentials-option.png differ
diff --git a/docs/integrations/app-connections/redis.mdx b/docs/integrations/app-connections/redis.mdx
new file mode 100644
index 000000000..224abbea8
--- /dev/null
+++ b/docs/integrations/app-connections/redis.mdx
@@ -0,0 +1,126 @@
+---
+title: "Redis Connection"
+description: "Learn how to configure a Redis Connection for Infisical."
+---
+
+Infisical supports the use of Username & Password authentication to connect with Redis databases
+
+## Configure a Redis user for Infisical
+
+
+
+ Infisical recommends creating a designated user in your Redis database for your connection.
+
+ ```bash
+ ACL SETUSER user_manager on >[ENTER-YOUR-USER-PASSWORD]
+ ```
+
+
+
+ Depending on how you intend to use your Redis connection, you'll need to grant one or more of the following permissions.
+
+
+ To learn more about Redis's permission system, please visit their [documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/).
+
+
+
+
+ For Secret Rotations, your Infisical user will require the ability to set and delete users:
+
+ ```bash
+ ACL SETUSER user_manager +acl|setuser +acl|deluser ~*
+ ```
+
+
+
+
+
+
+## Create Redis Connection in Infisical
+
+
+
+
+
+ In your Infisical dashboard, navigate to the **App Connections** page in the desired project.
+
+ 
+
+
+ Click the **+ Add Connection** button and select the **Redis Connection** option from the available integrations.
+
+ 
+
+
+ Complete the Redis Connection form by entering:
+ - A descriptive name for the connection
+ - An optional description for future reference
+ - The Redis host URL for your database
+ - The Redis port for your Redis database
+ - The Redis username for your Redis database
+ - The Redis password for your Redis database
+
+ You can optionally configure SSL/TLS for your Redis connection in the **SSL** section.
+
+
+ 
+
+
+ After clicking Create, your **Redis Connection** is established and ready to use with your Infisical project.
+
+ 
+
+
+
+
+ To create a Redis Connection, make an API request to the [Create Redis Connection](/api-reference/endpoints/app-connections/redis/create) API endpoint.
+
+ ### Sample request
+
+ ```bash Request
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/app-connections/redis \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "name": "my-redis-connection",
+ "method": "username-and-password",
+ "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
+ "credentials": {
+ "host": "[REDIS HOST]",
+ "port": 6379,
+ "username": "[REDIS USERNAME]",
+ "password": "[REDIS PASSWORD]",
+ }
+ }'
+ ```
+
+ ### Sample response
+
+ ```bash Response
+ {
+ "appConnection": {
+ "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
+ "name": "my-redis-connection",
+ "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
+ "description": null,
+ "version": 1,
+ "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",
+ "createdAt": "2025-04-23T19:46:34.831Z",
+ "updatedAt": "2025-04-23T19:46:34.831Z",
+ "isPlatformManagedCredentials": false,
+ "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f",
+ "app": "redis",
+ "method": "username-and-password",
+ credentials: {
+ "host": "",
+ "port": 6379,
+ "username": "",
+ "sslEnabled": true,
+ "sslRejectUnauthorized": false,
+ "sslCertificate": ""
+ }
+ }
+ }
+ ```
+
+
diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx
index 72ad40cff..951a643dd 100644
--- a/docs/snippets/AppConnectionsBrowser.jsx
+++ b/docs/snippets/AppConnectionsBrowser.jsx
@@ -42,6 +42,7 @@ export const AppConnectionsBrowser = () => {
{"name": "PostgreSQL", "slug": "postgres", "path": "/integrations/app-connections/postgres", "description": "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", "category": "Databases"},
{"name": "Microsoft SQL Server", "slug": "mssql", "path": "/integrations/app-connections/mssql", "description": "Learn how to connect your SQL Server database to pull secrets from Infisical.", "category": "Databases"},
{"name": "Oracle Database", "slug": "oracledb", "path": "/integrations/app-connections/oracledb", "description": "Learn how to connect your Oracle database to pull secrets from Infisical.", "category": "Databases"},
+ {"name": "Redis", "slug": "redis", "path": "/integrations/app-connections/redis", "description": "Learn how to connect Redis to pull secrets from Infisical.", "category": "Databases"},
{"name": "LDAP", "slug": "ldap", "path": "/integrations/app-connections/ldap", "description": "Learn how to connect your LDAP to pull secrets from Infisical.", "category": "Directory Services"},
{"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"},
{"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"}
diff --git a/docs/snippets/RotationsBrowser.jsx b/docs/snippets/RotationsBrowser.jsx
index 3d50656b6..3dbede698 100644
--- a/docs/snippets/RotationsBrowser.jsx
+++ b/docs/snippets/RotationsBrowser.jsx
@@ -14,6 +14,7 @@ export const RotationsBrowser = () => {
{"name": "LDAP Password", "slug": "ldap-password", "path": "/documentation/platform/secret-rotation/ldap-password", "description": "Learn how to automatically rotate LDAP user passwords.", "category": "Identity & Auth"},
{"name": "MySQL", "slug": "mysql-credentials", "path": "/documentation/platform/secret-rotation/mysql-credentials", "description": "Learn how to automatically rotate MySQL database credentials.", "category": "Databases"},
{"name": "PostgreSQL", "slug": "postgres-credentials", "path": "/documentation/platform/secret-rotation/postgres-credentials", "description": "Learn how to automatically rotate PostgreSQL database credentials.", "category": "Databases"},
+ {"name": "Redis", "slug": "redis-credentials", "path": "/documentation/platform/secret-rotation/redis-credentials", "description": "Learn how to automatically rotate Redis database credentials.", "category": "Databases"},
{"name": "Microsoft SQL Server", "slug": "mssql-credentials", "path": "/documentation/platform/secret-rotation/mssql-credentials", "description": "Learn how to automatically rotate Microsoft SQL Server credentials.", "category": "Databases"},
{"name": "Oracle Database", "slug": "oracledb-credentials", "path": "/documentation/platform/secret-rotation/oracledb-credentials", "description": "Learn how to automatically rotate Oracle Database credentials.", "category": "Databases"}
].sort(function(a, b) {
diff --git a/frontend/public/images/integrations/Redis.png b/frontend/public/images/integrations/Redis.png
new file mode 100644
index 000000000..3ef8adffd
Binary files /dev/null and b/frontend/public/images/integrations/Redis.png differ
diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx
new file mode 100644
index 000000000..18feefa09
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx
@@ -0,0 +1,38 @@
+import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
+
+import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
+import { TRedisCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/redis-credentials-rotation";
+
+type Props = {
+ generatedCredentialsResponse: TRedisCredentialsRotationGeneratedCredentialsResponse;
+};
+
+export const ViewRedisCredentialsRotationGeneratedCredentials = ({
+ generatedCredentialsResponse: { generatedCredentials, activeIndex }
+}: Props) => {
+ const inactiveIndex = activeIndex === 0 ? 1 : 0;
+
+ const activeCredentials = generatedCredentials[activeIndex];
+ const inactiveCredentials = generatedCredentials[inactiveIndex];
+
+ return (
+
+ {activeCredentials?.username}
+
+ {activeCredentials?.password}
+
+ >
+ }
+ inactiveCredentials={
+ <>
+ {inactiveCredentials?.username}
+
+ {inactiveCredentials?.password}
+
+ >
+ }
+ />
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
index 33d3fccc1..e8553f6d9 100644
--- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
+++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
@@ -23,6 +23,7 @@ import {
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials";
+import { ViewRedisCredentialsRotationGeneratedCredentials } from "./ViewRedisCredentialsRotationGeneratedCredentials";
type Props = {
secretRotation?: TSecretRotationV2;
@@ -107,6 +108,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/>
);
break;
+ case SecretRotation.RedisCredentials:
+ Component = (
+
+ );
+ break;
default:
throw new Error("Unhandled View Generated Credential Rotation Type");
}
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx
new file mode 100644
index 000000000..0aeffef21
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx
@@ -0,0 +1,197 @@
+import { Controller, useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { FormControl, Input } from "@app/components/v2";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+import { DEFAULT_PASSWORD_REQUIREMENTS } from "../schemas/shared";
+
+export const RedisCredentialsRotationParametersFields = () => {
+ const { control } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ return (
+ <>
+
+
(
+
+
+ This is the access control permissions that will be set for the issued Redis
+ users. The format must be a valid Redis ACL pattern.
+
+
+ The default value is{" "}
+
+ ~* +@all
+
+ . You can modify it to suit your needs.
+
+
+ For more information, please refer to the{" "}
+
+ Redis ACL documentation
+
+ .
+
+
+ }
+ label="Permission Scope"
+ isError={Boolean(error)}
+ errorText={error?.message}
+ >
+
+
+ )}
+ />
+
+
+
+ >
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
index 3f489b04e..f8f2685ec 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
@@ -9,6 +9,7 @@ import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRo
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
+import { RedisCredentialsRotationParametersFields } from "./RedisCredentialsRotationParametersFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx
new file mode 100644
index 000000000..871faf8b6
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx
@@ -0,0 +1,50 @@
+import { useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { GenericFieldLabel } from "@app/components/v2";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+
+import { SecretRotationReviewSection } from "./shared";
+
+export const RedisCredentialsRotationReviewFields = () => {
+ const { watch } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ const [parameters, { username, password }] = watch(["parameters", "secretsMapping"]);
+
+ const { passwordRequirements, permissionScope } = parameters;
+ return (
+ <>
+
+ {permissionScope}
+
+ {passwordRequirements && (
+
+ {passwordRequirements.length}
+
+ {passwordRequirements.required.digits}
+
+
+ {passwordRequirements.required.lowercase}
+
+
+ {passwordRequirements.required.uppercase}
+
+
+ {passwordRequirements.required.symbols}
+
+
+ {passwordRequirements.allowedSymbols}
+
+
+ )}
+
+ {username}
+ {password}
+
+ >
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
index 636cc98cc..05b6ad63c 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
@@ -12,6 +12,7 @@ import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotati
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
+import { RedisCredentialsRotationReviewFields } from "./RedisCredentialsRotationReviewFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
@@ -22,7 +23,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields
};
export const SecretRotationV2ReviewFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx
new file mode 100644
index 000000000..2ffac6a62
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx
@@ -0,0 +1,58 @@
+import { Controller, useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { FormControl, Input } from "@app/components/v2";
+import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
+
+import { SecretsMappingTable } from "./shared";
+
+export const RedisCredentialsRotationSecretsMappingFields = () => {
+ const { control } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ const { rotationOption } = useSecretRotationV2Option(SecretRotation.RedisCredentials);
+
+ const items = [
+ {
+ name: "Username",
+ input: (
+ (
+
+
+
+ )}
+ control={control}
+ name="secretsMapping.username"
+ />
+ )
+ },
+ {
+ name: "Password",
+ input: (
+ (
+
+
+
+ )}
+ control={control}
+ name="secretsMapping.password"
+ />
+ )
+ }
+ ];
+
+ return ;
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
index dd0ce9cab..15338c48a 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
@@ -9,6 +9,7 @@ import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecr
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
+import { RedisCredentialsRotationSecretsMappingFields } from "./RedisCredentialsRotationSecretsMappingFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields
};
export const SecretRotationV2SecretsMappingFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
index a6ebe2f64..199036a8f 100644
--- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
+++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
@@ -12,6 +12,7 @@ import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/typ
import { OktaClientSecretRotationSchema } from "./okta-client-secret-rotation-schema";
import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema";
+import { RedisCredentialsRotationSchema } from "./redis-credentials-rotation-schema";
export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
z
@@ -25,7 +26,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
OracleDBCredentialsRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema,
- OktaClientSecretRotationSchema
+ OktaClientSecretRotationSchema,
+ RedisCredentialsRotationSchema
]),
z.object({ id: z.string().optional() })
)
diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts
new file mode 100644
index 000000000..26fccf963
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts
@@ -0,0 +1,20 @@
+import { z } from "zod";
+
+import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+
+import { PasswordRequirementsSchema } from "./shared";
+
+export const RedisCredentialsRotationSchema = z
+ .object({
+ type: z.literal(SecretRotation.RedisCredentials),
+ parameters: z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().trim().min(1, "Permission scope is required")
+ }),
+ secretsMapping: z.object({
+ username: z.string().trim().min(1, "Username required"),
+ password: z.string().trim().min(1, "Password required")
+ })
+ })
+ .merge(BaseSecretRotationSchema);
diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts
index 99103c794..d0e3dcba1 100644
--- a/frontend/src/helpers/appConnections.ts
+++ b/frontend/src/helpers/appConnections.ts
@@ -36,6 +36,7 @@ import {
OnePassConnectionMethod,
OracleDBConnectionMethod,
PostgresConnectionMethod,
+ RedisConnectionMethod,
TAppConnection,
TeamCityConnectionMethod,
TerraformCloudConnectionMethod,
@@ -113,7 +114,8 @@ export const APP_CONNECTION_MAP: Record<
name: "Netlify",
image: "Netlify.png"
},
- [AppConnection.Okta]: { name: "Okta", image: "Okta.png" }
+ [AppConnection.Okta]: { name: "Okta", image: "Okta.png" },
+ [AppConnection.Redis]: { name: "Redis", image: "Redis.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -155,6 +157,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
+ case RedisConnectionMethod.UsernameAndPassword:
return { name: "Username & Password", icon: faLock };
case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:
diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts
index 2979a7623..d3bb83f19 100644
--- a/frontend/src/helpers/secretRotationsV2.ts
+++ b/frontend/src/helpers/secretRotationsV2.ts
@@ -49,6 +49,11 @@ export const SECRET_ROTATION_MAP: Record<
name: "Okta Client Secret",
image: "Okta.png",
size: 50
+ },
+ [SecretRotation.RedisCredentials]: {
+ name: "Redis Credentials",
+ image: "Redis.png",
+ size: 50
}
};
@@ -61,7 +66,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = {
[SecretRotation.AzureClientSecret]: true,
[SecretRotation.LdapPassword]: false,
[SecretRotation.AwsIamUserSecret]: true,
- [SecretRotation.OktaClientSecret]: true
+ [SecretRotation.OktaClientSecret]: true,
+ [SecretRotation.RedisCredentials]: true
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts
index 7b041b797..e897cf0f0 100644
--- a/frontend/src/hooks/api/appConnections/enums.ts
+++ b/frontend/src/hooks/api/appConnections/enums.ts
@@ -36,5 +36,6 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
- Okta = "okta"
+ Okta = "okta",
+ Redis = "redis"
}
diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts
index 67d8feb48..fdaae2c74 100644
--- a/frontend/src/hooks/api/appConnections/types/app-options.ts
+++ b/frontend/src/hooks/api/appConnections/types/app-options.ts
@@ -168,6 +168,10 @@ export type TAzureAdCsConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureADCS;
};
+export type TRedisConnectionOption = TAppConnectionOptionBase & {
+ app: AppConnection.Redis;
+};
+
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -247,4 +251,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Netlify]: TNetlifyConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption;
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
+ [AppConnection.Redis]: TRedisConnectionOption;
};
diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts
index 8c1d86ca3..9f8df7cfa 100644
--- a/frontend/src/hooks/api/appConnections/types/index.ts
+++ b/frontend/src/hooks/api/appConnections/types/index.ts
@@ -31,6 +31,7 @@ import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection";
+import { TRedisConnection } from "./redis-connection";
import { TRenderConnection } from "./render-connection";
import { TSupabaseConnection } from "./supabase-connection";
import { TTeamCityConnection } from "./teamcity-connection";
@@ -68,6 +69,7 @@ export * from "./okta-connection";
export * from "./oracledb-connection";
export * from "./postgres-connection";
export * from "./railway-connection";
+export * from "./redis-connection";
export * from "./render-connection";
export * from "./supabase-connection";
export * from "./teamcity-connection";
@@ -114,7 +116,8 @@ export type TAppConnection =
| TSupabaseConnection
| TDigitalOceanConnection
| TNetlifyConnection
- | TOktaConnection;
+ | TOktaConnection
+ | TRedisConnection;
export type TAvailableAppConnection = Pick;
diff --git a/frontend/src/hooks/api/appConnections/types/redis-connection.ts b/frontend/src/hooks/api/appConnections/types/redis-connection.ts
new file mode 100644
index 000000000..efbb78b07
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/types/redis-connection.ts
@@ -0,0 +1,21 @@
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
+
+export enum RedisConnectionMethod {
+ UsernameAndPassword = "username-and-password"
+}
+
+export type TRedisConnectionCredentials = {
+ host: string;
+ port: number;
+ username: string;
+ password?: string;
+ sslEnabled: boolean;
+ sslRejectUnauthorized: boolean;
+ sslCertificate?: string;
+};
+
+export type TRedisConnection = TRootAppConnection & { app: AppConnection.Redis } & {
+ method: RedisConnectionMethod.UsernameAndPassword;
+ credentials: TRedisConnectionCredentials;
+};
diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts
index be692cee3..264a6a4a4 100644
--- a/frontend/src/hooks/api/secretRotationsV2/enums.ts
+++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts
@@ -7,7 +7,8 @@ export enum SecretRotation {
AzureClientSecret = "azure-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret",
- OktaClientSecret = "okta-client-secret"
+ OktaClientSecret = "okta-client-secret",
+ RedisCredentials = "redis-credentials"
}
export enum SecretRotationStatus {
diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts
index 06783944b..a04b0e020 100644
--- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts
+++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts
@@ -44,6 +44,11 @@ import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationGeneratedCredentialsResponse
} from "./oracledb-credentials-rotation";
+import {
+ TRedisCredentialsRotation,
+ TRedisCredentialsRotationGeneratedCredentialsResponse,
+ TRedisCredentialsRotationOption
+} from "./redis-credentials-rotation";
export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
@@ -55,6 +60,7 @@ export type TSecretRotationV2 = (
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation
+ | TRedisCredentialsRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
};
@@ -65,7 +71,8 @@ export type TSecretRotationV2Option =
| TAzureClientSecretRotationOption
| TLdapPasswordRotationOption
| TAwsIamUserSecretRotationOption
- | TOktaClientSecretRotationOption;
+ | TOktaClientSecretRotationOption
+ | TRedisCredentialsRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -80,7 +87,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
| TAzureClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse
- | TOktaClientSecretRotationGeneratedCredentialsResponse;
+ | TOktaClientSecretRotationGeneratedCredentialsResponse
+ | TRedisCredentialsRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2,
@@ -133,6 +141,7 @@ export type TSecretRotationOptionMap = {
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption;
+ [SecretRotation.RedisCredentials]: TRedisCredentialsRotationOption;
};
export type TSecretRotationGeneratedCredentialsResponseMap = {
@@ -145,4 +154,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse;
+ [SecretRotation.RedisCredentials]: TRedisCredentialsRotationGeneratedCredentialsResponse;
};
diff --git a/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts
new file mode 100644
index 000000000..58ef1e01a
--- /dev/null
+++ b/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts
@@ -0,0 +1,39 @@
+import { TPasswordRequirements } from "@app/components/secret-rotations-v2/forms/schemas/shared";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+import {
+ TSecretRotationV2Base,
+ TSecretRotationV2GeneratedCredentialsResponseBase
+} from "@app/hooks/api/secretRotationsV2/types/shared";
+
+export type TRedisCredentialsRotation = TSecretRotationV2Base & {
+ type: SecretRotation.RedisCredentials;
+ parameters: {
+ passwordRequirements?: TPasswordRequirements;
+ permissionScope: string;
+ };
+ secretsMapping: {
+ username: string;
+ password: string;
+ };
+};
+
+export type TRedisCredentialsRotationGeneratedCredentials = {
+ username: string;
+ password: string;
+};
+
+export type TRedisCredentialsRotationGeneratedCredentialsResponse =
+ TSecretRotationV2GeneratedCredentialsResponseBase<
+ SecretRotation.RedisCredentials,
+ TRedisCredentialsRotationGeneratedCredentials
+ >;
+
+export type TRedisCredentialsRotationOption = {
+ name: string;
+ type: SecretRotation.RedisCredentials;
+ connection: AppConnection.Redis;
+ template: {
+ secretsMapping: TRedisCredentialsRotation["secretsMapping"];
+ };
+};
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
index e7cca2b90..bb331bf3d 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
@@ -47,6 +47,7 @@ import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm";
import { WindmillConnectionForm } from "./WindmillConnectionForm";
import { ZabbixConnectionForm } from "./ZabbixConnectionForm";
+import { RedisConnectionForm } from "./RedisConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -167,6 +168,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return ;
case AppConnection.Okta:
return ;
+ case AppConnection.Redis:
+ return ;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -320,6 +323,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return ;
case AppConnection.Okta:
return ;
+ case AppConnection.Redis:
+ return ;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx
new file mode 100644
index 000000000..602b9486c
--- /dev/null
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx
@@ -0,0 +1,316 @@
+import { useState } from "react";
+import { Controller, FormProvider, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { Tab } from "@headlessui/react";
+import {
+ Button,
+ FormControl,
+ Input,
+ ModalClose,
+ SecretInput,
+ Select,
+ SelectItem,
+ Switch,
+ TextArea,
+ Tooltip
+} from "@app/components/v2";
+import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import { RedisConnectionMethod, TRedisConnection } from "@app/hooks/api/appConnections";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+
+import {
+ genericAppConnectionFieldsSchema,
+ GenericAppConnectionsFields
+} from "./GenericAppConnectionFields";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
+
+type Props = {
+ appConnection?: TRedisConnection;
+ onSubmit: (formData: FormData) => Promise;
+};
+
+const rootSchema = genericAppConnectionFieldsSchema.extend({
+ app: z.literal(AppConnection.Redis)
+});
+
+const formSchema = z.discriminatedUnion("method", [
+ rootSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: z.object({
+ host: z.string().trim().min(1, "Host required"),
+ port: z.coerce.number().default(6379),
+ username: z.string().trim().min(1, "Username required"),
+ password: z.string().trim().optional(),
+ sslEnabled: z.boolean().default(false),
+ sslRejectUnauthorized: z.boolean().default(true),
+ sslCertificate: z
+ .string()
+ .trim()
+ .transform((value) => value || undefined)
+ .optional()
+ })
+ })
+]);
+
+type FormData = z.infer;
+
+export const RedisConnectionForm = ({ appConnection, onSubmit }: Props) => {
+ const isUpdate = Boolean(appConnection);
+ const [selectedTabIndex, setSelectedTabIndex] = useState(0);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: appConnection ?? {
+ app: AppConnection.Redis,
+ method: RedisConnectionMethod.UsernameAndPassword,
+ credentials: {
+ host: "",
+ port: 6379,
+ username: "",
+ password: "",
+ sslEnabled: false,
+ sslRejectUnauthorized: true,
+ sslCertificate: undefined
+ }
+ }
+ });
+
+ const {
+ handleSubmit,
+ watch,
+ control,
+ formState: { isSubmitting, isDirty }
+ } = form;
+
+ const sslEnabled = watch("credentials.sslEnabled");
+
+ return (
+
+
+
+
+ )}
+ />
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+ );
+};