diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index fa1a80ac3..c38a8f146 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; - await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); + await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { + projectId: folder.projectId + }); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); return; } @@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all( dynamicSecretLeases.map(({ externalEntityId }) => - selectedProvider.revoke(decryptedStoredInput, externalEntityId) + selectedProvider.revoke(decryptedStoredInput, externalEntityId, { + projectId: folder.projectId + }) ) ); } diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index f3f3f3acd..14666cb40 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -135,7 +135,8 @@ export const dynamicSecretLeaseServiceFactory = ({ result = await selectedProvider.create({ inputs: decryptedStoredInput, expireAt: expireAt.getTime(), - usernameTemplate: dynamicSecretCfg.usernameTemplate + usernameTemplate: dynamicSecretCfg.usernameTemplate, + metadata: { projectId } }); } catch (error: unknown) { if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { @@ -237,7 +238,8 @@ export const dynamicSecretLeaseServiceFactory = ({ const { entityId } = await selectedProvider.renew( decryptedStoredInput, dynamicSecretLease.externalEntityId, - expireAt.getTime() + expireAt.getTime(), + { projectId } ); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); @@ -313,7 +315,7 @@ export const dynamicSecretLeaseServiceFactory = ({ ) as object; const revokeResponse = await selectedProvider - .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) + .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId }) .catch(async (err) => { // only propogate this error if forced is false if (!isForced) return { error: err as Error }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 16ac10716..b502bf9f3 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({ throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); const selectedProvider = dynamicSecretProviders[provider.type]; - const inputs = await selectedProvider.validateProviderInputs(provider.inputs); + const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId }); let selectedGatewayId: string | null = null; if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { @@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({ selectedGatewayId = gateway.id; } - const isConnected = await selectedProvider.validateConnection(provider.inputs); + const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ @@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; - const updatedInput = await selectedProvider.validateProviderInputs(newInput); + const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId }); let selectedGatewayId: string | null = null; if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { @@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({ selectedGatewayId = gateway.id; } - const isConnected = await selectedProvider.validateConnection(newInput); + const isConnected = await selectedProvider.validateConnection(newInput, { projectId }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => { @@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; + const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput, { + projectId + })) as object; return { ...dynamicSecretCfg, inputs: providerInputs }; }; 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 9d8e10f60..b40eb69b3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -16,13 +16,16 @@ import { PutUserPolicyCommand, RemoveUserFromGroupCommand } from "@aws-sdk/client-iam"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { randomUUID } from "crypto"; import handlebars from "handlebars"; import { z } from "zod"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; +import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; const generateUsername = (usernameTemplate?: string | null) => { const randomUsername = alphaNumericNanoId(32); @@ -40,7 +43,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return providerInputs; }; - const $getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer, projectId: string) => { + const appCfg = getConfig(); + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + const stsClient = new STSClient({ + region: providerInputs.region, + 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 // if hosting on AWS + }); + + const command = new AssumeRoleCommand({ + RoleArn: providerInputs.roleArn, + RoleSessionName: `infisical-dynamic-secret-${randomUUID()}`, + DurationSeconds: 900, // 15 mins + ExternalId: projectId + }); + + const assumeRes = await stsClient.send(command); + + if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) { + throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" }); + } + const client = new IAMClient({ + region: providerInputs.region, + credentials: { + accessKeyId: assumeRes.Credentials?.AccessKeyId, + secretAccessKey: assumeRes.Credentials?.SecretAccessKey, + sessionToken: assumeRes.Credentials?.SessionToken + } + }); + return client; + } + const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -52,19 +91,36 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return client; }; - const validateConnection = async (inputs: unknown) => { + const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); - - const isConnected = await client.send(new GetUserCommand({})).then(() => true); + const client = await $getClient(providerInputs, projectId); + const isConnected = await client + .send(new GetUserCommand({})) + .then(() => true) + .catch((err) => { + const message = (err as Error)?.message; + if ( + providerInputs.method === AwsIamAuthType.AssumeRole && + // assume role will throw an error asking to provider username, but if so this has access in aws correctly + message.includes("Must specify userName when calling with non-User credentials") + ) { + return true; + } + throw err; + }); return isConnected; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + metadata: { projectId: string }; + }) => { + const { inputs, usernameTemplate, metadata } = data; const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); + const client = await $getClient(providerInputs, metadata.projectId); const username = generateUsername(usernameTemplate); const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; @@ -76,6 +132,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { UserName: username }) ); + if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); if (userGroups) { await Promise.all( @@ -125,9 +182,9 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; }; - const revoke = async (inputs: unknown, entityId: string) => { + const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); + 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 91d26da32..e6fa27492 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -20,6 +20,11 @@ export enum SqlProviders { Vertica = "vertica" } +export enum AwsIamAuthType { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + export enum ElasticSearchAuthTypes { User = "user", ApiKey = "api-key" @@ -168,16 +173,38 @@ export const DynamicSecretSapAseSchema = z.object({ revocationStatement: z.string().trim() }); -export const DynamicSecretAwsIamSchema = z.object({ - 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() -}); +export const DynamicSecretAwsIamSchema = z.preprocess( + (val) => { + if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) { + // eslint-disable-next-line no-param-reassign + (val as { method: string }).method = AwsIamAuthType.AccessKey; + } + return val; + }, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AwsIamAuthType.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() + }), + z.object({ + method: z.literal(AwsIamAuthType.AssumeRole), + roleArn: z.string().trim().min(1, "Role ARN required"), + 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() + }) + ]) +); export const DynamicSecretMongoAtlasSchema = z.object({ adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"), @@ -400,9 +427,15 @@ export type TDynamicProviderFns = { inputs: unknown; expireAt: number; usernameTemplate?: string | null; + metadata: { projectId: string }; }) => Promise<{ entityId: string; data: unknown }>; - validateConnection: (inputs: unknown) => Promise; - validateProviderInputs: (inputs: object) => Promise; - revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; - renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; + validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise; + validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise; + revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>; + renew: ( + inputs: unknown, + entityId: string, + expireAt: number, + metadata: { projectId: string } + ) => Promise<{ entityId: string }>; }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index cb53a71a6..e2fc73d8a 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -213,6 +213,12 @@ const envSchema = z GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), + DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default( + process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID + ), + DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default( + process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY + ), /* ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */ diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 56a10419c..53d61b061 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -50,110 +50,281 @@ Replace **\** with your AWS account id and **\** w ## Set up Dynamic Secrets with AWS IAM - - - Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. - - - ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) - - - ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) - - - - Name by which you want the secret to be referenced - + + + Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. + + To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role. - - Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) - + If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. - - Maximum time-to-live for a generated secret - + The following steps are for instances not deployed on AWS: + + + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. + + + Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAssumeAnyRole", + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/*" + } + ] + } + ``` + + + Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**. - - The managing AWS IAM User Access Key - + ![Access Key Step 1](/images/integrations/aws/integrations-aws-access-key-1.png) + ![Access Key Step 2](/images/integrations/aws/integrations-aws-access-key-2.png) + ![Access Key Step 3](/images/integrations/aws/integrations-aws-access-key-3.png) + + + 1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**. + 2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**. + + + - - The managing AWS IAM User Secret Key - + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) - - [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. - + 2. Select **AWS Account** as the **Trusted Entity Type**. + 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + 4. (Recommended) Enable "Require external ID" and input your **Project ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). + 5. Assign permission as shared in prerequisite. - - The AWS data center region. - + + When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Project ID**. - - 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. - + This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor. - - The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas - + Always enable "Require external ID" and use your Project ID when setting up the IAM Role. + + + + ![Copy IAM Role ARN](/images/integrations/aws/integration-aws-iam-assume-arn.png) + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png) + + Name by which you want the secret to be referenced + - - The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas - + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + - - The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas - + + Maximum time-to-live for a generated secret + - -Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Select *Assume Role* method. + -Allowed template variables are -- `{{randomUsername}}`: Random username string -- `{{unixTimestamp}}`: Current Unix timestamp - + + The ARN of the AWS Role to assume. + - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png) + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + - - - After submitting the form, you will see a dynamic secret created in the dashboard. + + The AWS data center region. + - ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) - - - Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. - To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. - Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + 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. + - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas + - When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas + - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + The AWS IAM inline policy that should be attached to the created users. + Multiple values can be provided by separating them with commas + - - Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4. - + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + Allowed template variables are - Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + + - ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) - - + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + + + + Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance. + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png) + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + + + + Maximum time-to-live for a generated secret + + + + Select *Access Key* method. + + + + The managing AWS IAM User Access Key + + + + 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. + + + + 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 + + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + + + ## Audit or Revoke Leases + Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases + To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) - Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic + secret diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png new file mode 100644 index 000000000..439208d83 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png new file mode 100644 index 000000000..e3be4b5f1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png deleted file mode 100644 index 0ba6aa172..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png and /dev/null differ diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index f9aa6d4d0..9b105cd64 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -44,6 +44,11 @@ export enum SqlProviders { MsSQL = "mssql" } +export enum DynamicSecretAwsIamAuth { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + export type TDynamicSecretProvider = | { type: DynamicSecretProviders.SqlDatabase; @@ -78,15 +83,26 @@ export type TDynamicSecretProvider = } | { type: DynamicSecretProviders.AwsIam; - inputs: { - accessKey: string; - secretAccessKey: string; - region: string; - awsPath?: string; - policyDocument?: string; - userGroups?: string; - policyArns?: string; - }; + inputs: + | { + method: DynamicSecretAwsIamAuth.AccessKey; + accessKey: string; + secretAccessKey: string; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; + } + | { + method: DynamicSecretAwsIamAuth.AssumeRole; + roleArn: string; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; + }; } | { type: DynamicSecretProviders.Redis; 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 cd7b330e8..80c80ee61 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 @@ -5,22 +5,46 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; -import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { + DynamicSecretAwsIamAuth, + DynamicSecretProviders +} from "@app/hooks/api/dynamicSecret/types"; import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ - provider: z.object({ - 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() - }), + provider: 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() + }), + 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() + }) + ]), defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) @@ -67,16 +91,21 @@ export const AwsIamInputForm = ({ const { control, formState: { isSubmitting }, - handleSubmit + handleSubmit, + watch } = useForm({ resolver: zodResolver(formSchema), defaultValues: { environment: isSingleEnvironmentMode ? environments[0] : undefined, - usernameTemplate: "{{randomUsername}}" + usernameTemplate: "{{randomUsername}}", + provider: { + method: DynamicSecretAwsIamAuth.AssumeRole + } } }); const createDynamicSecret = useCreateDynamicSecret(); + const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey; const handleCreateDynamicSecret = async ({ name, @@ -127,7 +156,7 @@ export const AwsIamInputForm = ({ isError={Boolean(error)} errorText={error?.message} > - + )} /> @@ -170,38 +199,82 @@ export const AwsIamInputForm = ({ Configuration
-
- ( - ( + + - - )} - /> - ( - - - - )} - /> -
+ + Assume Role (Recommended) + + Access Key + + + )} + /> + {isAccessKeyMethod ? ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ) : ( +
+ ( + + + + )} + /> +
+ )}
{ const valMs = ms(val); if (valMs < 60 * 1000) @@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({ }: Props) => { const { control, + watch, formState: { isSubmitting }, handleSubmit } = useForm({ @@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({ } } }); + const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey; const updateDynamicSecret = useUpdateDynamicSecret(); @@ -173,38 +186,82 @@ export const EditDynamicSecretAwsIamForm = ({
Configuration
-
- ( - ( + + - - )} - /> - ( - - - - )} - /> -
+ + Assume Role (Recommended) + + Access Key + + + )} + /> + {isAccessKeyMethod ? ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ) : ( +
+ ( + + + + )} + /> +
+ )}