Add Azure Client Secrets Rotation

This commit is contained in:
carlosmonastyrski
2025-04-16 05:04:41 -03:00
parent 7d97a76ecc
commit 1184ea1b11
36 changed files with 861 additions and 36 deletions
@@ -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 AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "Azure Client Secret",
type: SecretRotation.AzureClientSecret,
connection: AppConnection.AzureClientSecrets,
template: {
secretsMapping: {
clientId: "AZURE_CLIENT_ID",
clientSecret: "AZURE_CLIENT_SECRET"
}
}
};
@@ -0,0 +1,155 @@
import {
AzureAddPasswordResponse,
TAzureClientSecretRotationGeneratedCredentials,
TAzureClientSecretRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { request } from "@app/lib/config/request";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets";
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
export const azureClientSecretRotationFactory: TRotationFactory<
TAzureClientSecretRotationWithConnection,
TAzureClientSecretRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { appId },
secretsMapping,
rotationInterval
} = secretRotation;
/**
* Creates a new client secret for the Azure app.
*/
const $rotateClientSecret = async () => {
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${appId}/addPassword`;
await blockLocalAndPrivateIpAddresses(endpoint);
const endDateTime = new Date();
endDateTime.setDate(endDateTime.getDate() + rotationInterval);
try {
const { data } = await request.post<AzureAddPasswordResponse>(
endpoint,
{
passwordCredential: {
displayName: "Infisical Auto-Rotated Secret",
endDateTime: endDateTime.toISOString()
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
if (!data?.secretText || !data?.keyId) {
throw new Error("Invalid response from Azure: missing secretText or keyId.");
}
return {
clientSecret: data.secretText,
clientId: data.keyId
};
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to add client secret to Azure app ${appId}: ${message}`);
}
};
/**
* Revokes a client secret from the Azure app using its keyId.
*/
const revokeCredential = async (clientId: string) => {
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${appId}/removePassword`;
await blockLocalAndPrivateIpAddresses(endpoint);
try {
await request.post(
endpoint,
{ keyId: clientId },
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to remove client secret with keyId ${clientId} from app ${appId}: ${message}`);
}
};
/**
* Issues a new set of credentials.
*/
const issueCredentials: TRotationFactoryIssueCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
/**
* Revokes a list of credentials.
*/
const revokeCredentials: TRotationFactoryRevokeCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
credentials,
callback
) => {
if (!credentials?.length) return callback();
await Promise.all(credentials.map(({ clientId }) => revokeCredential(clientId)));
return callback();
};
/**
* Rotates credentials by issuing new ones and revoking the old.
*/
const rotateCredentials: TRotationFactoryRotateCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
oldCredentials,
callback
) => {
const newCredentials = await $rotateClientSecret();
if (oldCredentials?.clientId) {
await revokeCredential(oldCredentials.clientId);
}
return callback(newCredentials);
};
/**
* Maps the generated credentials into the secret payload format.
*/
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAzureClientSecretRotationGeneratedCredentials> = ({
clientSecret,
clientId
}) => [
{ key: secretsMapping.clientSecret, value: clientSecret },
{ key: secretsMapping.clientId, value: clientId }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};
@@ -0,0 +1,68 @@
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";
export const AzureClientSecretRotationGeneratedCredentialsSchema = z
.object({
clientId: z.string(),
clientSecret: z.string()
})
.array()
.min(1)
.max(2);
const AzureClientSecretRotationParametersSchema = z.object({
appId: z.string().trim().min(1, "Client ID Required").describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appId),
appName: z
.string()
.trim()
.min(1, "App Name Required")
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName)
});
const AzureClientSecretRotationSecretsMappingSchema = z.object({
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientId),
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientSecret)
});
export const AzureClientSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
clientId: z.string(),
clientSecret: z.string()
})
});
export const AzureClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AzureClientSecret).extend({
type: z.literal(SecretRotation.AzureClientSecret),
parameters: AzureClientSecretRotationParametersSchema,
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
});
export const CreateAzureClientSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.AzureClientSecret
).extend({
parameters: AzureClientSecretRotationParametersSchema,
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
});
export const UpdateAzureClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.AzureClientSecret
).extend({
parameters: AzureClientSecretRotationParametersSchema.optional(),
secretsMapping: AzureClientSecretRotationSecretsMappingSchema.optional()
});
export const AzureClientSecretRotationListItemSchema = z.object({
name: z.literal("Azure Client Secret"),
connection: z.literal(AppConnection.AzureClientSecrets),
type: z.literal(SecretRotation.AzureClientSecret),
template: AzureClientSecretRotationTemplateSchema
});
@@ -0,0 +1,41 @@
import { z } from "zod";
import { TAzureClientSecretsConnection } from "@app/services/app-connection/azure-client-secrets";
import {
AzureClientSecretRotationGeneratedCredentialsSchema,
AzureClientSecretRotationListItemSchema,
AzureClientSecretRotationSchema,
CreateAzureClientSecretRotationSchema
} from "./azure-client-secret-rotation-schemas";
export type TAzureClientSecretRotation = z.infer<typeof AzureClientSecretRotationSchema>;
export type TAzureClientSecretRotationInput = z.infer<typeof CreateAzureClientSecretRotationSchema>;
export type TAzureClientSecretRotationListItem = z.infer<typeof AzureClientSecretRotationListItemSchema>;
export type TAzureClientSecretRotationWithConnection = TAzureClientSecretRotation & {
connection: TAzureClientSecretsConnection;
};
export type TAzureClientSecretRotationGeneratedCredentials = z.infer<
typeof AzureClientSecretRotationGeneratedCredentialsSchema
>;
export interface TAzureClientSecretRotationParameters {
appId: string;
keyId?: string;
displayName?: string;
}
export interface TAzureClientSecretRotationSecretsMapping {
appId: string;
clientSecret: string;
keyId: string;
}
export interface AzureAddPasswordResponse {
secretText: string;
keyId: string;
}
@@ -0,0 +1,3 @@
export * from "./azure-client-secret-rotation-constants";
export * from "./azure-client-secret-rotation-schemas";
export * from "./azure-client-secret-rotation-types";