Merge pull request #4564 from Infisical/daniel/redis-secret-rotation-new

feat: redis app connection & secret rotation
This commit is contained in:
Daniel Hougaard
2025-09-24 03:13:38 +04:00
committed by GitHub
81 changed files with 1800 additions and 38 deletions

View File

@@ -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

View File

@@ -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
};

View File

@@ -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
});

View File

@@ -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) => {

View File

@@ -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";

View File

@@ -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"
}
}
};

View File

@@ -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<TRedisCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateAclUser();
return callback(credentials);
};
/**
* Revokes a list of credentials.
*/
const revokeCredentials: TRotationFactoryRevokeCredentials<TRedisCredentialsRotationGeneratedCredentials> = 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<TRedisCredentialsRotationGeneratedCredentials> = 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<TRedisCredentialsRotationGeneratedCredentials> = ({
username,
password
}) => [
{ key: secretsMapping.username, value: username },
{ key: secretsMapping.password, value: password }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -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
});

View File

@@ -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<typeof RedisCredentialsRotationSchema>;
export type TRedisCredentialsRotationInput = z.infer<typeof CreateRedisCredentialsRotationSchema>;
export type TRedisCredentialsRotationListItem = z.infer<typeof RedisCredentialsRotationListItemSchema>;
export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
connection: TRedisConnection;
};
export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
typeof RedisCredentialsRotationGeneratedCredentialsSchema
>;

View File

@@ -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 {

View File

@@ -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<SecretRotation, TSecretRotationV2List
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[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<SecretRotation, AppConnection> = {
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
[SecretRotation.RedisCredentials]: AppConnection.Redis
};

View File

@@ -85,6 +85,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { redisCredentialsRotationFactory } from "./redis-credentials/redis-credentials-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = {
@@ -132,7 +133,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({

View File

@@ -66,6 +66,13 @@ import {
TPostgresCredentialsRotationListItem,
TPostgresCredentialsRotationWithConnection
} from "./postgres-credentials";
import {
TRedisCredentialsRotation,
TRedisCredentialsRotationGeneratedCredentials,
TRedisCredentialsRotationInput,
TRedisCredentialsRotationListItem,
TRedisCredentialsRotationWithConnection
} from "./redis-credentials/redis-credentials-rotation-types";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums";
@@ -78,7 +85,8 @@ export type TSecretRotationV2 =
| TAzureClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation;
| TOktaClientSecretRotation
| TRedisCredentialsRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
@@ -89,7 +97,8 @@ export type TSecretRotationV2WithConnection =
| TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection;
| TOktaClientSecretRotationWithConnection
| TRedisCredentialsRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
@@ -97,7 +106,8 @@ export type TSecretRotationV2GeneratedCredentials =
| TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials
| TOktaClientSecretRotationGeneratedCredentials;
| TOktaClientSecretRotationGeneratedCredentials
| TRedisCredentialsRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
@@ -108,7 +118,8 @@ export type TSecretRotationV2Input =
| TAzureClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput;
| TOktaClientSecretRotationInput
| TRedisCredentialsRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
@@ -119,7 +130,8 @@ export type TSecretRotationV2ListItem =
| TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem;
| TOktaClientSecretRotationListItem
| TRedisCredentialsRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;

View File

@@ -9,6 +9,7 @@ import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { RedisCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
@@ -19,5 +20,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
AzureClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema
OktaClientSecretRotationSchema,
RedisCredentialsRotationSchema
]);

View File

@@ -2686,9 +2686,16 @@ export const SecretRotations = {
},
OKTA_CLIENT_SECRET: {
clientId: "The ID of the Okta Application to rotate the client secret for."
},
REDIS_CREDENTIALS: {
permissionScope: "The ACL permission scope to assign to the issued Redis users."
}
},
SECRETS_MAPPING: {
REDIS_CREDENTIALS: {
username: "The name of the secret that the username will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."
},
SQL_CREDENTIALS: {
username: "The name of the secret that the active username will be mapped to.",
password: "The name of the secret that the generated password will be mapped to."

View File

@@ -93,6 +93,7 @@ import {
RailwayConnectionListItemSchema,
SanitizedRailwayConnectionSchema
} from "@app/services/app-connection/railway";
import { RedisConnectionListItemSchema, SanitizedRedisConnectionSchema } from "@app/services/app-connection/redis";
import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
@@ -156,7 +157,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -197,7 +199,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -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<AppConnection, (server:
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter
};

View File

@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateRedisConnectionSchema,
SanitizedRedisConnectionSchema,
UpdateRedisConnectionSchema
} from "@app/services/app-connection/redis";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRedisConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Redis,
server,
sanitizedResponseSchema: SanitizedRedisConnectionSchema,
createSchema: CreateRedisConnectionSchema,
updateSchema: UpdateRedisConnectionSchema
});
};

