Merge pull request #3470 from Infisical/feat/awsSecretRotationV2

feat(secret-rotation-v2): Add AWS IAM User Secret rotation
This commit is contained in:
carlosmonastyrski
2025-04-25 16:43:22 -03:00
committed by GitHub
55 changed files with 1036 additions and 169 deletions
@@ -0,0 +1,19 @@
import {
AwsIamUserSecretRotationGeneratedCredentialsSchema,
AwsIamUserSecretRotationSchema,
CreateAwsIamUserSecretRotationSchema,
UpdateAwsIamUserSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.AwsIamUserSecret,
server,
responseSchema: AwsIamUserSecretRotationSchema,
createSchema: CreateAwsIamUserSecretRotationSchema,
updateSchema: UpdateAwsIamUserSecretRotationSchema,
generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema
});
@@ -1,6 +1,7 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -12,5 +13,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
> = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
};
@@ -2,6 +2,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -13,7 +14,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
Auth0ClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
@@ -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 AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "AWS IAM User Secret",
type: SecretRotation.AwsIamUserSecret,
connection: AppConnection.AWS,
template: {
secretsMapping: {
accessKeyId: "AWS_ACCESS_KEY_ID",
secretAccessKey: "AWS_SECRET_ACCESS_KEY"
}
}
};
@@ -0,0 +1,123 @@
import AWS from "aws-sdk";
import {
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws";
const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => {
return key.CreateDate ? new Date(key.CreateDate).getTime() : 0;
};
export const awsIamUserSecretRotationFactory: TRotationFactory<
TAwsIamUserSecretRotationWithConnection,
TAwsIamUserSecretRotationGeneratedCredentials
> = (secretRotation) => {
const {
parameters: { region, userName },
connection,
secretsMapping
} = secretRotation;
const $rotateClientSecret = async () => {
const { credentials } = await getAwsConnectionConfig(connection, region);
const iam = new AWS.IAM({ credentials });
const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise();
if (AccessKeyMetadata && AccessKeyMetadata.length > 0) {
// Sort keys by creation date (oldest first)
const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b));
// If we already have 2 keys, delete the oldest one
if (sortedKeys.length >= 2) {
const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId;
if (accessId) {
await iam
.deleteAccessKey({
UserName: userName,
AccessKeyId: accessId
})
.promise();
}
}
}
const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise();
return {
accessKeyId: AccessKey.AccessKeyId,
secretAccessKey: AccessKey.SecretAccessKey
};
};
const issueCredentials: TRotationFactoryIssueCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
generatedCredentials,
callback
) => {
const { credentials } = await getAwsConnectionConfig(connection, region);
const iam = new AWS.IAM({ credentials });
await Promise.all(
generatedCredentials.map((generatedCredential) =>
iam
.deleteAccessKey({
UserName: userName,
AccessKeyId: generatedCredential.accessKeyId
})
.promise()
)
);
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAwsIamUserSecretRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.accessKeyId,
value: generatedCredentials.accessKeyId
},
{
key: secretsMapping.secretAccessKey,
value: generatedCredentials.secretAccessKey
}
];
return secrets;
};
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, AWSRegion } from "@app/services/app-connection/app-connection-enums";
export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z
.object({
accessKeyId: z.string(),
secretAccessKey: z.string()
})
.array()
.min(1)
.max(2);
const AwsIamUserSecretRotationParametersSchema = z.object({
userName: z
.string()
.trim()
.min(1, "Client Name Required")
.describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName),
region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional()
});
const AwsIamUserSecretRotationSecretsMappingSchema = z.object({
accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId),
secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey)
});
export const AwsIamUserSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
accessKeyId: z.string(),
secretAccessKey: z.string()
})
});
export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({
type: z.literal(SecretRotation.AwsIamUserSecret),
parameters: AwsIamUserSecretRotationParametersSchema,
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
});
export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.AwsIamUserSecret
).extend({
parameters: AwsIamUserSecretRotationParametersSchema,
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
});
export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.AwsIamUserSecret
).extend({
parameters: AwsIamUserSecretRotationParametersSchema.optional(),
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional()
});
export const AwsIamUserSecretRotationListItemSchema = z.object({
name: z.literal("AWS IAM User Secret"),
connection: z.literal(AppConnection.AWS),
type: z.literal(SecretRotation.AwsIamUserSecret),
template: AwsIamUserSecretRotationTemplateSchema
});
@@ -0,0 +1,24 @@
import { z } from "zod";
import { TAwsConnection } from "@app/services/app-connection/aws";
import {
AwsIamUserSecretRotationGeneratedCredentialsSchema,
AwsIamUserSecretRotationListItemSchema,
AwsIamUserSecretRotationSchema,
CreateAwsIamUserSecretRotationSchema
} from "./aws-iam-user-secret-rotation-schemas";
export type TAwsIamUserSecretRotation = z.infer<typeof AwsIamUserSecretRotationSchema>;
export type TAwsIamUserSecretRotationInput = z.infer<typeof CreateAwsIamUserSecretRotationSchema>;
export type TAwsIamUserSecretRotationListItem = z.infer<typeof AwsIamUserSecretRotationListItemSchema>;
export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & {
connection: TAwsConnection;
};
export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer<
typeof AwsIamUserSecretRotationGeneratedCredentialsSchema
>;
@@ -0,0 +1,3 @@
export * from "./aws-iam-user-secret-rotation-constants";
export * from "./aws-iam-user-secret-rotation-schemas";
export * from "./aws-iam-user-secret-rotation-types";
@@ -1,7 +1,8 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
Auth0ClientSecret = "auth0-client-secret",
AwsIamUserSecret = "aws-iam-user-secret"
}
export enum SecretRotationStatus {
@@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -18,7 +19,8 @@ import {
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {
@@ -3,12 +3,14 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};
@@ -77,6 +77,7 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = {
@@ -114,7 +115,8 @@ type TRotationFactoryImplementation = TRotationFactory<
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({
@@ -12,6 +12,13 @@ import {
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import {
TAwsIamUserSecretRotation,
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationInput,
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@@ -27,26 +34,34 @@ import {
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
export type TSecretRotationV2 =
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
| TAuth0ClientSecretRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
| TAuth0ClientSecretRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
| TAuth0ClientSecretRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
| TAuth0ClientSecretRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
@@ -4,8 +4,11 @@ import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
Auth0ClientSecretRotationSchema,
AwsIamUserSecretRotationSchema
]);
+8
View File
@@ -2060,6 +2060,10 @@ export const SecretRotations = {
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in."
}
},
SECRETS_MAPPING: {
@@ -2070,6 +2074,10 @@ export const SecretRotations = {
AUTH0_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
},
AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
}
}
};
@@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
return { kmsKeys };
}
});
server.route({
method: "GET",
url: `/:connectionId/users`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
iamUsers: z
.object({
UserName: z.string(),
Arn: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const iamUsers = await server.services.appConnection.aws.listIamUsers(
{
connectionId
},
req.permission
);
return { iamUsers };
}
});
};
@@ -168,6 +168,10 @@ export type TListAwsConnectionKmsKeys = {
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
export type TListAwsConnectionIamUsers = {
connectionId: string;
};
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig
) => Promise<TAppConnection["credentials"]>;
@@ -2,7 +2,10 @@ import AWS from "aws-sdk";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
import {
TListAwsConnectionIamUsers,
TListAwsConnectionKmsKeys
} from "@app/services/app-connection/app-connection-types";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
return kmsKeys;
};
const listAwsIamUsers = async (appConnection: TAwsConnection) => {
const { credentials } = await getAwsConnectionConfig(appConnection);
const iam = new AWS.IAM({ credentials });
const userEntries: AWS.IAM.User[] = [];
let userMarker: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise();
userEntries.push(...(response.Users || []));
userMarker = response.Marker;
} while (userMarker);
return userEntries;
};
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listKmsKeys = async (
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
@@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
return kmsKeys;
};
const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
const iamUsers = await listAwsIamUsers(appConnection);
return iamUsers;
};
return {
listKmsKeys
listKmsKeys,
listIamUsers
};
};