diff --git a/.env.example b/.env.example index 53e36449a..05a888db0 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,7 @@ REDIS_URL=redis://redis:6379 # Required SITE_URL=http://localhost:8080 -# Mail/SMTP +# Mail/SMTP SMTP_HOST= SMTP_PORT= SMTP_FROM_ADDRESS= @@ -132,3 +132,6 @@ DATADOG_PROFILING_ENABLED= DATADOG_ENV= DATADOG_SERVICE= DATADOG_HOSTNAME= + +# kubernetes +KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN=false diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 01113f019..e07c8a9da 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -34,6 +34,7 @@ ARG INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV NODE_OPTIONS="--max-old-space-size=8192" # Build RUN npm run build @@ -77,6 +78,7 @@ RUN npm ci --only-production COPY /backend . COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh RUN npm i -D tsconfig-paths +ENV NODE_OPTIONS="--max-old-space-size=8192" RUN npm run build # Production stage diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index ec75bb2e4..329715941 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -21,7 +21,7 @@ import { randomUUID } from "crypto"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; @@ -81,6 +81,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return client; } + if (providerInputs.method === AwsIamAuthType.IRSA) { + // Allow instances to disable automatic service account token fetching (e.g. for shared cloud) + if (!appCfg.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN) { + throw new UnauthorizedError({ + message: "Failed to get AWS credentials via IRSA: KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN is not enabled." + }); + } + + // The SDK will automatically pick up credentials from the environment + const client = new IAMClient({ + region: providerInputs.region + }); + return client; + } + const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -101,7 +116,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { .catch((err) => { const message = (err as Error)?.message; if ( - providerInputs.method === AwsIamAuthType.AssumeRole && + (providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) && // assume role will throw an error asking to provider username, but if so this has access in aws correctly message.includes("Must specify userName when calling with non-User credentials") ) { diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index f6fa2a4a8..528ea414a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -28,7 +28,8 @@ export enum SqlProviders { export enum AwsIamAuthType { AssumeRole = "assume-role", - AccessKey = "access-key" + AccessKey = "access-key", + IRSA = "irsa" } export enum ElasticSearchAuthTypes { @@ -221,6 +222,16 @@ export const DynamicSecretAwsIamSchema = z.preprocess( userGroups: z.string().trim().optional(), policyArns: z.string().trim().optional(), tags: ResourceMetadataSchema.optional() + }), + z.object({ + method: z.literal(AwsIamAuthType.IRSA), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: ResourceMetadataSchema.optional() }) ]) ); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index c881f9daf..38b34f488 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -28,6 +28,7 @@ const databaseReadReplicaSchema = z const envSchema = z .object({ INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()), + KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN: zodStrBool.default("false"), PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000), DISABLE_SECRET_SCANNING: z .enum(["true", "false"]) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 81e911621..c3b204c48 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -49,7 +49,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgAuthEnforced: z.boolean().nullish(), defaultAuthOrgAuthMethod: z.string().nullish(), - isSecretScanningDisabled: z.boolean() + isSecretScanningDisabled: z.boolean(), + kubernetesAutoFetchServiceAccountToken: z.boolean() }) }) } @@ -61,7 +62,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE, - isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING + isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING, + kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN } }; } diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 03bc5df84..28b177c5f 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -3,13 +3,13 @@ title: "AWS IAM" 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 configured AWS policy. +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. ## Prerequisite -Infisical needs an initial AWS IAM user with the required permissions to create sub IAM users. This IAM user will be responsible for managing the lifecycle of new IAM users. +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. - + ```json { @@ -235,7 +235,169 @@ Replace **\** with your AWS account id and **\** w ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + This method is recommended for self-hosted Infisical instances running on AWS EKS. It uses [IAM Roles for Service Accounts (IRSA)](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) to securely grant permissions to the Infisical pods without managing static credentials. + + In order to use IRSA, the `KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN` environment variable must be set to `true` for your self-hosted Infisical instance. + + + + + If you don't already have one, you need to create an IAM OIDC provider for your EKS cluster. This allows IAM to trust authentication tokens from your Kubernetes cluster. + 1. Find your cluster's OIDC provider URL from the EKS console or by using the AWS CLI: + `aws eks describe-cluster --name --query "cluster.identity.oidc.issuer" --output text` + 2. Navigate to the [IAM Identity Providers](https://console.aws.amazon.com/iam/home#/providers) page in your AWS Console and create a new OpenID Connect provider with the URL and `sts.amazonaws.com` as the audience. + + ![Create OIDC Provider Placeholder](/images/integrations/aws/irsa-create-oidc-provider.png) + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + 2. Select **Web identity** as the **Trusted Entity Type**. + 3. Choose the OIDC provider you created in the previous step. + 4. For the **Audience**, select `sts.amazonaws.com`. + ![IAM Role Creation for IRSA](/images/integrations/aws/irsa-iam-role-creation.png) + 5. Attach the permission policy detailed in the **Prerequisite** section at the top of this page. + 6. After creating the role, edit its **Trust relationship** to specify the service account Infisical is using in your cluster. This ensures only the Infisical pod can assume this role. + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam:::oidc-provider/oidc.eks..amazonaws.com/id/" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "oidc.eks..amazonaws.com/id/:sub": "system:serviceaccount::", + "oidc.eks..amazonaws.com/id/:aud": "sts.amazonaws.com" + } + } + } + ] + } + ``` + Replace ``, ``, ``, ``, and `` with your specific values. + + + For the IRSA mechanism to work, the Infisical service account in your Kubernetes cluster must be annotated with the ARN of the IAM role you just created. + + Run the following command, replacing the placeholders with your values: + ```bash + kubectl annotate serviceaccount -n \ + eks.amazonaws.com/role-arn=arn:aws:iam:::role/ + ``` + This annotation tells the EKS Pod Identity Webhook to inject the necessary environment variables and tokens into the Infisical pod, allowing it to assume the specified IAM role. + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal for IRSA](/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png) + + Name by which you want the secret to be referenced + + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + + + Maximum time-to-live for a generated secret + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{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 + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + + + Tags to be added to the created IAM User resource. + + + Select *IRSA* method. + + + The ARN of the AWS IAM Role for the service account to assume. + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + + + The AWS data center region. + + + 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 AWS IAM groups that should be assigned 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 + + + The AWS IAM inline policy that should be attached to the created users. + Multiple values can be provided by separating them with commas + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance. @@ -263,9 +425,9 @@ Replace **\** with your AWS account id and **\** w Maximum time-to-live for a generated secret - - Select *Access Key* method. - + + Select *Access Key* method. + The managing AWS IAM User Access Key diff --git a/docs/images/integrations/aws/irsa-create-oidc-provider.png b/docs/images/integrations/aws/irsa-create-oidc-provider.png new file mode 100644 index 000000000..aff8d600e Binary files /dev/null and b/docs/images/integrations/aws/irsa-create-oidc-provider.png differ diff --git a/docs/images/integrations/aws/irsa-iam-role-creation.png b/docs/images/integrations/aws/irsa-iam-role-creation.png new file mode 100644 index 000000000..8fd725bca Binary files /dev/null and b/docs/images/integrations/aws/irsa-iam-role-creation.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png new file mode 100644 index 000000000..0051b0cf4 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png differ diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 42d289804..1e050ff14 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -59,6 +59,15 @@ Example values: connect with internal/private IP addresses. + + Determines whether your Infisical instance can automatically read the service account token of the pod it's running on. Used for features such as the IRSA auth method. + + ## CORS Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain. diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 296c6a87d..63446c95f 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -26,6 +26,7 @@ export const envConfig = { import.meta.env.VITE_TELEMETRY_CAPTURING_ENABLED === true ); }, + get PLATFORM_VERSION() { return import.meta.env.VITE_INFISICAL_PLATFORM_VERSION; } diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 4580c6581..e7d1f6a48 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -40,6 +40,7 @@ export type TServerConfig = { trustLdapEmails: boolean; trustOidcEmails: boolean; isSecretScanningDisabled: boolean; + kubernetesAutoFetchServiceAccountToken: boolean; defaultAuthOrgSlug: string | null; defaultAuthOrgId: string | null; defaultAuthOrgAuthMethod?: string | null; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 4dc635fa2..84b618153 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -54,7 +54,8 @@ export enum SqlProviders { export enum DynamicSecretAwsIamAuth { AssumeRole = "assume-role", - AccessKey = "access-key" + AccessKey = "access-key", + IRSA = "irsa" } export type TDynamicSecretProvider = @@ -111,6 +112,14 @@ export type TDynamicSecretProvider = policyDocument?: string; userGroups?: string; policyArns?: string; + } + | { + method: DynamicSecretAwsIamAuth.IRSA; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; }; } | { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 03db38935..f9ed9967c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -15,6 +15,7 @@ import { TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; +import { useGetServerConfig } from "@app/hooks/api/admin"; import { DynamicSecretAwsIamAuth, DynamicSecretProviders @@ -61,6 +62,23 @@ const formSchema = z.object({ }) ) .optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.IRSA), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: z + .array( + z.object({ + key: z.string().trim().min(1).max(128), + value: z.string().trim().min(1).max(256) + }) + ) + .optional() }) ]), defaultTTL: z.string().superRefine((val, ctx) => { @@ -106,6 +124,8 @@ export const AwsIamInputForm = ({ projectSlug, isSingleEnvironmentMode }: Props) => { + const { data: serverConfig } = useGetServerConfig(); + const { control, formState: { isSubmitting }, @@ -123,7 +143,7 @@ export const AwsIamInputForm = ({ }); const createDynamicSecret = useCreateDynamicSecret(); - const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey; + const method = watch("provider.method"); const handleCreateDynamicSecret = async ({ name, @@ -237,11 +257,14 @@ export const AwsIamInputForm = ({ Assume Role (Recommended) Access Key + {serverConfig?.kubernetesAutoFetchServiceAccountToken && ( + IRSA (EKS) + )} )} /> - {isAccessKeyMethod ? ( + {method === DynamicSecretAwsIamAuth.AccessKey && (
- ) : ( + )} + {method === DynamicSecretAwsIamAuth.AssumeRole && (
{ @@ -83,6 +95,8 @@ export const EditDynamicSecretAwsIamForm = ({ secretPath, projectSlug }: Props) => { + const { data: serverConfig } = useGetServerConfig(); + const { control, watch, @@ -100,7 +114,7 @@ export const EditDynamicSecretAwsIamForm = ({ } } }); - const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey; + const method = watch("inputs.method"); const updateDynamicSecret = useUpdateDynamicSecret(); @@ -214,11 +228,14 @@ export const EditDynamicSecretAwsIamForm = ({ Assume Role (Recommended) Access Key + {serverConfig?.kubernetesAutoFetchServiceAccountToken && ( + IRSA (EKS) + )} )} /> - {isAccessKeyMethod ? ( + {method === DynamicSecretAwsIamAuth.AccessKey && (
- ) : ( + )} + {method === DynamicSecretAwsIamAuth.AssumeRole && (