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/*" + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-policy.png) + + + **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": "" + } + } + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-trust-policy.png) + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-attach-policy.png) + + + **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 + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/create-resource.png) + + 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": "" + } + } + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/target-role-trust-policy.png) + + + + 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: + + ![Create AWS IAM Account](/images/pam/resources/aws-iam/create-account.png) + + + 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: + +![Create AWS IAM Resource](/images/pam/resources/aws-iam/access-account.png) + + + + 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 ( -
+
@@ -139,12 +204,12 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit render={({ field, fieldState: { error } }) => ( } > - + )} /> @@ -164,10 +229,9 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit

- The target role must have a trust policy that allows the PAM role (created in the - "Resources" tab) to assume it. If your target role name follows the - wildcard pattern you defined in the PAM role's permissions policy, no - additional changes are needed. + The target role must have a trust policy that allows the Resource Role (created in + the "Resources" tab) to assume it. Ensure the target role's trust + policy includes the Resource Role as a trusted principal.

@@ -182,12 +246,11 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit

- Note: The Principal role ARN shown above is from the PAM Resource + Note: The Principal role ARN shown above is from the Resource selected for this account. The External ID{" "} {projectId} is your - current project ID. If your target role name doesn't match the wildcard pattern - in your PAM Resource's role's permissions policy, you'll need to - update that policy to include this role's ARN. + current project ID. If you configured granular permissions in your Resource + Role's policy, ensure this target role's ARN is included.

diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountHeader.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountHeader.tsx index 0b84deb38..72120af8e 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountHeader.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountHeader.tsx @@ -1,5 +1,10 @@ +import { DocumentationLinkBadge } from "@app/components/v3"; import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam"; +const PAM_ACCOUNT_DOCS_MAP: Partial> = { + [PamResourceType.AwsIam]: "aws-iam#create-pam-accounts" +}; + type Props = { resourceName: string; resourceType: PamResourceType; @@ -8,6 +13,7 @@ type Props = { export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props) => { const details = PAM_RESOURCE_TYPE_MAP[resourceType]; + const docsPath = PAM_ACCOUNT_DOCS_MAP[resourceType]; return (
@@ -17,7 +23,14 @@ export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props) className="h-12 w-12 rounded-md bg-bunker-500 p-2" />
-
{resourceName}
+
+ {resourceName} + {docsPath && ( + + )} +

{details.name} resource

{onBack && ( diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/AwsIamResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/AwsIamResourceForm.tsx index ec97d0a6f..671f913e4 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/AwsIamResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/AwsIamResourceForm.tsx @@ -30,7 +30,7 @@ const AwsIamConnectionDetailsSchema = z.object({ roleArn: z .string() .trim() - .min(1, "PAM Role ARN is required") + .min(1, "Resource Role ARN is required") .refine((val) => arnRoleRegex.test(val), { message: "ARN must be in the format 'arn:aws:iam::123456789012:role/RoleName'" }) @@ -57,7 +57,7 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => { "Statement": [{ "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam:::role/-*" + "Resource": "arn:aws:iam:::role/*" }] }`; @@ -116,12 +116,15 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => { control={control} render={({ field, fieldState: { error } }) => ( - + )} /> @@ -148,12 +151,12 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => { Step 1: Create a permissions policy for assuming target roles

- This policy allows the PAM role to assume target roles. We recommend using a - wildcard pattern (e.g.,{" "} - pam-* or{" "} - privileged-*) so you - can add new accounts without updating this policy. Choose a prefix that fits your - naming conventions. + This policy allows the Resource Role to assume target roles. For simplicity, use a + wildcard to allow assuming any role in your account. For more granular control, + replace * with a + specific pattern like{" "} + /pam-* or{" "} + /infisical-*.

@@ -165,12 +168,12 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {

- Step 2: Create the PAM role with a trust policy + Step 2: Create the Resource Role with a trust policy

Create an IAM role (e.g.,{" "} - InfisicalPAMRole) - with the permissions policy above and the following trust policy: + InfisicalResourceRole + ) with the permissions policy above and the following trust policy:

@@ -189,11 +192,8 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => { {INFISICAL_AWS_ACCOUNT_EU} {" "} - for EU region. Replace{" "} - - <INFISICAL_AWS_ACCOUNT_ID> - {" "} - with the appropriate Infisical AWS account ID for your region. The External ID{" "} + for EU region. For dedicated instances, contact Infisical support. For self-hosted + instances, use your Infisical deployment's AWS account ID. The External ID{" "} {projectId} is your current project ID.

diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceHeader.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceHeader.tsx index 4b4fd34c4..c9b5e9274 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceHeader.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceHeader.tsx @@ -1,5 +1,10 @@ +import { DocumentationLinkBadge } from "@app/components/v3"; import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam"; +const PAM_RESOURCE_DOCS_MAP: Partial> = { + [PamResourceType.AwsIam]: "aws-iam#create-the-pam-resource" +}; + type Props = { resourceType: PamResourceType; onBack?: () => void; @@ -7,6 +12,7 @@ type Props = { export const PamResourceHeader = ({ resourceType, onBack }: Props) => { const details = PAM_RESOURCE_TYPE_MAP[resourceType]; + const docsPath = PAM_RESOURCE_DOCS_MAP[resourceType]; return (
@@ -16,7 +22,14 @@ export const PamResourceHeader = ({ resourceType, onBack }: Props) => { className="h-12 w-12 rounded-md bg-bunker-500 p-2" />
-
{details.name}
+
+ {details.name} + {docsPath && ( + + )} +

Resource

{onBack && (