mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 07:28:21 +00:00
Merge pull request #4976 from Infisical/chore/external-kms-api-refactor
chore: external-kms API refactor
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user