Merge pull request #3750 from akhilmhdh/feat/dynamic-secret-aws

feat: assume role mode for aws dynamic secret iam
This commit is contained in:
Akhil Mohan
2025-06-07 00:59:22 +05:30
committed by GitHub
13 changed files with 619 additions and 198 deletions

View File

@@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, {
projectId: folder.projectId
});
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
return; return;
} }
@@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all( await Promise.all(
dynamicSecretLeases.map(({ externalEntityId }) => dynamicSecretLeases.map(({ externalEntityId }) =>
selectedProvider.revoke(decryptedStoredInput, externalEntityId) selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
projectId: folder.projectId
})
) )
); );
} }

View File

@@ -135,7 +135,8 @@ export const dynamicSecretLeaseServiceFactory = ({
result = await selectedProvider.create({ result = await selectedProvider.create({
inputs: decryptedStoredInput, inputs: decryptedStoredInput,
expireAt: expireAt.getTime(), expireAt: expireAt.getTime(),
usernameTemplate: dynamicSecretCfg.usernameTemplate usernameTemplate: dynamicSecretCfg.usernameTemplate,
metadata: { projectId }
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
@@ -237,7 +238,8 @@ export const dynamicSecretLeaseServiceFactory = ({
const { entityId } = await selectedProvider.renew( const { entityId } = await selectedProvider.renew(
decryptedStoredInput, decryptedStoredInput,
dynamicSecretLease.externalEntityId, dynamicSecretLease.externalEntityId,
expireAt.getTime() expireAt.getTime(),
{ projectId }
); );
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
@@ -313,7 +315,7 @@ export const dynamicSecretLeaseServiceFactory = ({
) as object; ) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
.catch(async (err) => { .catch(async (err) => {
// only propogate this error if forced is false // only propogate this error if forced is false
if (!isForced) return { error: err as Error }; if (!isForced) return { error: err as Error };

View File

@@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type]; 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; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
@@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({
selectedGatewayId = gateway.id; 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" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
@@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
@@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({
selectedGatewayId = gateway.id; 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" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => { const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
@@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; 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 }; return { ...dynamicSecretCfg, inputs: providerInputs };
}; };

View File

@@ -16,13 +16,16 @@ import {
PutUserPolicyCommand, PutUserPolicyCommand,
RemoveUserFromGroupCommand RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam"; } from "@aws-sdk/client-iam";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { randomUUID } from "crypto";
import handlebars from "handlebars"; import handlebars from "handlebars";
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
const generateUsername = (usernameTemplate?: string | null) => { const generateUsername = (usernameTemplate?: string | null) => {
const randomUsername = alphaNumericNanoId(32); const randomUsername = alphaNumericNanoId(32);
@@ -40,7 +43,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return providerInputs; return providerInputs;
}; };
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => { const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>, 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({ const client = new IAMClient({
region: providerInputs.region, region: providerInputs.region,
credentials: { credentials: {
@@ -52,19 +91,36 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return client; return client;
}; };
const validateConnection = async (inputs: unknown) => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs, projectId);
const isConnected = await client
const isConnected = await client.send(new GetUserCommand({})).then(() => true); .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; return isConnected;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
metadata: { projectId: string };
}) => {
const { inputs, usernameTemplate, metadata } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs, metadata.projectId);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
@@ -76,6 +132,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
UserName: username UserName: username
}) })
); );
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) { if (userGroups) {
await Promise.all( 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 providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs, metadata.projectId);
const username = entityId; const username = entityId;

View File

@@ -20,6 +20,11 @@ export enum SqlProviders {
Vertica = "vertica" Vertica = "vertica"
} }
export enum AwsIamAuthType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export enum ElasticSearchAuthTypes { export enum ElasticSearchAuthTypes {
User = "user", User = "user",
ApiKey = "api-key" ApiKey = "api-key"
@@ -168,7 +173,17 @@ export const DynamicSecretSapAseSchema = z.object({
revocationStatement: z.string().trim() revocationStatement: z.string().trim()
}); });
export const DynamicSecretAwsIamSchema = z.object({ 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), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -177,7 +192,19 @@ export const DynamicSecretAwsIamSchema = z.object({
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: 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({ export const DynamicSecretMongoAtlasSchema = z.object({
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"), adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
@@ -400,9 +427,15 @@ export type TDynamicProviderFns = {
inputs: unknown; inputs: unknown;
expireAt: number; expireAt: number;
usernameTemplate?: string | null; usernameTemplate?: string | null;
metadata: { projectId: string };
}) => Promise<{ entityId: string; data: unknown }>; }) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown) => Promise<boolean>; validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
validateProviderInputs: (inputs: object) => Promise<unknown>; validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; renew: (
inputs: unknown,
entityId: string,
expireAt: number,
metadata: { projectId: string }
) => Promise<{ entityId: string }>;
}; };

View File

@@ -213,6 +213,12 @@ const envSchema = z
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), 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 ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */

View File

@@ -50,6 +50,166 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
## Set up Dynamic Secrets with AWS IAM ## Set up Dynamic Secrets with AWS IAM
<Tabs>
<Tab title="Assume Role (Recommended)">
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
<Accordion title="Self-Hosted Instance">
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.
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.
The following steps are for instances not deployed on AWS:
<Steps>
<Step title="Create an IAM User">
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
</Step>
<Step title="Create an Inline Policy">
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/*"
}
]
}
```
</Step>
<Step title="Obtain the IAM User Credentials">
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**.
![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)
</Step>
<Step title="Set Up Connection Keys">
1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**.
2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**.
</Step>
</Steps>
</Accordion>
<Steps>
<Step title="Create the Managing User IAM Role for Infisical">
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)
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) <strong>Enable "Require external ID"</strong> 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.
<Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
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**.
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.
<strong>Always enable "Require external ID" and use your Project ID when setting up the IAM Role.</strong>
</Warning>
</Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role ARN](/images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Secret Overview Dashboard">
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select AWS IAM">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png)
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret
</ParamField>
<ParamField path="Method" type="string" required>
Select *Assume Role* method.
</ParamField>
<ParamField path="Aws Role ARN" type="string" required>
The ARN of the AWS Role to assume.
</ParamField>
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
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.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
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
</ParamField>
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step>
<Step title="Generate dynamic secrets">
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)
<Tip>
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
</Tip>
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)
</Step>
</Steps>
</Tab>
<Tab title="Access Key">
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
<Steps> <Steps>
<Step title="Secret Overview Dashboard"> <Step title="Secret Overview Dashboard">
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
@@ -61,6 +221,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png)
</Step> </Step>
<Step title="Provide the inputs for dynamic secret parameters"> <Step title="Provide the inputs for dynamic secret parameters">
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png)
<ParamField path="Secret Name" type="string" required> <ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced Name by which you want the secret to be referenced
</ParamField> </ParamField>
@@ -73,6 +234,10 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Maximum time-to-live for a generated secret Maximum time-to-live for a generated secret
</ParamField> </ParamField>
<ParamField path="Method" type="string" required>
Select *Access Key* method.
</ParamField>
<ParamField path="AWS Access Key" type="string" required> <ParamField path="AWS Access Key" type="string" required>
The managing AWS IAM User Access Key The managing AWS IAM User Access Key
</ParamField> </ParamField>
@@ -102,25 +267,26 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
</ParamField> </ParamField>
<ParamField path="AWS IAM Policy Document" type="string"> <ParamField path="AWS IAM Policy Document" type="string">
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 inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
Allowed template variables are Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> </ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png)
</Step> </Step>
<Step title="Click 'Submit'"> <Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard. After submitting the form, you will see a dynamic secret created in the dashboard.
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step> </Step>
<Step title="Generate dynamic secrets"> <Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. 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. To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
@@ -134,26 +300,31 @@ Allowed template variables are
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip> <Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4. Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
</Tip> </Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. 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) ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png)
</Step> </Step>
</Steps> </Steps>
</Tab>
</Tabs>
## Audit or Revoke Leases ## 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. 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases ## 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning> <Warning>
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
</Warning> </Warning>

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

View File

@@ -44,6 +44,11 @@ export enum SqlProviders {
MsSQL = "mssql" MsSQL = "mssql"
} }
export enum DynamicSecretAwsIamAuth {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export type TDynamicSecretProvider = export type TDynamicSecretProvider =
| { | {
type: DynamicSecretProviders.SqlDatabase; type: DynamicSecretProviders.SqlDatabase;
@@ -78,7 +83,9 @@ export type TDynamicSecretProvider =
} }
| { | {
type: DynamicSecretProviders.AwsIam; type: DynamicSecretProviders.AwsIam;
inputs: { inputs:
| {
method: DynamicSecretAwsIamAuth.AccessKey;
accessKey: string; accessKey: string;
secretAccessKey: string; secretAccessKey: string;
region: string; region: string;
@@ -86,6 +93,15 @@ export type TDynamicSecretProvider =
policyDocument?: string; policyDocument?: string;
userGroups?: string; userGroups?: string;
policyArns?: string; policyArns?: string;
}
| {
method: DynamicSecretAwsIamAuth.AssumeRole;
roleArn: string;
region: string;
awsPath?: string;
policyDocument?: string;
userGroups?: string;
policyArns?: string;
}; };
} }
| { | {

View File

@@ -5,13 +5,26 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; 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 { 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"; import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({ const formSchema = z.object({
provider: z.object({ provider: z.discriminatedUnion("method", [
z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
accessKey: z.string().trim().min(1), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -21,6 +34,17 @@ const formSchema = z.object({
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: 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) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -67,16 +91,21 @@ export const AwsIamInputForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit,
watch
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
environment: isSingleEnvironmentMode ? environments[0] : undefined, environment: isSingleEnvironmentMode ? environments[0] : undefined,
usernameTemplate: "{{randomUsername}}" usernameTemplate: "{{randomUsername}}",
provider: {
method: DynamicSecretAwsIamAuth.AssumeRole
}
} }
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey;
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@@ -127,7 +156,7 @@ export const AwsIamInputForm = ({
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="dynamic-postgres" /> <Input {...field} placeholder="dynamic-aws-iam" />
</FormControl> </FormControl>
)} )}
/> />
@@ -170,6 +199,31 @@ export const AwsIamInputForm = ({
Configuration Configuration
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<Controller
name="provider.method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
Assume Role (Recommended)
</SelectItem>
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
</Select>
</FormControl>
)}
/>
{isAccessKeyMethod ? (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
@@ -202,6 +256,25 @@ export const AwsIamInputForm = ({
)} )}
/> />
</div> </div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}

View File

@@ -5,14 +5,15 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api"; import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({ const formSchema = z.object({
inputs: z inputs: z.discriminatedUnion("method", [
.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
accessKey: z.string().trim().min(1), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -21,8 +22,18 @@ const formSchema = z.object({
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: 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()
}) })
.partial(), ]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({
}: Props) => { }: Props) => {
const { const {
control, control,
watch,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
@@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({
} }
} }
}); });
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
@@ -173,6 +186,31 @@ export const EditDynamicSecretAwsIamForm = ({
<div> <div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div> <div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div className="flex flex-col"> <div className="flex flex-col">
<Controller
name="inputs.method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
Assume Role (Recommended)
</SelectItem>
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
</Select>
</FormControl>
)}
/>
{isAccessKeyMethod ? (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
@@ -205,6 +243,25 @@ export const EditDynamicSecretAwsIamForm = ({
)} )}
/> />
</div> </div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="inputs.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}