mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 00:29:04 +00:00
Dynamic Secrets: add Temporary Credentials for AWS IAM Roles
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
PutUserPolicyCommand,
|
||||
RemoveUserFromGroupCommand
|
||||
} from "@aws-sdk/client-iam";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { AssumeRoleCommand, GetSessionTokenCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
@@ -26,9 +26,14 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
// AWS STS duration constants (in seconds)
|
||||
const AWS_STS_MIN_DURATION = 900; // 15 minutes
|
||||
const AWS_STS_MAX_DURATION_SESSION_TOKEN = 43200; // 12 hours for GetSessionToken
|
||||
const AWS_STS_MAX_DURATION_ASSUME_ROLE = 43200; // 12 hours for AssumeRole
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
@@ -120,6 +125,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
try {
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials: {
|
||||
accessKeyId: providerInputs.accessKey,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
|
||||
return true;
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
const appCfg = getConfig();
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials:
|
||||
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
await stsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: providerInputs.roleArn,
|
||||
RoleSessionName: `infisical-validation-${crypto.nativeCrypto.randomUUID()}`,
|
||||
DurationSeconds: AWS_STS_MIN_DURATION,
|
||||
ExternalId: projectId
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.IRSA) {
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
});
|
||||
|
||||
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
@@ -137,13 +194,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [];
|
||||
const sensitiveTokens: string[] = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
@@ -163,102 +228,258 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
metadata: { projectId: string };
|
||||
}) => {
|
||||
const { inputs, usernameTemplate, metadata, identity } = data;
|
||||
const { inputs, usernameTemplate, metadata, identity, expireAt } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
try {
|
||||
let stsClient: STSClient;
|
||||
let entityId: string;
|
||||
|
||||
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
|
||||
const additionalTags = providerInputs.tags.map((tag) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}));
|
||||
awsTags.push(...additionalTags);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const requestedDuration = expireAt - currentTime;
|
||||
|
||||
if (requestedDuration <= 0) {
|
||||
throw new BadRequestError({ message: "Expiration time must be in the future" });
|
||||
}
|
||||
|
||||
let durationSeconds = Math.min(requestedDuration, AWS_STS_MAX_DURATION_SESSION_TOKEN);
|
||||
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
const appCfg = getConfig();
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials:
|
||||
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
durationSeconds = Math.min(durationSeconds, AWS_STS_MAX_DURATION_ASSUME_ROLE);
|
||||
|
||||
const assumeRoleRes = await stsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: providerInputs.roleArn,
|
||||
RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`,
|
||||
DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION),
|
||||
ExternalId: metadata.projectId
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!assumeRoleRes.Credentials?.AccessKeyId ||
|
||||
!assumeRoleRes.Credentials?.SecretAccessKey ||
|
||||
!assumeRoleRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||
}
|
||||
|
||||
entityId = `assume-role-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: assumeRoleRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: assumeRoleRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: assumeRoleRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials: {
|
||||
accessKeyId: providerInputs.accessKey,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
const sessionTokenRes = await stsClient.send(
|
||||
new GetSessionTokenCommand({
|
||||
DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION)
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!sessionTokenRes.Credentials?.AccessKeyId ||
|
||||
!sessionTokenRes.Credentials?.SecretAccessKey ||
|
||||
!sessionTokenRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({ message: "Failed to get session token - verify credentials and permissions" });
|
||||
}
|
||||
|
||||
entityId = `session-token-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.IRSA) {
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
});
|
||||
|
||||
const sessionTokenRes = await stsClient.send(
|
||||
new GetSessionTokenCommand({
|
||||
DurationSeconds: Math.max(durationSeconds, AWS_STS_MIN_DURATION)
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!sessionTokenRes.Credentials?.AccessKeyId ||
|
||||
!sessionTokenRes.Credentials?.SecretAccessKey ||
|
||||
!sessionTokenRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get session token - verify IRSA credentials and permissions"
|
||||
});
|
||||
}
|
||||
|
||||
entityId = `irsa-session-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Unsupported authentication method for temporary credentials" });
|
||||
} catch (err) {
|
||||
const sensitiveTokens: string[] = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create temporary credentials: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.IamUser) {
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
|
||||
|
||||
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
|
||||
const additionalTags = providerInputs.tags.map((tag) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}));
|
||||
awsTags.push(...additionalTags);
|
||||
}
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid credential type specified" });
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
return { entityId };
|
||||
}
|
||||
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
@@ -32,6 +32,11 @@ export enum AwsIamAuthType {
|
||||
IRSA = "irsa"
|
||||
}
|
||||
|
||||
export enum AwsIamCredentialType {
|
||||
IamUser = "iam-user",
|
||||
TemporaryCredentials = "temporary-credentials"
|
||||
}
|
||||
|
||||
export enum ElasticSearchAuthTypes {
|
||||
User = "user",
|
||||
ApiKey = "api-key"
|
||||
@@ -202,6 +207,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AccessKey),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
@@ -214,6 +220,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AssumeRole),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
roleArn: z.string().trim().min(1, "Role ARN required"),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
@@ -225,6 +232,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.IRSA),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
|
||||
Reference in New Issue
Block a user