View File

@@ -36,7 +36,8 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
Okta = "okta"
Okta = "okta",
Redis = "redis"
}
export enum AWSRegion {

View File

@@ -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 (

View File

@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[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<AppConnection, AppConnectionPlanType> = {
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular
[AppConnection.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular
};

View File

@@ -99,6 +99,7 @@ import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRedisConnectionCredentialsSchema } from "./redis";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
@@ -166,7 +167,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({

View File

@@ -179,6 +179,12 @@ import {
TRailwayConnectionInput,
TValidateRailwayConnectionCredentialsSchema
} from "./railway";
import {
TRedisConnection,
TRedisConnectionConfig,
TRedisConnectionInput,
TValidateRedisConnectionCredentialsSchema
} from "./redis";
import {
TRenderConnection,
TRenderConnectionConfig,
@@ -261,6 +267,7 @@ export type TAppConnection = { id: string } & (
| TDigitalOceanConnection
| TNetlifyConnection
| TOktaConnection
| TRedisConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -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;

View File

@@ -0,0 +1,4 @@
export * from "./redis-connection-enums";
export * from "./redis-connection-fns";
export * from "./redis-connection-schemas";
export * from "./redis-connection-types";

View File

@@ -0,0 +1,3 @@
export enum RedisConnectionMethod {
UsernameAndPassword = "username-and-password"
}

View File

@@ -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();
}
};

View File

@@ -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)
});

View File

@@ -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<typeof RedisConnectionSchema>;
export type TRedisConnectionInput = z.infer<typeof CreateRedisConnectionSchema> & {
app: AppConnection.Redis;
};
export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
export type TRedisConnectionConfig = DiscriminativePick<TRedisConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/redis/available"
---

View File

@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/redis"
---
<Note>
Check out the configuration docs for [Redis Connections](/integrations/app-connections/redis) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/redis/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/redis/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/redis/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/redis"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/redis/{connectionId}"
---
<Note>
Check out the configuration docs for [Redis Connections](/integrations/app-connections/redis) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/redis-credentials"
---
<Note>
Check out the configuration docs for [Redis
Credentials Rotations](/documentation/platform/secret-rotation/redis-credentials) to learn how to obtain the
required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/redis-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/redis-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/redis-credentials/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/redis-credentials/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/redis-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/redis-credentials/{rotationId}/rotate-secrets"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/redis-credentials/{rotationId}"
---
<Note>
Check out the configuration docs for [Redis
Credentials Rotations](/documentation/platform/secret-rotation/redis-credentials) to learn how to obtain the
required parameters.
</Note>

View File

@@ -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": [

View File

@@ -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
<Tabs>
<Tab title="Infisical UI">
1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown.
![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png)
2. Select the **Redis Credentials** option.
![Select Redis Credentials](/images/secret-rotations-v2/redis-credentials/select-redis-credentials-option.png)
3. Select the **Redis Connection** to use and configure the rotation behavior. Then click **Next**.
![Rotation Configuration](/images/secret-rotations-v2/redis-credentials/redis-credentials-configuration.png)
- **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**.
![Rotation Parameters](/images/secret-rotations-v2/redis-credentials/redis-credentials-parameters.png)
- **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**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/redis-credentials/redis-credentials-secrets-mapping.png)
- **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**.
![Rotation Details](/images/secret-rotations-v2/redis-credentials/redis-credentials-details.png)
- **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**.
![Rotation Review](/images/secret-rotations-v2/redis-credentials/redis-credentials-confirm.png)
8. Your **Redis Credentials** are now available for use via the mapped secrets.
![Rotation Created](/images/secret-rotations-v2/redis-credentials/redis-credentials-created.png)
</Tab>
<Tab title="API">
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": "<string>",
"description": "<string>",
"connectionId": "<redis-connection-id>",
"environment": "dev|staging|prod",
"secretPath": "<string>",
"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"
}
}
}
```
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

View File

@@ -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
<Steps>
<Step title="Create a Redis user">
Infisical recommends creating a designated user in your Redis database for your connection.
```bash
ACL SETUSER user_manager on >[ENTER-YOUR-USER-PASSWORD]
```
</Step>
<Step title="Grant Relevant Permissions">
Depending on how you intend to use your Redis connection, you'll need to grant one or more of the following permissions.
<Tip>
To learn more about Redis's permission system, please visit their [documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/).
</Tip>
<Tabs>
<Tab title="Secret Rotation">
For Secret Rotations, your Infisical user will require the ability to set and delete users:
```bash
ACL SETUSER user_manager +acl|setuser +acl|deluser ~*
```
</Tab>
</Tabs>
</Step>
</Steps>
## Create Redis Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Redis Connection">
Click the **+ Add Connection** button and select the **Redis Connection** option from the available integrations.
![Select Redis Connection](/images/app-connections/redis/redis-app-connection-option.png)
</Step>
<Step title="Fill out the Redis Connection Modal">
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.
![Redis Connection Modal](/images/app-connections/redis/redis-app-connection-form.png)
</Step>
<Step title="Connection Created">
After clicking Create, your **Redis Connection** is established and ready to use with your Infisical project.
![Redis Connection Created](/images/app-connections/redis/redis-app-connection-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
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": "<redis-host>",
"port": 6379,
"username": "<redis-username>",
"sslEnabled": true,
"sslRejectUnauthorized": false,
"sslCertificate": "<redis-ssl-certificate>"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -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"}

View File

@@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -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 (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Username">{activeCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{activeCredentials?.password}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Username">{inactiveCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{inactiveCredentials?.password}
</CredentialDisplay>
</>
}
/>
);
};

View File

@@ -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 = (
<ViewRedisCredentialsRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
default:
throw new Error("Unhandled View Generated Credential Rotation Type");
}

View File

@@ -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 (
<>
<div>
<Controller
control={control}
name="parameters.permissionScope"
defaultValue={""}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-[40rem] w-full"
tooltipText={
<div className="flex flex-col gap-4">
<p>
This is the access control permissions that will be set for the issued Redis
users. The format must be a valid Redis ACL pattern.
</p>
<p>
The default value is{" "}
<code className="rounded bg-mineshaft-700 px-1 py-0.5 font-mono font-medium text-bunker-300">
~* +@all
</code>
. You can modify it to suit your needs.
</p>
<p>
For more information, please refer to the{" "}
<a
className="font-medium text-primary-500 underline hover:text-primary-600"
href="https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/"
target="_blank"
rel="noopener noreferrer"
>
Redis ACL documentation
</a>
.
</p>
</div>
}
label="Permission Scope"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="~* +@read @write" />
</FormControl>
)}
/>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Password Requirements</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1 rounded border border-mineshaft-600 bg-mineshaft-700 px-3 pt-3">
<Controller
control={control}
name="parameters.passwordRequirements.length"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.length}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The length of the password to generate"
>
<Input
type="number"
min={1}
max={250}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.digits"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.digits}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digit Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of digits"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.lowercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.lowercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Lowercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of lowercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.uppercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.uppercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Uppercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of uppercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.symbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.symbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbol Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of symbols"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.allowedSymbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.allowedSymbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Symbols"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Symbols to use in generated password"
>
<Input
placeholder="-_.~!*"
size="sm"
{...field}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</div>
</>
);
};

View File

@@ -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, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
[SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {

View File

@@ -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 (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="Permission Scope">{permissionScope}</GenericFieldLabel>
</SecretRotationReviewSection>
{passwordRequirements && (
<SecretRotationReviewSection label="Password Requirements">
<GenericFieldLabel label="Length">{passwordRequirements.length}</GenericFieldLabel>
<GenericFieldLabel label="Minimum Digits">
{passwordRequirements.required.digits}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Lowercase Characters">
{passwordRequirements.required.lowercase}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Uppercase Characters">
{passwordRequirements.required.uppercase}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Symbols">
{passwordRequirements.required.symbols}
</GenericFieldLabel>
<GenericFieldLabel label="Allowed Symbols">
{passwordRequirements.allowedSymbols}
</GenericFieldLabel>
</SecretRotationReviewSection>
)}
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Username">{username}</GenericFieldLabel>
<GenericFieldLabel label="Password">{password}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};

View File

@@ -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, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
@@ -22,7 +23,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields,
[SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields
};
export const SecretRotationV2ReviewFields = () => {

View File

@@ -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: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.username}
/>
</FormControl>
)}
control={control}
name="secretsMapping.username"
/>
)
},
{
name: "Password",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.password}
/>
</FormControl>
)}
control={control}
name="secretsMapping.password"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};

View File

@@ -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, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields,
[SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields
};
export const SecretRotationV2SecretsMappingFields = () => {

View File

@@ -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() })
)

View File

@@ -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);

View File

@@ -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:

View File

@@ -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, AppConnectio
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
[SecretRotation.RedisCredentials]: AppConnection.Redis
};
// if a rotation can potentially have downtime due to rotating a single credential set this to false
@@ -74,7 +80,8 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[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"]) => {

View File

@@ -36,5 +36,6 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
Okta = "okta"
Okta = "okta",
Redis = "redis"
}

View File

@@ -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;
};

View File

@@ -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<TAppConnection, "name" | "id" | "projectId">;

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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;
};

View File

@@ -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"];
};
};

View File

@@ -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 <NetlifyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
case AppConnection.Redis:
return <RedisConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -320,6 +323,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <DigitalOceanConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Redis:
return <RedisConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -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<void>;
};
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<typeof formSchema>;
export const RedisConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
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 (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Redis].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(RedisConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
Configuration
</Tab>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
SSL ({sslEnabled ? "Enabled" : "Disabled"})
</Tab>
</Tab.List>
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
<Tab.Panel>
<div className="mt-[0.675rem] flex items-start gap-2">
<Controller
name="credentials.host"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Host"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.port"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="w-28"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Port"
>
<Input type="number" {...field} />
</FormControl>
)}
/>
</div>
<div className="mb-[0.675rem] flex items-start gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
className="flex-1"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
className="flex-1"
>
<SecretInput
containerClassName="text-gray-400 w-full group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</Tab.Panel>
<Tab.Panel>
<Controller
name="credentials.sslEnabled"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Enable SSL
</Switch>
</FormControl>
)}
/>
<Controller
name="credentials.sslCertificate"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
isOptional
>
<TextArea
className="h-[3.5rem] !resize-none"
{...field}
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
<Controller
name="credentials.sslRejectUnauthorized"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className={sslEnabled ? "" : "opacity-50"}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-reject-unauthorized"
thumbClassName="bg-mineshaft-800"
isChecked={sslEnabled ? value : false}
onCheckedChange={onChange}
isDisabled={!sslEnabled}
>
<p className="w-[9.5rem]">
Reject Unauthorized
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will only connect to the server if it has a
valid, trusted SSL certificate.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Database"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};