diff --git a/docs/docs.json b/docs/docs.json
index 39ebf31a9..91fedf0bb 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -795,6 +795,12 @@
"documentation/platform/pam/product-reference/session-recording",
"documentation/platform/pam/product-reference/credential-rotation"
]
+ },
+ {
+ "group": "Resources",
+ "pages": [
+ "documentation/platform/pam/resources/aws-iam"
+ ]
}
]
}
diff --git a/docs/documentation/platform/pam/resources/aws-iam.mdx b/docs/documentation/platform/pam/resources/aws-iam.mdx
new file mode 100644
index 000000000..662d76b5e
--- /dev/null
+++ b/docs/documentation/platform/pam/resources/aws-iam.mdx
@@ -0,0 +1,258 @@
+---
+title: "AWS IAM"
+sidebarTitle: "AWS IAM"
+description: "Learn how to configure AWS Management Console access through Infisical PAM for secure, audited, and just-in-time access to AWS."
+---
+
+Infisical PAM supports secure, just-in-time access to the **AWS Management Console** through federated sign-in. This allows your team to access AWS without sharing long-lived credentials, while maintaining a complete audit trail of who accessed what and when.
+
+## How It Works
+
+Unlike database or SSH resources that require a Gateway for network connectivity, AWS Console access works differently. Infisical uses AWS STS (Security Token Service) to assume roles on your behalf and generates temporary federated sign-in URLs.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Infisical
+ participant Resource Role as Resource Role (Your AWS Account)
+ participant Target Role as Target Role (Your AWS Account)
+ participant Console as AWS Console
+
+ User->>Infisical: Request AWS Console access
+ Infisical->>Resource Role: AssumeRole (with ExternalId)
+ Resource Role-->>Infisical: Temporary credentials
+ Infisical->>Target Role: AssumeRole (role chaining)
+ Target Role-->>Infisical: Session credentials
+ Infisical->>Console: Generate federation URL
+ Console-->>Infisical: Signed console URL
+ Infisical-->>User: Return console URL
+ User->>Console: Open AWS Console (federated)
+```
+
+### Key Concepts
+
+1. **Resource Role**: An IAM role in your AWS account that trusts Infisical. This is the "bridge" role that Infisical assumes first.
+
+2. **Target Role**: The IAM role that end users will actually use in the AWS Console. The Resource Role assumes this role on behalf of the user.
+
+3. **Role Chaining**: Infisical uses AWS role chaining - it first assumes the Resource Role, then uses those credentials to assume the Target Role. This provides an additional layer of security and audit capability.
+
+4. **External ID**: A unique identifier (your Infisical Project ID) used in the trust policy to prevent [confused deputy attacks](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
+
+## Session Behavior
+
+### Session Duration
+
+The session duration is set when creating the account and applies to all access requests. You can specify the duration using human-readable formats like `15m`, `30m`, or `1h`. Due to AWS role chaining limitations:
+
+- **Minimum**: 15 minutes (`15m`)
+- **Maximum**: 1 hour (`1h`)
+
+### Session Tracking
+
+Infisical tracks:
+- When the session was created
+- Who accessed which role
+- When the session expires
+
+
+ **Important**: AWS Console sessions cannot be terminated early. Once a federated URL is generated, the session remains valid until the configured duration expires. However, you can [revoke active sessions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html) by modifying the role's trust policy.
+
+
+### CloudTrail Integration
+
+All actions performed in the AWS Console are logged in [AWS CloudTrail](https://console.aws.amazon.com/cloudtrail). The session is identified by the `RoleSessionName`, which includes the user's email address for attribution:
+
+```
+arn:aws:sts::123456789012:assumed-role/pam-readonly/user@example.com
+```
+
+This allows you to correlate Infisical PAM sessions with CloudTrail logs for complete audit visibility.
+
+## Prerequisites
+
+Before configuring AWS Console access in Infisical PAM, you need to set up two IAM roles in your AWS account:
+
+1. **Resource Role** - Trusted by Infisical, can assume target roles
+2. **Target Role(s)** - The actual roles users will use in the console
+
+
+ **No Gateway Required**: Unlike database or SSH resources, AWS Console access does not require an Infisical Gateway. Infisical communicates directly with AWS APIs.
+
+
+## Create the PAM Resource
+
+The PAM Resource represents the connection between Infisical and your AWS account. It contains the Resource Role that Infisical will assume.
+
+
+
+ First, create an IAM policy that allows the Resource Role to assume your target roles. For simplicity, you can use a wildcard to allow assuming any role in your account:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Action": "sts:AssumeRole",
+ "Resource": "arn:aws:iam:::role/*"
+ }]
+ }
+ ```
+
+ 
+
+
+ **For more granular control**: If you want to restrict which roles the Resource Role can assume, replace the wildcard (`/*`) with a more specific pattern. For example:
+ - `arn:aws:iam:::role/pam-*` to only allow roles with the `pam-` prefix
+ - `arn:aws:iam:::role/infisical-*` to only allow roles with the `infisical-` prefix
+
+ This allows you to limit the blast radius of the Resource Role's permissions.
+
+
+
+
+ Create an IAM role (e.g., `InfisicalResourceRole`) with:
+ - The permissions policy from the previous step attached
+ - The following trust policy:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam:::root"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ""
+ }
+ }
+ }]
+ }
+ ```
+
+ 
+
+ 
+
+
+ **Security Best Practice**: Always use the External ID condition. This prevents confused deputy attacks where another Infisical customer could potentially trick Infisical into assuming your role.
+
+
+ **Infisical AWS Account IDs:**
+ | Region | Account ID |
+ |--------|------------|
+ | US | `381492033652` |
+ | EU | `345594589636` |
+
+
+ **For Dedicated Instances**: Your AWS account ID differs from the ones listed above. Please contact Infisical support to obtain your dedicated AWS account ID.
+
+
+
+ **For Self-Hosted Instances**: Use the AWS account ID where your Infisical instance is deployed. This is the account that hosts your Infisical infrastructure and will be assuming the Resource Role.
+
+
+
+
+ 1. Navigate to your PAM project and go to the **Resources** tab
+ 2. Click **Add Resource** and select **AWS IAM**
+ 3. Enter a name for the resource (e.g., `production-aws`)
+ 4. Enter the **Resource Role ARN** - the ARN of the role you created in the previous step
+
+ 
+
+ Clicking **Create Resource** will validate that Infisical can assume the Resource Role. If the connection fails, verify:
+ - The trust policy has the correct Infisical AWS account ID
+ - The External ID matches your project ID
+ - The role ARN is correct
+
+
+
+## Create PAM Accounts
+
+A PAM Account represents a specific Target Role that users can request access to. You can create multiple accounts per resource, each pointing to a different target role with different permission levels.
+
+
+
+ Each target role needs a trust policy that allows your Resource Role to assume it:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam:::role/InfisicalResourceRole"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ""
+ }
+ }
+ }]
+ }
+ ```
+
+ 
+
+
+
+ 1. Navigate to the **Accounts** tab in your PAM project
+ 2. Click **Add Account** and select the AWS IAM resource you created
+ 3. Fill in the account details:
+
+ 
+
+
+ A friendly name for this account (e.g., `readonly`, `admin`, `developer`)
+
+
+
+ Optional description of what this account is used for
+
+
+
+ The ARN of the IAM role users will assume (e.g., `arn:aws:iam::123456789012:role/pam-readonly`)
+
+
+
+ Session duration using human-readable format (e.g., `15m`, `30m`, `1h`). Minimum 15 minutes, maximum 1 hour.
+
+
+ Due to AWS role chaining limitations, the maximum session duration is **1 hour**, regardless of the target role's configured maximum session duration.
+
+
+
+
+
+## Access the AWS Console
+
+Once your resource and accounts are configured, users can request access through Infisical:
+
+
+
+
+
+ Go to the **Accounts** tab in your PAM project.
+
+
+
+ Find the AWS Console account you want to access.
+
+
+
+ Click the **Access** button.
+
+ Infisical will:
+ 1. Assume the Resource Role using your project's External ID
+ 2. Assume the Target Role using role chaining
+ 3. Generate a federated sign-in URL
+ 4. Open the AWS Console in a new browser tab
+
+ The user will be signed into the AWS Console with the permissions of the Target Role.
+
+
\ No newline at end of file
diff --git a/docs/images/pam/resources/aws-iam/access-account.png b/docs/images/pam/resources/aws-iam/access-account.png
new file mode 100644
index 000000000..6e6a57e1e
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/access-account.png differ
diff --git a/docs/images/pam/resources/aws-iam/create-account.png b/docs/images/pam/resources/aws-iam/create-account.png
new file mode 100644
index 000000000..4d2203667
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/create-account.png differ
diff --git a/docs/images/pam/resources/aws-iam/create-resource.png b/docs/images/pam/resources/aws-iam/create-resource.png
new file mode 100644
index 000000000..766b86ffc
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/create-resource.png differ
diff --git a/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png b/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png
new file mode 100644
index 000000000..39201c7cd
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png differ
diff --git a/docs/images/pam/resources/aws-iam/resource-role-policy.png b/docs/images/pam/resources/aws-iam/resource-role-policy.png
new file mode 100644
index 000000000..1c52aaebb
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-policy.png differ
diff --git a/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png b/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png
new file mode 100644
index 000000000..7c018f985
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png differ
diff --git a/docs/images/pam/resources/aws-iam/target-role-trust-policy.png b/docs/images/pam/resources/aws-iam/target-role-trust-policy.png
new file mode 100644
index 000000000..b91dd0030
Binary files /dev/null and b/docs/images/pam/resources/aws-iam/target-role-trust-policy.png differ
diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx
index 677fb9e90..93ca6507f 100644
--- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx
+++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx
@@ -2,8 +2,10 @@ import { Controller, FormProvider, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
+import ms from "ms";
import { z } from "zod";
+import { TtlFormLabel } from "@app/components/features";
import {
Accordion,
AccordionContent,
@@ -25,11 +27,23 @@ import {
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
+const AWS_STS_MIN_SESSION_DURATION = 900; // 15 minutes
+const AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING = 3600; // 1 hour
+
+type SubmitData = {
+ name: string;
+ description?: string | null;
+ credentials: {
+ targetRoleArn: string;
+ defaultSessionDuration: number;
+ };
+};
+
type Props = {
account?: TAwsIamAccount;
resourceId?: string;
resourceType?: PamResourceType;
- onSubmit: (formData: FormData) => Promise;
+ onSubmit: (formData: SubmitData) => Promise;
};
const arnRoleRegex = /^arn:aws:iam::\d{12}:role\/[\w+=,.@/-]+$/;
@@ -42,12 +56,29 @@ const AwsIamCredentialsSchema = z.object({
.refine((val) => arnRoleRegex.test(val), {
message: "ARN must be in the format 'arn:aws:iam::123456789012:role/RoleName'"
}),
- // Max 1 hour (3600s) due to AWS role chaining limitation, min 15 min (900s)
- defaultSessionDuration: z.coerce
- .number()
- .min(900, "Minimum session duration is 900 seconds (15 minutes)")
- .max(3600, "Maximum session duration is 3600 seconds (1 hour)")
- .default(3600)
+ defaultSessionDuration: z.string().superRefine((val, ctx) => {
+ const valMs = ms(val);
+ if (typeof valMs !== "number" || valMs <= 0) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Invalid duration format. Use formats like 15m, 30m, 1h"
+ });
+ return;
+ }
+ const valSeconds = valMs / 1000;
+ if (valSeconds < AWS_STS_MIN_SESSION_DURATION) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Minimum session duration is 15 minutes (15m)"
+ });
+ }
+ if (valSeconds > AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Maximum session duration is 1 hour (1h) due to AWS role chaining"
+ });
+ }
+ })
});
const formSchema = genericAccountFieldsSchema.extend({
@@ -66,17 +97,17 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
enabled: !!resourceIdToFetch && !!resourceTypeToFetch
});
- const pamRoleArn =
+ const resourceRoleArn =
(resource?.resourceType === PamResourceType.AwsIam &&
(resource as TAwsIamResource).connectionDetails?.roleArn) ||
- "arn:aws:iam:::role/";
+ "arn:aws:iam:::role/";
const targetRoleTrustPolicy = `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
- "AWS": "${pamRoleArn}"
+ "AWS": "${resourceRoleArn}"
},
"Action": "sts:AssumeRole",
"Condition": {
@@ -87,16 +118,36 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
}]
}`;
+ // Convert seconds to human-readable format for existing accounts
+ const getDefaultSessionDuration = () => {
+ if (account?.credentials?.defaultSessionDuration) {
+ const seconds = account.credentials.defaultSessionDuration;
+ if (seconds >= 3600 && seconds % 3600 === 0) {
+ return `${seconds / 3600}h`;
+ }
+ return `${seconds / 60}m`;
+ }
+ return "1h";
+ };
+
const form = useForm({
resolver: zodResolver(formSchema),
- defaultValues: account ?? {
- name: "",
- description: "",
- credentials: {
- targetRoleArn: "",
- defaultSessionDuration: 3600
- }
- }
+ defaultValues: account
+ ? {
+ ...account,
+ credentials: {
+ ...account.credentials,
+ defaultSessionDuration: getDefaultSessionDuration()
+ }
+ }
+ : {
+ name: "",
+ description: "",
+ credentials: {
+ targetRoleArn: "",
+ defaultSessionDuration: "1h"
+ }
+ }
});
const {
@@ -105,9 +156,23 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
formState: { isSubmitting, isDirty }
} = form;
+ const handleFormSubmit = async (formData: FormData) => {
+ const durationMs = ms(formData.credentials.defaultSessionDuration);
+ const durationSeconds = Math.floor(durationMs / 1000);
+
+ await onSubmit({
+ name: formData.name,
+ description: formData.description,
+ credentials: {
+ targetRoleArn: formData.credentials.targetRoleArn,
+ defaultSessionDuration: durationSeconds
+ }
+ });
+ };
+
return (
-