Merge pull request #4471 from Infisical/ENG-3630

Dynamic Secrets: add Temporary Credentials for AWS IAM Roles
This commit is contained in:
carlosmonastyrski
2025-09-16 12:55:37 -03:00
committed by GitHub
7 changed files with 936 additions and 405 deletions
@@ -16,7 +16,7 @@ import {
PutUserPolicyCommand, PutUserPolicyCommand,
RemoveUserFromGroupCommand RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam"; } 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 { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { CustomAWSHasher } from "@app/lib/aws/hashing";
@@ -26,9 +26,12 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn"; import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils"; import { compileUsernameTemplate } from "./templateUtils";
// AWS STS duration constants (in seconds)
const AWS_STS_MIN_DURATION = 900;
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32); const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
@@ -120,6 +123,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
try { 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 client = await $getClient(providerInputs, projectId);
const isConnected = await client const isConnected = await client
.send(new GetUserCommand({})) .send(new GetUserCommand({}))
@@ -137,7 +192,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
}); });
return isConnected; return isConnected;
} catch (err) { } catch (err) {
const sensitiveTokens = []; const sensitiveTokens: string[] = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) { if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
} }
@@ -163,9 +218,168 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
}; };
metadata: { projectId: string }; metadata: { projectId: string };
}) => { }) => {
const { inputs, usernameTemplate, metadata, identity } = data; const { inputs, usernameTemplate, metadata, identity, expireAt } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
try {
let stsClient: STSClient;
let entityId: string;
const currentTime = Date.now();
const requestedDuration = Math.floor((expireAt - currentTime) / 1000);
if (requestedDuration <= 0) {
throw new BadRequestError({ message: "Expiration time must be in the future" });
}
let durationSeconds: number;
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
durationSeconds = requestedDuration;
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
});
const assumeRoleRes = await stsClient.send(
new AssumeRoleCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`,
DurationSeconds: durationSeconds,
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) {
durationSeconds = requestedDuration;
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: durationSeconds
})
);
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) {
durationSeconds = requestedDuration;
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
const sessionTokenRes = await stsClient.send(
new GetSessionTokenCommand({
DurationSeconds: durationSeconds
})
);
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);
}
let errorMessage = (err as Error)?.message || "Unknown error";
if (err && typeof err === "object" && "name" in err && "$metadata" in err) {
const awsError = err as { name?: string; message?: string; $metadata?: object };
if (awsError.name) {
errorMessage = `${awsError.name}: ${errorMessage}`;
}
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create temporary credentials: ${sanitizedErrorMessage}`
});
}
}
if (providerInputs.credentialType === AwsIamCredentialType.IamUser) {
const client = await $getClient(providerInputs, metadata.projectId); const client = await $getClient(providerInputs, metadata.projectId);
const username = generateUsername(usernameTemplate, identity); const username = generateUsername(usernameTemplate, identity);
@@ -255,10 +469,18 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
message: `Failed to create lease from provider: ${sanitizedErrorMessage}` 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 revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
return { entityId };
}
const client = await $getClient(providerInputs, metadata.projectId); const client = await $getClient(providerInputs, metadata.projectId);
const username = entityId; const username = entityId;
@@ -32,6 +32,11 @@ export enum AwsIamAuthType {
IRSA = "irsa" IRSA = "irsa"
} }
export enum AwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export enum ElasticSearchAuthTypes { export enum ElasticSearchAuthTypes {
User = "user", User = "user",
ApiKey = "api-key" ApiKey = "api-key"
@@ -203,6 +208,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
z.discriminatedUnion("method", [ z.discriminatedUnion("method", [
z.object({ z.object({
method: z.literal(AwsIamAuthType.AccessKey), method: z.literal(AwsIamAuthType.AccessKey),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
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),
@@ -215,6 +221,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.AssumeRole), method: z.literal(AwsIamAuthType.AssumeRole),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
roleArn: z.string().trim().min(1, "Role ARN required"), roleArn: z.string().trim().min(1, "Role ARN required"),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
@@ -226,6 +233,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.IRSA), method: z.literal(AwsIamAuthType.IRSA),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
@@ -3,14 +3,29 @@ title: "AWS IAM"
description: "Learn how to dynamically generate AWS IAM Users." 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.
## AWS STS Duration Limits
When using **Temporary Credentials**, AWS STS has specific maximum duration limits:
- **AssumeRole operations**: Maximum 1 hour (3600 seconds) when using temporary credentials
- **GetSessionToken operations** (Access Key & IRSA): Maximum 12 hours (43200 seconds)
<Info>
**Automatic Duration Adjustment**: If you specify a TTL that exceeds these AWS limits, Infisical will automatically use the maximum allowed duration instead of failing the operation. This ensures your dynamic secrets work reliably within AWS constraints.
</Info>
## Prerequisite ## 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.
<Accordion title="Required IAM Permissions"> <Accordion title="Required IAM Permissions">
<Tabs>
<Tab title="IAM User">
Required permissions for creating temporary IAM users:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
@@ -46,6 +61,35 @@ To minimize managing user access you can attach a resource in format
> arn:aws:iam::\<account-id\>:user/\<aws-scope-path\> > arn:aws:iam::\<account-id\>:user/\<aws-scope-path\>
Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access. Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access.
</Tab>
<Tab title="Temporary Credentials">
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::\<account-id\>:user/\<aws-scope-path\>
Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access.
</Tab>
</Tabs>
</Accordion> </Accordion>
@@ -170,43 +214,76 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Select *Assume Role* method. Select *Assume Role* method.
</ParamField> </ParamField>
<ParamField path="Aws Role ARN" type="string" required> <ParamField path="Credential Type" type="string" required>
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
</ParamField> </ParamField>
<ParamField path="AWS IAM Path" type="string"> <ParamField path="Aws Role ARN" type="string" required>
[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.
</ParamField> </ParamField>
<ParamField path="AWS Region" type="string" required> <ParamField path="AWS Region" type="string" required>
The AWS data center region. The AWS data center region.
</ParamField> </ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required> <Tabs>
<Tab title="IAM User">
<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="IAM User Permission Boundary" type="string">
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 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>
<ParamField path="AWS IAM Groups" type="string"> <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 The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField> </ParamField>
<ParamField path="AWS Policy ARNs" type="string"> <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 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>
<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. The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
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
- `{{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
</ParamField> </ParamField>
<ParamField path="Tags" type="map<string, string>[]">
Tags to be added to the created IAM User resource.
</ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
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
</Info>
<Warning>
**Duration Limit**: AssumeRole temporary credentials are limited to 1 hour maximum by AWS. TTL values exceeding this limit will be automatically adjusted to 1 hour.
</Warning>
</Tab>
</Tabs>
</Step> </Step>
<Step title="Click 'Submit'"> <Step title="Click 'Submit'">
@@ -232,6 +309,18 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
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.
**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) ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png)
</Step> </Step>
</Steps> </Steps>
@@ -342,36 +431,75 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
<ParamField path="Method" type="string" required> <ParamField path="Method" type="string" required>
Select *IRSA* method. Select *IRSA* method.
</ParamField> </ParamField>
<ParamField path="Credential Type" type="string" required>
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
</ParamField>
<ParamField path="Aws Role ARN" type="string" required> <ParamField path="Aws Role ARN" type="string" required>
The ARN of the AWS IAM Role for the service account to assume. The ARN of the AWS IAM Role for the service account to assume.
</ParamField> </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> <ParamField path="AWS Region" type="string" required>
The AWS data center region. The AWS data center region.
</ParamField> </ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
<Tabs>
<Tab title="IAM User">
<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="IAM User Permission Boundary" type="string">
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 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>
<ParamField path="AWS IAM Groups" type="string"> <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 The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField> </ParamField>
<ParamField path="AWS Policy ARNs" type="string"> <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 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>
<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. The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
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
- `{{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
</ParamField> </ParamField>
<ParamField path="Tags" type="map<string, string>[]">
Tags to be added to the created IAM User resource.
</ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
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
</Info>
<Note>
**Duration Limit**: IRSA temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted.
</Note>
</Tab>
</Tabs>
</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.
@@ -429,6 +557,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Select *Access Key* method. Select *Access Key* method.
</ParamField> </ParamField>
<ParamField path="Credential Type" type="string" required>
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
</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>
@@ -437,43 +571,66 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
The managing AWS IAM User Secret Key The managing AWS IAM User Secret Key
</ParamField> </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> <ParamField path="AWS Region" type="string" required>
The AWS data center region. The AWS data center region.
</ParamField> </ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required> <Tabs>
<Tab title="IAM User">
<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="IAM User Permission Boundary" type="string">
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 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>
<ParamField path="AWS IAM Groups" type="string"> <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 The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField> </ParamField>
<ParamField path="AWS Policy ARNs" type="string"> <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 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>
<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. The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
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
- `{{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
</ParamField> </ParamField>
<ParamField path="Tags" type="map[string]string"> <ParamField path="Tags" type="map[string]string">
Tags to be added to the created IAM User resource. Tags to be added to the created IAM User resource.
</ParamField> </ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
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
</Info>
<Note>
**Duration Limit**: Access Key temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted.
</Note>
</Tab>
</Tabs>
</Step> </Step>
@@ -500,6 +657,18 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
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.
**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) ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png)
</Step> </Step>
</Steps> </Steps>
@@ -59,6 +59,11 @@ export enum DynamicSecretAwsIamAuth {
IRSA = "irsa" IRSA = "irsa"
} }
export enum DynamicSecretAwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export type TDynamicSecretProvider = export type TDynamicSecretProvider =
| { | {
type: DynamicSecretProviders.SqlDatabase; type: DynamicSecretProviders.SqlDatabase;
@@ -97,6 +102,7 @@ export type TDynamicSecretProvider =
inputs: inputs:
| { | {
method: DynamicSecretAwsIamAuth.AccessKey; method: DynamicSecretAwsIamAuth.AccessKey;
credentialType: DynamicSecretAwsIamCredentialType;
accessKey: string; accessKey: string;
secretAccessKey: string; secretAccessKey: string;
region: string; region: string;
@@ -107,6 +113,7 @@ export type TDynamicSecretProvider =
} }
| { | {
method: DynamicSecretAwsIamAuth.AssumeRole; method: DynamicSecretAwsIamAuth.AssumeRole;
credentialType: DynamicSecretAwsIamCredentialType;
roleArn: string; roleArn: string;
region: string; region: string;
awsPath?: string; awsPath?: string;
@@ -116,6 +123,7 @@ export type TDynamicSecretProvider =
} }
| { | {
method: DynamicSecretAwsIamAuth.IRSA; method: DynamicSecretAwsIamAuth.IRSA;
credentialType: DynamicSecretAwsIamCredentialType;
region: string; region: string;
awsPath?: string; awsPath?: string;
policyDocument?: string; policyDocument?: string;
@@ -18,6 +18,7 @@ import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetServerConfig } from "@app/hooks/api/admin"; import { useGetServerConfig } from "@app/hooks/api/admin";
import { import {
DynamicSecretAwsIamAuth, DynamicSecretAwsIamAuth,
DynamicSecretAwsIamCredentialType,
DynamicSecretProviders DynamicSecretProviders
} from "@app/hooks/api/dynamicSecret/types"; } from "@app/hooks/api/dynamicSecret/types";
import { ProjectEnv } from "@app/hooks/api/types"; import { ProjectEnv } from "@app/hooks/api/types";
@@ -28,6 +29,9 @@ const formSchema = z.object({
provider: z.discriminatedUnion("method", [ provider: z.discriminatedUnion("method", [
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey), method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
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),
@@ -47,6 +51,9 @@ const formSchema = z.object({
}), }),
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
roleArn: z.string().trim().min(1), roleArn: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
@@ -65,6 +72,9 @@ const formSchema = z.object({
}), }),
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.IRSA), method: z.literal(DynamicSecretAwsIamAuth.IRSA),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
@@ -137,13 +147,15 @@ export const AwsIamInputForm = ({
environment: isSingleEnvironmentMode ? environments[0] : undefined, environment: isSingleEnvironmentMode ? environments[0] : undefined,
usernameTemplate: "{{randomUsername}}", usernameTemplate: "{{randomUsername}}",
provider: { provider: {
method: DynamicSecretAwsIamAuth.AssumeRole method: DynamicSecretAwsIamAuth.AssumeRole,
credentialType: DynamicSecretAwsIamCredentialType.IamUser
} }
} }
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const method = watch("provider.method"); const method = watch("provider.method");
const credentialType = watch("provider.credentialType");
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@@ -264,6 +276,39 @@ export const AwsIamInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
name="provider.credentialType"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Credential Type"
>
<>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={DynamicSecretAwsIamCredentialType.IamUser}>
IAM User
</SelectItem>
<SelectItem value={DynamicSecretAwsIamCredentialType.TemporaryCredentials}>
Temporary Credentials
</SelectItem>
</Select>
<div className="mt-1 text-xs text-mineshaft-300">
{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."}
</div>
</>
</FormControl>
)}
/>
{method === DynamicSecretAwsIamAuth.AccessKey && ( {method === DynamicSecretAwsIamAuth.AccessKey && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
@@ -318,6 +363,7 @@ export const AwsIamInputForm = ({
</div> </div>
)} )}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<Controller <Controller
control={control} control={control}
name="provider.awsPath" name="provider.awsPath"
@@ -334,6 +380,7 @@ export const AwsIamInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
)}
<Controller <Controller
control={control} control={control}
name="provider.region" name="provider.region"
@@ -341,7 +388,11 @@ export const AwsIamInputForm = ({
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="AWS Region" label="AWS Region"
className="flex-grow" className={
credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials
? "w-full"
: "flex-grow"
}
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
@@ -350,6 +401,8 @@ export const AwsIamInputForm = ({
)} )}
/> />
</div> </div>
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<>
<Controller <Controller
control={control} control={control}
name="provider.permissionBoundaryPolicyArn" name="provider.permissionBoundaryPolicyArn"
@@ -421,6 +474,9 @@ export const AwsIamInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
</>
)}
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<Controller <Controller
control={control} control={control}
name="usernameTemplate" name="usernameTemplate"
@@ -440,7 +496,10 @@ export const AwsIamInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
)}
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<MetadataForm control={control} name="provider.tags" title="Tags" isValueRequired /> <MetadataForm control={control} name="provider.tags" title="Tags" isValueRequired />
)}
{!isSingleEnvironmentMode && ( {!isSingleEnvironmentMode && (
<Controller <Controller
control={control} control={control}
@@ -154,20 +154,22 @@ const renderOutputForm = (
} }
if (provider === DynamicSecretProviders.AwsIam) { if (provider === DynamicSecretProviders.AwsIam) {
const { USERNAME, ACCESS_KEY, SECRET_ACCESS_KEY } = data as { const { USERNAME, ACCESS_KEY, SECRET_ACCESS_KEY, SESSION_TOKEN } = data as {
ACCESS_KEY: string; ACCESS_KEY: string;
SECRET_ACCESS_KEY: string; SECRET_ACCESS_KEY: string;
USERNAME: string; USERNAME?: string;
SESSION_TOKEN?: string;
}; };
return ( return (
<div> <div>
<OutputDisplay label="AWS Username" value={USERNAME} /> {USERNAME && <OutputDisplay label="AWS IAM Username" value={USERNAME} />}
<OutputDisplay label="AWS IAM Access Key" value={ACCESS_KEY} /> <OutputDisplay label="AWS IAM Access Key" value={ACCESS_KEY} />
<OutputDisplay <OutputDisplay label="AWS IAM Secret Key" value={SECRET_ACCESS_KEY} />
label="AWS IAM Secret Key" {SESSION_TOKEN && <OutputDisplay label="AWS IAM Session Token" value={SESSION_TOKEN} />}
value={SECRET_ACCESS_KEY} <div className="mt-2 text-xs text-mineshaft-300">
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal." Important: Copy these credentials now. You will not be able to see them again after you
/> close the modal.
</div>
</div> </div>
); );
} }
@@ -7,7 +7,11 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useGetServerConfig, useUpdateDynamicSecret } from "@app/hooks/api"; import { useGetServerConfig, useUpdateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import {
DynamicSecretAwsIamAuth,
DynamicSecretAwsIamCredentialType,
TDynamicSecret
} from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { MetadataForm } from "../MetadataForm"; import { MetadataForm } from "../MetadataForm";
@@ -16,6 +20,9 @@ const formSchema = z.object({
inputs: z.discriminatedUnion("method", [ inputs: z.discriminatedUnion("method", [
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey), method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
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),
@@ -30,6 +37,9 @@ const formSchema = z.object({
}), }),
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
roleArn: z.string().trim().min(1), roleArn: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
@@ -43,6 +53,9 @@ const formSchema = z.object({
}), }),
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.IRSA), method: z.literal(DynamicSecretAwsIamAuth.IRSA),
credentialType: z
.nativeEnum(DynamicSecretAwsIamCredentialType)
.default(DynamicSecretAwsIamCredentialType.IamUser),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
@@ -78,6 +91,7 @@ const formSchema = z.object({
newName: slugSchema().optional(), newName: slugSchema().optional(),
usernameTemplate: z.string().trim().nullable().optional() usernameTemplate: z.string().trim().nullable().optional()
}); });
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
type Props = { type Props = {
@@ -115,6 +129,7 @@ export const EditDynamicSecretAwsIamForm = ({
} }
}); });
const method = watch("inputs.method"); const method = watch("inputs.method");
const credentialType = watch("inputs.credentialType");
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
@@ -235,6 +250,39 @@ export const EditDynamicSecretAwsIamForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
name="inputs.credentialType"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Credential Type"
>
<>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={DynamicSecretAwsIamCredentialType.IamUser}>
IAM User
</SelectItem>
<SelectItem value={DynamicSecretAwsIamCredentialType.TemporaryCredentials}>
Temporary Credentials
</SelectItem>
</Select>
<div className="mt-1 text-xs text-mineshaft-300">
{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."}
</div>
</>
</FormControl>
)}
/>
{method === DynamicSecretAwsIamAuth.AccessKey && ( {method === DynamicSecretAwsIamAuth.AccessKey && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
@@ -289,6 +337,7 @@ export const EditDynamicSecretAwsIamForm = ({
</div> </div>
)} )}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<Controller <Controller
control={control} control={control}
name="inputs.awsPath" name="inputs.awsPath"
@@ -297,6 +346,7 @@ export const EditDynamicSecretAwsIamForm = ({
<FormControl <FormControl
label="AWS IAM Path" label="AWS IAM Path"
className="flex-grow" className="flex-grow"
isOptional
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
@@ -304,6 +354,7 @@ export const EditDynamicSecretAwsIamForm = ({
</FormControl> </FormControl>
)} )}
/> />
)}
<Controller <Controller
control={control} control={control}
name="inputs.region" name="inputs.region"
@@ -311,7 +362,11 @@ export const EditDynamicSecretAwsIamForm = ({
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="AWS Region" label="AWS Region"
className="flex-grow" className={
credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials
? "w-full"
: "flex-grow"
}
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
@@ -320,6 +375,8 @@ export const EditDynamicSecretAwsIamForm = ({
)} )}
/> />
</div> </div>
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<>
<Controller <Controller
control={control} control={control}
name="inputs.userGroups" name="inputs.userGroups"
@@ -388,6 +445,9 @@ export const EditDynamicSecretAwsIamForm = ({
</FormControl> </FormControl>
)} )}
/> />
</>
)}
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<Controller <Controller
control={control} control={control}
name="usernameTemplate" name="usernameTemplate"
@@ -406,7 +466,10 @@ export const EditDynamicSecretAwsIamForm = ({
</FormControl> </FormControl>
)} )}
/> />
)}
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
<MetadataForm control={control} name="inputs.tags" title="Tags" isValueRequired /> <MetadataForm control={control} name="inputs.tags" title="Tags" isValueRequired />
)}
</div> </div>
</div> </div>
<div className="mt-4 flex items-center space-x-4"> <div className="mt-4 flex items-center space-x-4">