From 392b72bdbd054f65aa982226a49f80407b2f4866 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 3 Sep 2025 23:48:38 -0300 Subject: [PATCH] Address greptile comments --- .../dynamic-secret/providers/aws-iam.ts | 21 +- .../platform/dynamic-secrets/aws-iam.mdx | 23 + frontend/src/hooks/api/dynamicSecret/types.ts | 6 +- .../AwsIamInputForm.tsx | 179 ++++---- .../EditDynamicSecretAwsIamForm.tsx | 403 +++++++++++------- 5 files changed, 370 insertions(+), 262 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index c64cb9856..601a5da41 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -31,7 +31,8 @@ import { compileUsernameTemplate } from "./templateUtils"; // AWS STS duration constants (in seconds) const AWS_STS_MIN_DURATION = 900; -const AWS_STS_MAX_DURATION_SESSION_TOKEN = 43200; +const AWS_STS_MAX_DURATION_SESSION_TOKEN = 43200; // 12 hours for GetSessionToken +const AWS_STS_MAX_DURATION_ASSUME_ROLE = 3600; // 1 hour for AssumeRole when using temp credentials const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); @@ -200,14 +201,6 @@ export const AwsIamProvider = (): TDynamicProviderFns => { if (providerInputs.method === AwsIamAuthType.AssumeRole) { sensitiveTokens.push(providerInputs.roleArn); } - if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) { - if (providerInputs.method === AwsIamAuthType.AccessKey) { - sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); - } - if (providerInputs.method === AwsIamAuthType.AssumeRole) { - sensitiveTokens.push(providerInputs.roleArn); - } - } const sanitizedErrorMessage = sanitizeString({ unsanitizedString: (err as Error)?.message, tokens: sensitiveTokens @@ -243,9 +236,11 @@ export const AwsIamProvider = (): TDynamicProviderFns => { throw new BadRequestError({ message: "Expiration time must be in the future" }); } - let durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_SESSION_TOKEN); + let durationSeconds: number; if (providerInputs.method === AwsIamAuthType.AssumeRole) { + // AssumeRole has a lower maximum duration when using temporary credentials + durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_ASSUME_ROLE); const appCfg = getConfig(); stsClient = new STSClient({ region: providerInputs.region, @@ -260,8 +255,6 @@ export const AwsIamProvider = (): TDynamicProviderFns => { : undefined }); - durationSeconds = Math.min(durationSeconds, AWS_STS_MAX_DURATION_SESSION_TOKEN); - const assumeRoleRes = await stsClient.send( new AssumeRoleCommand({ RoleArn: providerInputs.roleArn, @@ -290,6 +283,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; } if (providerInputs.method === AwsIamAuthType.AccessKey) { + // GetSessionToken supports longer durations + durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_SESSION_TOKEN); stsClient = new STSClient({ region: providerInputs.region, useFipsEndpoint: crypto.isFipsModeEnabled(), @@ -325,6 +320,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; } if (providerInputs.method === AwsIamAuthType.IRSA) { + // GetSessionToken supports longer durations + durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_SESSION_TOKEN); stsClient = new STSClient({ region: providerInputs.region, useFipsEndpoint: crypto.isFipsModeEnabled(), diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 44bbb1180..2d500a83a 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -5,6 +5,17 @@ description: "Learn how to dynamically generate AWS IAM Users." The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users and temporary credentials on demand based on a configured AWS policy. Infisical supports several authentication methods to connect to your AWS account, including assuming an IAM Role, using IAM Roles for Service Accounts (IRSA) on EKS, or static Access Keys. +## AWS STS Duration Limits + +When using **Temporary Credentials**, AWS STS has specific maximum duration limits: + +- **AssumeRole operations**: Maximum 1 hour (3600 seconds) when using temporary credentials +- **GetSessionToken operations** (Access Key & IRSA): Maximum 12 hours (43200 seconds) + + +**Automatic Duration Adjustment**: If you specify a TTL that exceeds these AWS limits, Infisical will automatically use the maximum allowed duration instead of failing the operation. This ensures your dynamic secrets work reliably within AWS constraints. + + ## Prerequisite Infisical needs an AWS IAM principal (a user or a role) with the required permissions to create and manage other IAM users and temporary credentials. This principal will be responsible for the lifecycle of the dynamically generated users and temporary credentials. @@ -267,6 +278,10 @@ Infisical needs an AWS IAM principal (a user or a role) with the required permis - Include an AWS Session Token - Be valid for the duration specified in Default TTL + + + **Duration Limit**: AssumeRole temporary credentials are limited to 1 hour maximum by AWS. TTL values exceeding this limit will be automatically adjusted to 1 hour. + @@ -479,6 +494,10 @@ Infisical needs an AWS IAM principal (a user or a role) with the required permis - Include an AWS Session Token - Be valid for the duration specified in Default TTL + + + **Duration Limit**: IRSA temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted. + @@ -606,6 +625,10 @@ Infisical needs an AWS IAM principal (a user or a role) with the required permis - Include an AWS Session Token - Be valid for the duration specified in Default TTL + + + **Duration Limit**: Access Key temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted. + diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 616c8eef3..03e018dae 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -102,7 +102,7 @@ export type TDynamicSecretProvider = inputs: | { method: DynamicSecretAwsIamAuth.AccessKey; - credentialType?: DynamicSecretAwsIamCredentialType; + credentialType: DynamicSecretAwsIamCredentialType; accessKey: string; secretAccessKey: string; region: string; @@ -113,7 +113,7 @@ export type TDynamicSecretProvider = } | { method: DynamicSecretAwsIamAuth.AssumeRole; - credentialType?: DynamicSecretAwsIamCredentialType; + credentialType: DynamicSecretAwsIamCredentialType; roleArn: string; region: string; awsPath?: string; @@ -123,7 +123,7 @@ export type TDynamicSecretProvider = } | { method: DynamicSecretAwsIamAuth.IRSA; - credentialType?: DynamicSecretAwsIamCredentialType; + credentialType: DynamicSecretAwsIamCredentialType; region: string; awsPath?: string; policyDocument?: string; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 616010f93..f1c72b580 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -25,85 +25,74 @@ import { WorkspaceEnv } from "@app/hooks/api/types"; import { MetadataForm } from "../../DynamicSecretListView/MetadataForm"; -const formSchema = z.object({ - provider: z.discriminatedUnion("method", [ - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.AccessKey), - credentialType: z - .nativeEnum(DynamicSecretAwsIamCredentialType) - .default(DynamicSecretAwsIamCredentialType.IamUser), - accessKey: z.string().trim().min(1), - secretAccessKey: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array( - z.object({ - key: z.string().trim().min(1).max(128), - value: z.string().trim().min(1).max(256) - }) - ) - .optional() - }), - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), - credentialType: z - .nativeEnum(DynamicSecretAwsIamCredentialType) - .default(DynamicSecretAwsIamCredentialType.IamUser), - roleArn: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array( - z.object({ - key: z.string().trim().min(1).max(128), - value: z.string().trim().min(1).max(256) - }) - ) - .optional() - }), - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.IRSA), - credentialType: z - .nativeEnum(DynamicSecretAwsIamCredentialType) - .default(DynamicSecretAwsIamCredentialType.IamUser), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array( - z.object({ - key: z.string().trim().min(1).max(128), - value: z.string().trim().min(1).max(256) - }) - ) - .optional() - }) - ]), - defaultTTL: z.string().superRefine((val, ctx) => { - const valMs = ms(val); - if (valMs < 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); - // a day - if (valMs > 24 * 60 * 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); - }), - maxTTL: z - .string() - .optional() - .superRefine((val, ctx) => { - if (!val) return; +const formSchema = z + .object({ + provider: z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AccessKey), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + accessKey: z.string().trim().min(1), + secretAccessKey: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array( + z.object({ + key: z.string().trim().min(1).max(128), + value: z.string().trim().min(1).max(256) + }) + ) + .optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + roleArn: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array( + z.object({ + key: z.string().trim().min(1).max(128), + value: z.string().trim().min(1).max(256) + }) + ) + .optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.IRSA), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array( + z.object({ + key: z.string().trim().min(1).max(128), + value: z.string().trim().min(1).max(256) + }) + ) + .optional() + }) + ]), + defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); @@ -111,10 +100,34 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environment: z.object({ name: z.string(), slug: z.string() }), - usernameTemplate: z.string().nullable().optional() -}); + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }), + usernameTemplate: z.string().nullable().optional() + }) + .refine( + (data) => { + if (data.provider.credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials) { + return !data.provider.awsPath || data.provider.awsPath === ""; + } + return true; + }, + { + message: "AWS IAM Path cannot be set when using temporary credentials", + path: ["provider", "awsPath"] + } + ); type TForm = z.infer; type Props = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx index 926bd4482..2354a2731 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx @@ -7,77 +7,103 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2"; import { useGetServerConfig, useUpdateDynamicSecret } from "@app/hooks/api"; -import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; +import { + DynamicSecretAwsIamAuth, + DynamicSecretAwsIamCredentialType, + TDynamicSecret +} from "@app/hooks/api/dynamicSecret/types"; import { slugSchema } from "@app/lib/schemas"; import { MetadataForm } from "../MetadataForm"; -const formSchema = z.object({ - inputs: z.discriminatedUnion("method", [ - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.AccessKey), - accessKey: z.string().trim().min(1), - secretAccessKey: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) - .optional() - }), - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), - roleArn: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) - .optional() - }), - z.object({ - method: z.literal(DynamicSecretAwsIamAuth.IRSA), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional(), - tags: z - .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) - .optional() - }) - ]), - defaultTTL: z.string().superRefine((val, ctx) => { - const valMs = ms(val); - if (valMs < 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); - // a day - if (valMs > 24 * 60 * 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); - }), - maxTTL: z - .string() - .optional() - .superRefine((val, ctx) => { - if (!val) return; +const formSchema = z + .object({ + inputs: z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AccessKey), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + accessKey: z.string().trim().min(1), + secretAccessKey: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) + .optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + roleArn: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) + .optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.IRSA), + credentialType: z + .nativeEnum(DynamicSecretAwsIamCredentialType) + .default(DynamicSecretAwsIamCredentialType.IamUser), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) + .optional() + }) + ]), + defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); // a day if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); - }) - .nullable(), - newName: slugSchema().optional(), - usernameTemplate: z.string().trim().nullable().optional() -}); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }) + .nullable(), + newName: slugSchema().optional(), + usernameTemplate: z.string().trim().nullable().optional() + }) + .refine( + (data) => { + if (data.inputs.credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials) { + return !data.inputs.awsPath || data.inputs.awsPath === ""; + } + return true; + }, + { + message: "AWS IAM Path cannot be set when using temporary credentials", + path: ["inputs", "awsPath"] + } + ); type TForm = z.infer; type Props = { @@ -115,6 +141,7 @@ export const EditDynamicSecretAwsIamForm = ({ } }); const method = watch("inputs.method"); + const credentialType = watch("inputs.credentialType"); const updateDynamicSecret = useUpdateDynamicSecret(); @@ -235,6 +262,39 @@ export const EditDynamicSecretAwsIamForm = ({ )} /> + ( + + <> + +
+ {value === DynamicSecretAwsIamCredentialType.IamUser + ? "Creates temporary IAM users with access keys" + : "Uses STS to generate temporary credentials from your connection. Duration is controlled by the Default TTL setting above."} +
+ +
+ )} + /> {method === DynamicSecretAwsIamAuth.AccessKey && (
)}
- ( - - - - )} - /> + {credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && ( + ( + + + + )} + /> + )} ( @@ -320,93 +387,101 @@ export const EditDynamicSecretAwsIamForm = ({ )} />
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - -