From 233f003e0c9df3156acd18a3aa6f8b8bff7d5cd3 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 3 Sep 2025 22:19:18 -0300 Subject: [PATCH 1/4] Dynamic Secrets: add Temporary Credentials for AWS IAM Roles --- .../dynamic-secret/providers/aws-iam.ts | 383 ++++++++++++++---- .../dynamic-secret/providers/models.ts | 8 + .../platform/dynamic-secrets/aws-iam.mdx | 352 +++++++++++----- frontend/src/hooks/api/dynamicSecret/types.ts | 8 + .../AwsIamInputForm.tsx | 277 ++++++++----- .../CreateDynamicSecretLease.tsx | 18 +- 6 files changed, 745 insertions(+), 301 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 d4fceb674..bc325dece 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -16,7 +16,7 @@ import { PutUserPolicyCommand, RemoveUserFromGroupCommand } from "@aws-sdk/client-iam"; -import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { AssumeRoleCommand, GetSessionTokenCommand, STSClient } from "@aws-sdk/client-sts"; import { z } from "zod"; import { CustomAWSHasher } from "@app/lib/aws/hashing"; @@ -26,9 +26,14 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; +import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; import { compileUsernameTemplate } from "./templateUtils"; +// AWS STS duration constants (in seconds) +const AWS_STS_MIN_DURATION = 900; // 15 minutes +const AWS_STS_MAX_DURATION_SESSION_TOKEN = 43200; // 12 hours for GetSessionToken +const AWS_STS_MAX_DURATION_ASSUME_ROLE = 43200; // 12 hours for AssumeRole + const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); if (!usernameTemplate) return randomUsername; @@ -120,6 +125,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); try { + if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) { + if (providerInputs.method === AwsIamAuthType.AccessKey) { + const stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher, + credentials: { + accessKeyId: providerInputs.accessKey, + secretAccessKey: providerInputs.secretAccessKey + } + }); + + await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION })); + return true; + } + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + const appCfg = getConfig(); + const stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher, + credentials: + appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + ? { + accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + } + : undefined + }); + + await stsClient.send( + new AssumeRoleCommand({ + RoleArn: providerInputs.roleArn, + RoleSessionName: `infisical-validation-${crypto.nativeCrypto.randomUUID()}`, + DurationSeconds: AWS_STS_MIN_DURATION, + ExternalId: projectId + }) + ); + return true; + } + if (providerInputs.method === AwsIamAuthType.IRSA) { + const stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher + }); + + await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION })); + return true; + } + } + const client = await $getClient(providerInputs, projectId); const isConnected = await client .send(new GetUserCommand({})) @@ -137,13 +194,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }); return isConnected; } catch (err) { - const sensitiveTokens = []; + const sensitiveTokens: string[] = []; if (providerInputs.method === AwsIamAuthType.AccessKey) { sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); } 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 @@ -163,102 +228,258 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; metadata: { projectId: string }; }) => { - const { inputs, usernameTemplate, metadata, identity } = data; + const { inputs, usernameTemplate, metadata, identity, expireAt } = data; const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs, metadata.projectId); - const username = generateUsername(usernameTemplate, identity); - const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; - const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }]; + if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) { + try { + let stsClient: STSClient; + let entityId: string; - if (providerInputs.tags && Array.isArray(providerInputs.tags)) { - const additionalTags = providerInputs.tags.map((tag) => ({ - Key: tag.key, - Value: tag.value - })); - awsTags.push(...additionalTags); + const currentTime = Math.floor(Date.now() / 1000); + const requestedDuration = expireAt - currentTime; + + if (requestedDuration <= 0) { + throw new BadRequestError({ message: "Expiration time must be in the future" }); + } + + let durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_SESSION_TOKEN); + + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + const appCfg = getConfig(); + stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher, + credentials: + appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + ? { + accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + } + : undefined + }); + + durationSeconds = Math.min(durationSeconds, AWS_STS_MAX_DURATION_ASSUME_ROLE); + + const assumeRoleRes = await stsClient.send( + new AssumeRoleCommand({ + RoleArn: providerInputs.roleArn, + RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`, + DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION), + ExternalId: metadata.projectId + }) + ); + + if ( + !assumeRoleRes.Credentials?.AccessKeyId || + !assumeRoleRes.Credentials?.SecretAccessKey || + !assumeRoleRes.Credentials?.SessionToken + ) { + throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" }); + } + + entityId = `assume-role-${alphaNumericNanoId(8)}`; + return { + entityId, + data: { + ACCESS_KEY: assumeRoleRes.Credentials.AccessKeyId, + SECRET_ACCESS_KEY: assumeRoleRes.Credentials.SecretAccessKey, + SESSION_TOKEN: assumeRoleRes.Credentials.SessionToken + } + }; + } + if (providerInputs.method === AwsIamAuthType.AccessKey) { + stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher, + credentials: { + accessKeyId: providerInputs.accessKey, + secretAccessKey: providerInputs.secretAccessKey + } + }); + + const sessionTokenRes = await stsClient.send( + new GetSessionTokenCommand({ + DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION) + }) + ); + + if ( + !sessionTokenRes.Credentials?.AccessKeyId || + !sessionTokenRes.Credentials?.SecretAccessKey || + !sessionTokenRes.Credentials?.SessionToken + ) { + throw new BadRequestError({ message: "Failed to get session token - verify credentials and permissions" }); + } + + entityId = `session-token-${alphaNumericNanoId(8)}`; + return { + entityId, + data: { + ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId, + SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey, + SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken + } + }; + } + if (providerInputs.method === AwsIamAuthType.IRSA) { + stsClient = new STSClient({ + region: providerInputs.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher + }); + + const sessionTokenRes = await stsClient.send( + new GetSessionTokenCommand({ + DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION) + }) + ); + + if ( + !sessionTokenRes.Credentials?.AccessKeyId || + !sessionTokenRes.Credentials?.SecretAccessKey || + !sessionTokenRes.Credentials?.SessionToken + ) { + throw new BadRequestError({ + message: "Failed to get session token - verify IRSA credentials and permissions" + }); + } + + entityId = `irsa-session-${alphaNumericNanoId(8)}`; + return { + entityId, + data: { + ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId, + SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey, + SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken + } + }; + } + + throw new BadRequestError({ message: "Unsupported authentication method for temporary credentials" }); + } catch (err) { + const sensitiveTokens: string[] = []; + 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 + }); + throw new BadRequestError({ + message: `Failed to create temporary credentials: ${sanitizedErrorMessage}` + }); + } } - try { - const createUserRes = await client.send( - new CreateUserCommand({ - Path: awsPath, - PermissionsBoundary: permissionBoundaryPolicyArn || undefined, - Tags: awsTags, - UserName: username - }) - ); + if (providerInputs.credentialType === AwsIamCredentialType.IamUser) { + const client = await $getClient(providerInputs, metadata.projectId); - if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); - if (userGroups) { - await Promise.all( - userGroups - .split(",") - .filter(Boolean) - .map((group) => - client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group })) - ) - ); + const username = generateUsername(usernameTemplate, identity); + const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; + const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }]; + + if (providerInputs.tags && Array.isArray(providerInputs.tags)) { + const additionalTags = providerInputs.tags.map((tag) => ({ + Key: tag.key, + Value: tag.value + })); + awsTags.push(...additionalTags); } - if (policyArns) { - await Promise.all( - policyArns - .split(",") - .filter(Boolean) - .map((policyArn) => - client.send( - new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }) - ) - ) - ); - } - if (policyDocument) { - await client.send( - new PutUserPolicyCommand({ - UserName: createUserRes.User.UserName, - PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`, - PolicyDocument: policyDocument + + try { + const createUserRes = await client.send( + new CreateUserCommand({ + Path: awsPath, + PermissionsBoundary: permissionBoundaryPolicyArn || undefined, + Tags: awsTags, + UserName: username }) ); - } - const createAccessKeyRes = await client.send( - new CreateAccessKeyCommand({ - UserName: createUserRes.User.UserName - }) - ); - if (!createAccessKeyRes.AccessKey) - throw new BadRequestError({ message: "Failed to create AWS IAM User access key" }); - - return { - entityId: username, - data: { - ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId, - SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey, - USERNAME: username + if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); + if (userGroups) { + await Promise.all( + userGroups + .split(",") + .filter(Boolean) + .map((group) => + client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group })) + ) + ); } - }; - } catch (err) { - const sensitiveTokens = [username]; - if (providerInputs.method === AwsIamAuthType.AccessKey) { - sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); + if (policyArns) { + await Promise.all( + policyArns + .split(",") + .filter(Boolean) + .map((policyArn) => + client.send( + new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }) + ) + ) + ); + } + if (policyDocument) { + await client.send( + new PutUserPolicyCommand({ + UserName: createUserRes.User.UserName, + PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`, + PolicyDocument: policyDocument + }) + ); + } + + const createAccessKeyRes = await client.send( + new CreateAccessKeyCommand({ + UserName: createUserRes.User.UserName + }) + ); + if (!createAccessKeyRes.AccessKey) + throw new BadRequestError({ message: "Failed to create AWS IAM User access key" }); + + return { + entityId: username, + data: { + ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId, + SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey, + USERNAME: username + } + }; + } catch (err) { + const sensitiveTokens = [username]; + 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 + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } - if (providerInputs.method === AwsIamAuthType.AssumeRole) { - sensitiveTokens.push(providerInputs.roleArn); - } - const sanitizedErrorMessage = sanitizeString({ - unsanitizedString: (err as Error)?.message, - tokens: sensitiveTokens - }); - throw new BadRequestError({ - message: `Failed to create lease from provider: ${sanitizedErrorMessage}` - }); } + + throw new BadRequestError({ message: "Invalid credential type specified" }); }; const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); + + if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) { + return { entityId }; + } + const client = await $getClient(providerInputs, metadata.projectId); const username = entityId; diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index ae1bcfc25..b8782efe0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -32,6 +32,11 @@ export enum AwsIamAuthType { IRSA = "irsa" } +export enum AwsIamCredentialType { + IamUser = "iam-user", + TemporaryCredentials = "temporary-credentials" +} + export enum ElasticSearchAuthTypes { User = "user", ApiKey = "api-key" @@ -202,6 +207,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess( z.discriminatedUnion("method", [ z.object({ method: z.literal(AwsIamAuthType.AccessKey), + credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser), accessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1), region: z.string().trim().min(1), @@ -214,6 +220,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess( }), z.object({ method: z.literal(AwsIamAuthType.AssumeRole), + credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser), roleArn: z.string().trim().min(1, "Role ARN required"), region: z.string().trim().min(1), awsPath: z.string().trim().optional(), @@ -225,6 +232,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess( }), z.object({ method: z.literal(AwsIamAuthType.IRSA), + credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser), region: z.string().trim().min(1), awsPath: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(), diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 28b177c5f..44bbb1180 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -3,49 +3,82 @@ title: "AWS IAM" description: "Learn how to dynamically generate AWS IAM Users." --- -The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users 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. +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. ## Prerequisite -Infisical needs an AWS IAM principal (a user or a role) with the required permissions to create and manage other IAM users. This principal will be responsible for the lifecycle of the dynamically generated users. +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. -```json -{ - "Version": "2012-10-17", - "Statement": [ + + + Required permissions for creating temporary IAM users: + + ```json { - "Effect": "Allow", - "Action": [ - "iam:AttachUserPolicy", - "iam:CreateAccessKey", - "iam:CreateUser", - "iam:DeleteAccessKey", - "iam:DeleteUser", - "iam:DeleteUserPolicy", - "iam:DetachUserPolicy", - "iam:GetUser", - "iam:ListAccessKeys", - "iam:ListAttachedUserPolicies", - "iam:ListGroupsForUser", - "iam:ListUserPolicies", - "iam:PutUserPolicy", - "iam:AddUserToGroup", - "iam:RemoveUserFromGroup", - "iam:TagUser" - ], - "Resource": ["*"] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:AttachUserPolicy", + "iam:CreateAccessKey", + "iam:CreateUser", + "iam:DeleteAccessKey", + "iam:DeleteUser", + "iam:DeleteUserPolicy", + "iam:DetachUserPolicy", + "iam:GetUser", + "iam:ListAccessKeys", + "iam:ListAttachedUserPolicies", + "iam:ListGroupsForUser", + "iam:ListUserPolicies", + "iam:PutUserPolicy", + "iam:AddUserToGroup", + "iam:RemoveUserFromGroup", + "iam:TagUser" + ], + "Resource": ["*"] + } + ] } - ] -} -``` + ``` -To minimize managing user access you can attach a resource in format + To minimize managing user access you can attach a resource in format -> arn:aws:iam::\:user/\ + > arn:aws:iam::\:user/\ -Replace **\** with your AWS account id and **\** with a path to minimize managing user access. + Replace **\** with your AWS account id and **\** with a path to minimize managing user access. + + + + Required permissions for Access Key and Assume Role methods: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "sts:GetSessionToken", + "sts:AssumeRole" + ], + "Resource": ["*"] + } + ] + } + ``` + + + To minimize managing user access you can attach a resource in format + + > arn:aws:iam::\:user/\ + + Replace **\** with your AWS account id and **\** with a path to minimize managing user access. + + @@ -170,43 +203,72 @@ Replace **\** with your AWS account id and **\** w Select *Assume Role* method. - - The ARN of the AWS Role to assume. + + Choose the credential generation approach: + - **IAM User (Default)**: Creates new temporary IAM users in your AWS account + - **Temporary Credentials**: Generates temporary credentials from your role connection - - [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + + The ARN of the AWS Role to assume. The AWS data center region. - - The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. - + + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + - - The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas - + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + - - The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas - + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas. + - - The AWS IAM inline policy that should be attached to the created users. - Multiple values can be provided by separating them with commas - + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas. + - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas. + - Allowed template variables are + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are: + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are: + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + + + Tags to be added to the created IAM User resource. + + + + + When **Credential Type** is set to **Temporary Credentials**: + + + No additional configuration parameters are required. The generated credentials will: + - Inherit the permissions of the assumed role + - Include an AWS Session Token + - Be valid for the duration specified in Default TTL + + + @@ -232,6 +294,18 @@ Replace **\** with your AWS account id and **\** w Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + **Credentials format depends on your chosen credential type:** + + **IAM User credential type:** + - AWS Username + - AWS Access Key ID + - AWS Secret Access Key + + **Temporary Credentials credential type:** + - AWS Access Key ID + - AWS Secret Access Key + - AWS Session Token + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) @@ -342,36 +416,71 @@ Replace **\** with your AWS account id and **\** w Select *IRSA* method. + + Choose the credential generation approach: + - **IAM User**: Creates new temporary IAM users in your AWS account + - **Temporary Credentials**: Generates temporary credentials from your IRSA role connection + The ARN of the AWS IAM Role for the service account to assume. - - [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. - + The AWS data center region. - - The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. - - - The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas - - - The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas - - - The AWS IAM inline policy that should be attached to the created users. - Multiple values can be provided by separating them with commas - - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are + + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + + + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas. + + + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas. + + + + The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas. + + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are: + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are: + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + + + Tags to be added to the created IAM User resource. + + + + + When **Credential Type** is set to **Temporary Credentials**: + + + No additional configuration parameters are required. The generated credentials will: + - Inherit the permissions of the assumed IRSA role + - Include an AWS Session Token + - Be valid for the duration specified in Default TTL + + + After submitting the form, you will see a dynamic secret created in the dashboard. @@ -429,6 +538,12 @@ Replace **\** with your AWS account id and **\** w Select *Access Key* method. + + Choose the credential generation approach: + - **IAM User**: Creates new temporary IAM users in your AWS account + - **Temporary Credentials**: Generates temporary credentials from your access key connection + + The managing AWS IAM User Access Key @@ -437,43 +552,62 @@ Replace **\** with your AWS account id and **\** w The managing AWS IAM User Secret Key - - [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. - - The AWS data center region. - - The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. - + + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + - - The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas - + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + - - The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas - + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas. + - - The AWS IAM inline policy that should be attached to the created users. - Multiple values can be provided by separating them with commas - + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas. + - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas. + - Allowed template variables are + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are: + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters - - Tags to be added to the created IAM User resource. - + Allowed template functions are: + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + + + Tags to be added to the created IAM User resource. + + + + + When **Credential Type** is set to **Temporary Credentials**: + + + No additional configuration parameters are required. The generated credentials will: + - Inherit the permissions of your access key connection + - Include an AWS Session Token + - Be valid for the duration specified in Default TTL + + + @@ -500,6 +634,18 @@ Replace **\** with your AWS account id and **\** w Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + **Credentials format depends on your chosen credential type:** + + **IAM User credential type:** + - AWS Username + - AWS Access Key ID + - AWS Secret Access Key + + **Temporary Credentials credential type:** + - AWS Access Key ID + - AWS Secret Access Key + - AWS Session Token + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 289bc1d04..616c8eef3 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -59,6 +59,11 @@ export enum DynamicSecretAwsIamAuth { IRSA = "irsa" } +export enum DynamicSecretAwsIamCredentialType { + IamUser = "iam-user", + TemporaryCredentials = "temporary-credentials" +} + export type TDynamicSecretProvider = | { type: DynamicSecretProviders.SqlDatabase; @@ -97,6 +102,7 @@ export type TDynamicSecretProvider = inputs: | { method: DynamicSecretAwsIamAuth.AccessKey; + credentialType?: DynamicSecretAwsIamCredentialType; accessKey: string; secretAccessKey: string; region: string; @@ -107,6 +113,7 @@ export type TDynamicSecretProvider = } | { method: DynamicSecretAwsIamAuth.AssumeRole; + credentialType?: DynamicSecretAwsIamCredentialType; roleArn: string; region: string; awsPath?: string; @@ -116,6 +123,7 @@ export type TDynamicSecretProvider = } | { method: DynamicSecretAwsIamAuth.IRSA; + 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 f9ed9967c..616010f93 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 @@ -18,6 +18,7 @@ import { useCreateDynamicSecret } from "@app/hooks/api"; import { useGetServerConfig } from "@app/hooks/api/admin"; import { DynamicSecretAwsIamAuth, + DynamicSecretAwsIamCredentialType, DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { WorkspaceEnv } from "@app/hooks/api/types"; @@ -28,6 +29,9 @@ 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), @@ -47,6 +51,9 @@ const formSchema = z.object({ }), 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(), @@ -65,6 +72,9 @@ const formSchema = z.object({ }), 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(), @@ -137,13 +147,15 @@ export const AwsIamInputForm = ({ environment: isSingleEnvironmentMode ? environments[0] : undefined, usernameTemplate: "{{randomUsername}}", provider: { - method: DynamicSecretAwsIamAuth.AssumeRole + method: DynamicSecretAwsIamAuth.AssumeRole, + credentialType: DynamicSecretAwsIamCredentialType.IamUser } } }); const createDynamicSecret = useCreateDynamicSecret(); const method = watch("provider.method"); + const credentialType = watch("provider.credentialType"); const handleCreateDynamicSecret = async ({ name, @@ -264,6 +276,39 @@ export const AwsIamInputForm = ({ )} /> + ( + + <> + +
+ {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 && ( + ( + + + + )} + /> + )} ( @@ -350,97 +401,105 @@ export const AwsIamInputForm = ({ )} />
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - -