mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 10:29:52 +00:00
feat(dynamic-secrets): AWS IRSA auth method
This commit is contained in:
@@ -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()
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user