feat: redis app connection & secret rotation

This commit is contained in:
Daniel Hougaard
2025-09-20 06:12:08 +04:00
parent f26eb355f0
commit fe3a46a9e7
50 changed files with 1377 additions and 37 deletions
@@ -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";
@@ -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"
}
}
};
@@ -0,0 +1,170 @@
/* 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";
export const redisCredentialsRotationFactory: TRotationFactory<
TRedisCredentialsRotationWithConnection,
TRedisCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
const { connection, secretsMapping, parameters } = secretRotation;
const $getClient = async () => {
let conn: Redis | null = null;
try {
conn = new Redis({
username: connection.credentials.username,
host: connection.credentials.host,
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 () => {
const client = await $getClient();
const username = generatePassword({
length: 32,
required: {
symbols: 0,
digits: 5,
uppercase: 5,
lowercase: 5
}
});
const password = generatePassword(parameters.passwordRequirements || DEFAULT_PASSWORD_REQUIREMENTS);
try {
// 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 || "~* +@all").split(" ");
await client.call("ACL", "SETUSER", username, `>${password}`, "on", ...permissionParts);
return {
username,
password
};
} catch (error: unknown) {
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
/**
* Revokes a ACL password from the Redis server using its username and password.
*/
const revokeCredential = async (username: string) => {
const client = await $getClient();
try {
await client.call("ACL", "DELUSER", username);
} catch (error: unknown) {
throw new BadRequestError({
message: "Unable to revoke credential: verify credentials"
});
}
};
/**
* 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
};
};
@@ -0,0 +1,75 @@
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().optional().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: z.object({
passwordRequirements: PasswordRequirementsSchema.optional(),
permissionScope: z.string().optional()
}),
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
});
export const CreateRedisCredentialsRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.RedisCredentials
).extend({
parameters: z.object({
passwordRequirements: PasswordRequirementsSchema.optional(),
permissionScope: z.string().optional()
}),
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
});
export const UpdateRedisCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.RedisCredentials
).extend({
parameters: z.object({
passwordRequirements: PasswordRequirementsSchema.optional(),
permissionScope: z.string().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
});
@@ -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
>;