Merge pull request #4976 from Infisical/chore/external-kms-api-refactor

chore: external-kms API refactor
This commit is contained in:
Piyush Gupta
2025-12-06 01:23:05 +05:30
committed by GitHub
19 changed files with 1093 additions and 420 deletions
@@ -4,15 +4,10 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
ExternalKmsAwsSchema,
ExternalKmsGcpCredentialSchema,
ExternalKmsGcpSchema,
ExternalKmsInputSchema,
ExternalKmsInputUpdateSchema,
KmsGcpKeyFetchAuthType,
KmsProviders,
TExternalKmsGcpCredentialSchema
ExternalKmsInputUpdateSchema
} from "@app/ee/services/external-kms/providers/model";
import { NotFoundError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -293,67 +288,4 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
return { externalKms };
}
});
server.route({
method: "POST",
url: "/gcp/keys",
config: {
rateLimit: writeLimit
},
schema: {
body: z.discriminatedUnion("authMethod", [
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
region: z.string().trim().min(1),
credential: ExternalKmsGcpCredentialSchema
}),
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
region: z.string().trim().min(1),
kmsId: z.string().trim().min(1)
})
]),
response: {
200: z.object({
keys: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { region, authMethod } = req.body;
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
if (authMethod === KmsGcpKeyFetchAuthType.Credential) {
credentialJson = req.body.credential;
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms) {
const externalKms = await server.services.externalKms.findById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.kmsId
});
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
}
credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema;
}
if (!credentialJson) {
throw new NotFoundError({
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
});
}
const results = await server.services.externalKms.fetchGcpKeys({
credential: credentialJson,
gcpRegion: region
});
return results;
}
});
};
@@ -0,0 +1,12 @@
import { ExternalKmsAwsSchema, KmsProviders } from "@app/ee/services/external-kms/providers/model";
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
export const registerAwsKmsRouter = async (server: FastifyZodProvider) => {
registerExternalKmsEndpoints({
server,
provider: KmsProviders.Aws,
createSchema: ExternalKmsAwsSchema,
updateSchema: ExternalKmsAwsSchema.partial()
});
};
@@ -0,0 +1,288 @@
import { z } from "zod";
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
KmsProviders,
SanitizedExternalKmsAwsSchema,
SanitizedExternalKmsGcpSchema,
TExternalKmsInputSchema,
TExternalKmsInputUpdateSchema
} from "@app/ee/services/external-kms/providers/model";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const sanitizedExternalSchema = KmsKeysSchema.extend({
externalKms: ExternalKmsSchema.pick({
id: true,
status: true,
statusDetails: true,
provider: true
}).extend({
configuration: z.union([SanitizedExternalKmsAwsSchema, SanitizedExternalKmsGcpSchema]),
credentialsHash: z.string().optional()
})
});
export const registerExternalKmsEndpoints = <
T extends { type: KmsProviders; inputs: TExternalKmsInputSchema["inputs"] }
>({
server,
provider,
createSchema,
updateSchema
}: {
server: FastifyZodProvider;
provider: T["type"];
createSchema: z.ZodType<T["inputs"]>;
updateSchema: z.ZodType<Partial<T["inputs"]>>;
}) => {
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string().trim().min(1)
}),
response: {
200: sanitizedExternalSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const externalKms = await server.services.externalKms.findById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
// Validate that the KMS is of the expected provider type
if (externalKms.external.provider !== provider) {
throw new BadRequestError({
message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`
});
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMS,
metadata: {
kmsId: externalKms.id,
name: externalKms.name
}
}
});
const {
external: { providerInput: configuration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
name: z.string().min(1).trim().toLowerCase(),
description: z.string().trim().optional(),
configuration: createSchema
}),
response: {
200: sanitizedExternalSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, description, configuration } = req.body as {
name: string;
description?: string;
configuration: T["inputs"];
};
const providerInput = {
type: provider,
inputs: configuration
} as TExternalKmsInputSchema;
const externalKms = await server.services.externalKms.create({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name,
provider: providerInput,
description
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_KMS,
metadata: {
kmsId: externalKms.id,
provider,
name,
description
}
}
});
const {
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string().trim().min(1)
}),
body: z.object({
name: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().trim().optional(),
configuration: updateSchema.optional()
}),
response: {
200: sanitizedExternalSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, description, configuration } = req.body as {
name?: string;
description?: string;
configuration: Partial<T["inputs"]>;
};
const providerInput = {
type: provider,
inputs: configuration
} as TExternalKmsInputUpdateSchema;
const externalKms = await server.services.externalKms.updateById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name,
provider: providerInput,
description,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_KMS,
metadata: {
kmsId: externalKms.id,
provider,
name,
description
}
}
});
const {
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string().trim().min(1)
}),
response: {
200: sanitizedExternalSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const externalKms = await server.services.externalKms.deleteById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
// Validate that the KMS is of the expected provider type
if (externalKms.external.provider !== provider) {
throw new BadRequestError({
message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`
});
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_KMS,
metadata: {
kmsId: externalKms.id,
name: externalKms.name
}
}
});
const {
external: { providerInput: configuration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
}
});
};
@@ -0,0 +1,88 @@
import { z } from "zod";
import {
ExternalKmsGcpCredentialSchema,
ExternalKmsGcpSchema,
KmsGcpKeyFetchAuthType,
KmsProviders,
TExternalKmsGcpCredentialSchema
} from "@app/ee/services/external-kms/providers/model";
import { NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
export const registerGcpKmsRouter = async (server: FastifyZodProvider) => {
registerExternalKmsEndpoints({
server,
provider: KmsProviders.Gcp,
createSchema: ExternalKmsGcpSchema,
updateSchema: ExternalKmsGcpSchema.partial()
});
server.route({
method: "POST",
url: "/keys",
config: {
rateLimit: writeLimit
},
schema: {
body: z.discriminatedUnion("authMethod", [
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
region: z.string().trim().min(1),
credential: ExternalKmsGcpCredentialSchema
}),
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
region: z.string().trim().min(1),
kmsId: z.string().trim().min(1)
})
]),
response: {
200: z.object({
keys: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { region, authMethod } = req.body;
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
if (authMethod === KmsGcpKeyFetchAuthType.Credential && "credential" in req.body) {
credentialJson = req.body.credential;
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms && "kmsId" in req.body) {
const externalKms = await server.services.externalKms.findById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.kmsId
});
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
}
const providerInput = externalKms.external.providerInput as { credential: TExternalKmsGcpCredentialSchema };
credentialJson = providerInput.credential;
}
if (!credentialJson) {
throw new NotFoundError({
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
});
}
const results = await server.services.externalKms.fetchGcpKeys({
credential: credentialJson,
gcpRegion: region
});
return results;
}
});
};
@@ -0,0 +1,9 @@
import { KmsProviders } from "@app/ee/services/external-kms/providers/model";
import { registerAwsKmsRouter } from "./aws-kms-router";
import { registerGcpKmsRouter } from "./gcp-kms-router";
export const EXTERNAL_KMS_REGISTER_ROUTER_MAP: Record<KmsProviders, (server: FastifyZodProvider) => Promise<void>> = {
[KmsProviders.Aws]: registerAwsKmsRouter,
[KmsProviders.Gcp]: registerGcpKmsRouter
};
+15 -3
View File
@@ -12,6 +12,8 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers";
import { registerGatewayRouter } from "./gateway-router";
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
import { registerGroupRouter } from "./group-router";
@@ -162,9 +164,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/additional-privilege" }
);
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
await server.register(
async (externalKmsRouter) => {
await externalKmsRouter.register(registerExternalKmsRouter);
// Provider-specific endpoints
await Promise.all(
Object.entries(EXTERNAL_KMS_REGISTER_ROUTER_MAP).map(([provider, router]) =>
externalKmsRouter.register(router, { prefix: `/${provider}` })
)
);
},
{ prefix: "/external-kms" }
);
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
@@ -24,7 +24,13 @@ import {
} from "./external-kms-types";
import { AwsKmsProviderFactory } from "./providers/aws-kms";
import { GcpKmsProviderFactory } from "./providers/gcp-kms";
import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model";
import {
ExternalKmsAwsSchema,
ExternalKmsGcpSchema,
KmsProviders,
TExternalKmsAwsSchema,
TExternalKmsGcpSchema
} from "./providers/model";
type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory;
@@ -72,6 +78,7 @@ export const externalKmsServiceFactory = ({
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
let sanitizedProviderInput = "";
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
switch (provider.type) {
case KmsProviders.Aws:
{
@@ -88,9 +95,18 @@ export const externalKmsServiceFactory = ({
try {
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInputObject = newProviderInput;
sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection();
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection"
});
} finally {
await externalKms.cleanup();
}
@@ -101,7 +117,16 @@ export const externalKmsServiceFactory = ({
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
try {
await externalKms.validateConnection();
sanitizedProviderInputObject = provider.inputs;
sanitizedProviderInput = JSON.stringify(provider.inputs);
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection"
});
} finally {
await externalKms.cleanup();
}
@@ -139,7 +164,10 @@ export const externalKmsServiceFactory = ({
},
tx
);
return { ...kms, external: externalKmsCfg };
return {
...kms,
external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject }
};
});
return externalKms;
@@ -179,6 +207,7 @@ export const externalKmsServiceFactory = ({
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
let sanitizedProviderInput = "";
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
@@ -199,7 +228,16 @@ export const externalKmsServiceFactory = ({
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
try {
await externalKms.validateConnection();
sanitizedProviderInputObject = updatedProviderInput;
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection"
});
} finally {
await externalKms.cleanup();
}
@@ -214,7 +252,16 @@ export const externalKmsServiceFactory = ({
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
try {
await externalKms.validateConnection();
sanitizedProviderInputObject = updatedProviderInput;
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection"
});
} finally {
await externalKms.cleanup();
}
@@ -234,14 +281,17 @@ export const externalKmsServiceFactory = ({
}
const externalKms = await externalKmsDAL.transaction(async (tx) => {
const kms = await kmsDAL.updateById(
kmsDoc.id,
{
description,
name: kmsName
},
tx
);
let kms = kmsDoc;
if (kmsName || description) {
kms = await kmsDAL.updateById(
kmsDoc.id,
{
description,
name: kmsName
},
tx
);
}
if (encryptedProviderInputs) {
const externalKmsCfg = await externalKmsDAL.updateById(
externalKmsDoc.id,
@@ -250,9 +300,9 @@ export const externalKmsServiceFactory = ({
},
tx
);
return { ...kms, external: externalKmsCfg };
return { ...kms, external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } };
}
return { ...kms, external: externalKmsDoc };
return { ...kms, external: { ...externalKmsDoc, providerInput: sanitizedProviderInputObject } };
});
return externalKms;
@@ -273,9 +323,40 @@ export const externalKmsServiceFactory = ({
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
let decryptedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});
switch (externalKmsDoc.provider) {
case KmsProviders.Aws: {
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString())
);
decryptedProviderInputObject = decryptedProviderInput;
break;
}
case KmsProviders.Gcp: {
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString())
);
decryptedProviderInputObject = decryptedProviderInput;
break;
}
default:
break;
}
const externalKms = await externalKmsDAL.transaction(async (tx) => {
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
return { ...kms, external: externalKmsDoc };
return { ...kms, external: { ...externalKmsDoc, providerInput: decryptedProviderInputObject } };
});
return externalKms;
@@ -393,6 +474,14 @@ export const externalKmsServiceFactory = ({
const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } });
try {
return await externalKms.getKeysList();
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to fetch GCP keys"
});
} finally {
await externalKms.cleanup();
}
@@ -3,6 +3,7 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
@@ -22,7 +23,7 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
});
const response = await stsClient.send(command);
if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey)
throw new Error("Failed to assume role");
throw new BadRequestError({ message: "Failed to assume role" });
const kmsClient = new KMSClient({
region: providerInputs.awsRegion,
@@ -67,7 +68,7 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
const kmsKey = await awsClient.send(command);
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
if (!kmsKey.KeyMetadata?.KeyId) throw new BadRequestError({ message: "Failed to generate kms key" });
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
...providerInputs,
@@ -19,27 +19,31 @@ export enum KmsGcpKeyFetchAuthType {
Kms = "kmsId"
}
const AwsConnectionAssumeRoleCredentialsSchema = z.object({
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
externalId: z
.string()
.trim()
.min(1)
.optional()
.describe("AWS assume role external id for further security in authentication")
});
const AwsConnectionAccessTokenCredentialsSchema = z.object({
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
});
export const ExternalKmsAwsSchema = z.object({
credential: z
.discriminatedUnion("type", [
z.object({
type: z.literal(KmsAwsCredentialType.AccessKey),
data: z.object({
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
})
data: AwsConnectionAccessTokenCredentialsSchema
}),
z.object({
type: z.literal(KmsAwsCredentialType.AssumeRole),
data: z.object({
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
externalId: z
.string()
.trim()
.min(1)
.optional()
.describe("AWS assume role external id for furthur security in authentication")
})
data: AwsConnectionAssumeRoleCredentialsSchema
})
])
.describe("AWS credential information to connect"),
@@ -52,6 +56,22 @@ export const ExternalKmsAwsSchema = z.object({
});
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
export const SanitizedExternalKmsAwsSchema = ExternalKmsAwsSchema.extend({
credential: z.discriminatedUnion("type", [
z.object({
type: z.literal(KmsAwsCredentialType.AccessKey),
data: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKey: true })
}),
z.object({
type: z.literal(KmsAwsCredentialType.AssumeRole),
data: AwsConnectionAssumeRoleCredentialsSchema.pick({
assumeRoleArn: true,
externalId: true
})
})
])
});
export const ExternalKmsGcpCredentialSchema = z.object({
type: z.literal(KmsGcpCredentialType.ServiceAccount),
project_id: z.string().min(1),
@@ -75,6 +95,8 @@ export const ExternalKmsGcpSchema = z.object({
});
export type TExternalKmsGcpSchema = z.infer<typeof ExternalKmsGcpSchema>;
export const SanitizedExternalKmsGcpSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true });
const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({
credential: ExternalKmsGcpCredentialSchema
});
+20 -15
View File
@@ -253,7 +253,7 @@ export const kmsServiceFactory = ({
}
if (!org.kmsDefaultKeyId) {
throw new Error("Invalid organization KMS");
throw new BadRequestError({ message: "Invalid organization KMS" });
}
return org.kmsDefaultKeyId;
@@ -292,7 +292,7 @@ export const kmsServiceFactory = ({
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
throw new Error("Invalid organization KMS");
throw new BadRequestError({ message: "Invalid organization KMS" });
}
// The idea is external kms connection info is encrypted by an org default KMS
@@ -338,7 +338,7 @@ export const kmsServiceFactory = ({
break;
}
default:
throw new Error("Invalid KMS provider.");
throw new BadRequestError({ message: "Invalid KMS provider." });
}
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
@@ -509,7 +509,7 @@ export const kmsServiceFactory = ({
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
throw new Error("Invalid organization KMS");
throw new BadRequestError({ message: "Invalid organization KMS" });
}
const orgKmsDecryptor = await decryptWithKmsKey({
@@ -550,7 +550,7 @@ export const kmsServiceFactory = ({
break;
}
default:
throw new Error("Invalid KMS provider.");
throw new BadRequestError({ message: "Invalid KMS provider." });
}
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
@@ -651,7 +651,7 @@ export const kmsServiceFactory = ({
}
if (!org.kmsEncryptedDataKey) {
throw new Error("Invalid organization KMS");
throw new BadRequestError({ message: "Invalid organization KMS" });
}
const kmsDecryptor = await decryptWithKmsKey({
@@ -723,7 +723,7 @@ export const kmsServiceFactory = ({
}
if (!project.kmsSecretManagerKeyId) {
throw new Error("Missing project KMS key ID");
throw new BadRequestError({ message: "Missing project KMS key ID" });
}
return project.kmsSecretManagerKeyId;
@@ -832,9 +832,10 @@ export const kmsServiceFactory = ({
const isBase64 = !envConfig.ENCRYPTION_KEY;
if (!encryptionKey)
throw new Error(
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
);
throw new BadRequestError({
message:
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
});
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
@@ -846,7 +847,9 @@ export const kmsServiceFactory = ({
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
throw new BadRequestError({
message: "Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"
});
}
const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey);
@@ -861,14 +864,16 @@ export const kmsServiceFactory = ({
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
}
throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`);
throw new BadRequestError({ message: `Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}` });
};
const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => {
if (strategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
throw new BadRequestError({
message: "Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"
});
}
const encrypted = await hsmService.encrypt(plainKeyBuffer);
return encrypted;
@@ -882,7 +887,7 @@ export const kmsServiceFactory = ({
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Invalid root key encryption strategy: ${strategy}`);
throw new BadRequestError({ message: `Invalid root key encryption strategy: ${strategy}` });
};
// by keeping the decrypted data key in inner scope
@@ -1130,7 +1135,7 @@ export const kmsServiceFactory = ({
if (!encryptedRootKey) {
logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy");
throw new Error("Failed to re-encrypt ROOT Key with selected strategy");
throw new BadRequestError({ message: "Failed to re-encrypt ROOT Key with selected strategy" });
}
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {