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..2c1352c49
--- /dev/null
+++ b/docs/documentation/platform/pam/resources/aws-iam.mdx
@@ -0,0 +1,244 @@
+---
+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 PAM Role as PAM 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->>PAM Role: AssumeRole (with ExternalId)
+ PAM 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. **PAM 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 PAM Role assumes this role on behalf of the user.
+
+3. **Role Chaining**: Infisical uses AWS role chaining - it first assumes the PAM 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).
+
+## Prerequisites
+
+Before configuring AWS Console access in Infisical PAM, you need to set up two IAM roles in your AWS account:
+
+1. **PAM 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.
+
+
+## Step 1: Create the PAM Resource
+
+The PAM Resource represents the connection between Infisical and your AWS account. It contains the PAM Role that Infisical will assume.
+
+### 1.1 Create the PAM Role Permissions Policy
+
+First, create an IAM policy that allows the PAM Role to assume your target roles. We recommend using a wildcard pattern so you can add new target roles without updating this policy.
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Action": "sts:AssumeRole",
+ "Resource": "arn:aws:iam:::role/-*"
+ }]
+}
+```
+
+
+ Choose a naming convention for your target roles (e.g., `pam-*`, `infisical-*`, or `privileged-*`). This allows you to add new accounts without modifying the PAM Role's permissions.
+
+
+### 1.2 Create the PAM Role with Trust Policy
+
+Create an IAM role (e.g., `InfisicalPAMRole`) with:
+- The permissions policy from step 1.1 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.
+
+
+### 1.3 Create the Resource in Infisical
+
+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 **PAM Role ARN** - the ARN of the role you created in step 1.2
+
+
+
+Clicking **Create Resource** will validate that Infisical can assume the PAM 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
+
+## Step 2: 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.
+
+### 2.1 Create the Target Role Trust Policy
+
+Each target role needs a trust policy that allows your PAM Role to assume it:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam:::role/InfisicalPAMRole"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ""
+ }
+ }
+ }]
+}
+```
+
+
+ **Tip**: If your target role name matches the wildcard pattern in your PAM Role's permissions policy (e.g., `pam-readonly`), you don't need to update the PAM Role's permissions. Just ensure the target role's trust policy is correct.
+
+
+### 2.2 Create the Account in Infisical
+
+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.
+
+
+
+
+
+## Step 3: Access the AWS Console
+
+Once your resource and accounts are configured, users can request access through Infisical:
+
+1. Navigate to the **Accounts** tab
+2. Find the AWS Console account you want to access
+3. Click the **Access** button
+
+Infisical will:
+1. Assume the PAM 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.
+
+## 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.
+
+## Example: Multi-Environment Setup
+
+A typical setup might include:
+
+**Resource**: `aws-production` (PAM Role in production account)
+- **Account**: `readonly` → `arn:aws:iam::111111111111:role/pam-readonly`
+- **Account**: `developer` → `arn:aws:iam::111111111111:role/pam-developer`
+- **Account**: `admin` → `arn:aws:iam::111111111111:role/pam-admin`
+
+**Resource**: `aws-staging` (PAM Role in staging account)
+- **Account**: `full-access` → `arn:aws:iam::222222222222:role/pam-full-access`
+
+Users can then be granted access to specific accounts based on their role and the approval workflows configured in Infisical.
\ No newline at end of file
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/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx
index 677fb9e90..90e6cfa2e 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,6 +27,9 @@ 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 Props = {
account?: TAwsIamAccount;
resourceId?: string;
@@ -42,12 +47,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({
@@ -87,16 +109,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 +147,22 @@ 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({
+ ...formData,
+ credentials: {
+ ...formData.credentials,
+ defaultSessionDuration: durationSeconds
+ }
+ } as any);
+ };
+
return (
-