feat(dynamic-secrets): AWS IRSA auth method

This commit is contained in:
x032205
2025-07-05 00:15:54 -04:00
parent 06f5af1200
commit a0e8496256
14 changed files with 314 additions and 21 deletions
@@ -16,12 +16,13 @@ import {
PutUserPolicyCommand,
RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand, STSClient } from "@aws-sdk/client-sts";
import { randomUUID } from "crypto";
import { promises as fs } from "fs";
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 +82,54 @@ 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."
});
}
const tokenFilePath =
appCfg.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH || "/var/run/secrets/kubernetes.io/serviceaccount/token";
let webIdentityToken;
try {
webIdentityToken = await fs.readFile(tokenFilePath, "utf-8");
} catch (error) {
throw new BadRequestError({
message: `Failed to get AWS credentials via IRSA: service account token not found at ${tokenFilePath}`
});
}
const stsClient = new STSClient({
region: providerInputs.region
});
const command = new AssumeRoleWithWebIdentityCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-dynamic-secret-irsa-${randomUUID()}`,
WebIdentityToken: webIdentityToken,
DurationSeconds: 900 // 15 mins
});
const assumeRes = await stsClient.send(command);
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
throw new BadRequestError({ message: "Failed to assume role with web identity - verify IRSA configuration" });
}
const client = new IAMClient({
region: providerInputs.region,
credentials: {
accessKeyId: assumeRes.Credentials.AccessKeyId,
secretAccessKey: assumeRes.Credentials.SecretAccessKey,
sessionToken: assumeRes.Credentials.SessionToken
}
});
return client;
}
const client = new IAMClient({
region: providerInputs.region,
credentials: {
@@ -101,7 +150,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")
) {
@@ -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,17 @@ 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),
roleArn: z.string().trim().min(1, "Role ARN required"),
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()
})
])
);
+2
View File
@@ -28,6 +28,8 @@ const databaseReadReplicaSchema = z
const envSchema = z
.object({
INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()),
INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH: 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"])
+4 -2
View File
@@ -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
}
};
}