misc: initial instance KMIP PKI setup

This commit is contained in:
Sheen Capadngan
2025-02-06 02:57:40 +08:00
parent 8b315c946c
commit 9f9ded5102
20 changed files with 873 additions and 8 deletions

View File

@@ -146,6 +146,12 @@ import {
TKmipClients,
TKmipClientsInsert,
TKmipClientsUpdate,
TKmipInstanceConfigs,
TKmipInstanceConfigsInsert,
TKmipInstanceConfigsUpdate,
TKmipInstanceServerCertificates,
TKmipInstanceServerCertificatesInsert,
TKmipInstanceServerCertificatesUpdate,
TKmsKeys,
TKmsKeysInsert,
TKmsKeysUpdate,
@@ -906,5 +912,15 @@ declare module "knex/types/tables" {
>;
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
[TableName.KmipClient]: KnexOriginal.CompositeTableType<TKmipClients, TKmipClientsInsert, TKmipClientsUpdate>;
[TableName.KmipInstanceConfig]: KnexOriginal.CompositeTableType<
TKmipInstanceConfigs,
TKmipInstanceConfigsInsert,
TKmipInstanceConfigsUpdate
>;
[TableName.KmipInstanceServerCertificates]: KnexOriginal.CompositeTableType<
TKmipInstanceServerCertificates,
TKmipInstanceServerCertificatesInsert,
TKmipInstanceServerCertificatesUpdate
>;
}
}

View File

@@ -1,6 +1,7 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
@@ -14,6 +15,55 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig);
if (!hasKmipInstanceConfigTable) {
await knex.schema.createTable(TableName.KmipInstanceConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("caKeyAlgorithm").notNullable();
t.datetime("rootCaIssuedAt").notNullable();
t.datetime("rootCaExpiration").notNullable();
t.string("rootCaSerialNumber").notNullable();
t.binary("encryptedRootCaCertificate").notNullable();
t.binary("encryptedRootCaPrivateKey").notNullable();
t.datetime("serverIntermediateCaIssuedAt").notNullable();
t.datetime("serverIntermediateCaExpiration").notNullable();
t.string("serverIntermediateCaSerialNumber");
t.binary("encryptedServerIntermediateCaCertificate").notNullable();
t.binary("encryptedServerIntermediateCaChain").notNullable();
t.binary("encryptedServerIntermediateCaPrivateKey").notNullable();
t.datetime("clientIntermediateCaIssuedAt").notNullable();
t.datetime("clientIntermediateCaExpiration").notNullable();
t.string("clientIntermediateCaSerialNumber").notNullable();
t.binary("encryptedClientIntermediateCaCertificate").notNullable();
t.binary("encryptedClientIntermediateCaChain").notNullable();
t.binary("encryptedClientIntermediateCaPrivateKey").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.KmipInstanceConfig);
}
const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates);
if (!hasKmipInstanceServerCertTable) {
await knex.schema.createTable(TableName.KmipInstanceServerCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("commonName").notNullable();
t.string("altNames").notNullable();
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
t.binary("encryptedCertificate").notNullable();
t.binary("encryptedChain").notNullable();
t.binary("encryptedPrivateKey").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -21,4 +71,15 @@ export async function down(knex: Knex): Promise<void> {
if (hasKmipClientTable) {
await knex.schema.dropTable(TableName.KmipClient);
}
const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig);
if (hasKmipInstanceConfigTable) {
await knex.schema.dropTable(TableName.KmipInstanceConfig);
await dropOnUpdateTrigger(knex, TableName.KmipInstanceConfig);
}
const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates);
if (hasKmipInstanceServerCertTable) {
await knex.schema.dropTable(TableName.KmipInstanceServerCertificates);
}
}

View File

@@ -46,6 +46,8 @@ export * from "./integration-auths";
export * from "./integrations";
export * from "./internal-kms";
export * from "./kmip-clients";
export * from "./kmip-instance-configs";
export * from "./kmip-instance-server-certificates";
export * from "./kms-key-versions";
export * from "./kms-keys";
export * from "./kms-root-config";

View File

@@ -0,0 +1,38 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipInstanceConfigsSchema = z.object({
id: z.string().uuid(),
caKeyAlgorithm: z.string(),
rootCaIssuedAt: z.date(),
rootCaExpiration: z.date(),
rootCaSerialNumber: z.string(),
encryptedRootCaCertificate: zodBuffer,
encryptedRootCaPrivateKey: zodBuffer,
serverIntermediateCaIssuedAt: z.date(),
serverIntermediateCaExpiration: z.date(),
serverIntermediateCaSerialNumber: z.string().nullable().optional(),
encryptedServerIntermediateCaCertificate: zodBuffer,
encryptedServerIntermediateCaChain: zodBuffer,
encryptedServerIntermediateCaPrivateKey: zodBuffer,
clientIntermediateCaIssuedAt: z.date(),
clientIntermediateCaExpiration: z.date(),
clientIntermediateCaSerialNumber: z.string(),
encryptedClientIntermediateCaCertificate: zodBuffer,
encryptedClientIntermediateCaChain: zodBuffer,
encryptedClientIntermediateCaPrivateKey: zodBuffer,
createdAt: z.date(),
updatedAt: z.date()
});
export type TKmipInstanceConfigs = z.infer<typeof KmipInstanceConfigsSchema>;
export type TKmipInstanceConfigsInsert = Omit<z.input<typeof KmipInstanceConfigsSchema>, TImmutableDBKeys>;
export type TKmipInstanceConfigsUpdate = Partial<Omit<z.input<typeof KmipInstanceConfigsSchema>, TImmutableDBKeys>>;

View File

@@ -0,0 +1,32 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipInstanceServerCertificatesSchema = z.object({
id: z.string().uuid(),
commonName: z.string(),
altNames: z.string(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date(),
encryptedCertificate: zodBuffer,
encryptedChain: zodBuffer,
encryptedPrivateKey: zodBuffer
});
export type TKmipInstanceServerCertificates = z.infer<typeof KmipInstanceServerCertificatesSchema>;
export type TKmipInstanceServerCertificatesInsert = Omit<
z.input<typeof KmipInstanceServerCertificatesSchema>,
TImmutableDBKeys
>;
export type TKmipInstanceServerCertificatesUpdate = Partial<
Omit<z.input<typeof KmipInstanceServerCertificatesSchema>, TImmutableDBKeys>
>;

View File

@@ -133,7 +133,9 @@ export enum TableName {
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections",
SecretSync = "secret_syncs",
KmipClient = "kmip_clients"
KmipClient = "kmip_clients",
KmipInstanceConfig = "kmip_instance_configs",
KmipInstanceServerCertificates = "kmip_instance_server_certificates"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipInstanceConfigDALFactory = ReturnType<typeof kmipInstanceConfigDALFactory>;
export const kmipInstanceConfigDALFactory = (db: TDbClient) => {
const kmipInstanceConfigOrm = ormify(db, TableName.KmipInstanceConfig);
return {
...kmipInstanceConfigOrm
};
};

View File

@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipInstanceServerCertificateDALFactory = ReturnType<typeof kmipInstanceServerCertificateDALFactory>;
export const kmipInstanceServerCertificateDALFactory = (db: TDbClient) => {
const kmipInstanceServerCertificateOrm = ormify(db, TableName.KmipInstanceServerCertificates);
return {
...kmipInstanceServerCertificateOrm
};
};

View File

@@ -36,6 +36,8 @@ import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/i
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal";
import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal";
import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
@@ -383,6 +385,8 @@ export const registerRoutes = async (
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const kmipClientDAL = kmipClientDALFactory(db);
const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db);
const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@@ -623,7 +627,9 @@ export const registerRoutes = async (
orgService,
keyStore,
licenseService,
kmsService
kmsService,
kmipInstanceConfigDAL,
kmipInstanceServerCertificateDAL
});
const orgAdminService = orgAdminServiceFactory({

View File

@@ -1,3 +1,4 @@
import ms from "ms";
import { z } from "zod";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
@@ -7,6 +8,8 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators";
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
@@ -316,4 +319,88 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/kmip",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}),
response: {
200: z.object({
serverCertificateChain: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
return server.services.superAdmin.setupInstanceKmip({
...req.body
});
}
});
server.route({
method: "GET",
url: "/kmip",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
serverCertificateChain: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
return server.services.superAdmin.getInstanceKmip();
}
});
server.route({
method: "POST",
url: "/kmip/server-certificates",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
commonName: z.string().trim().min(1),
altNames: validateAltNamesField,
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
return server.services.superAdmin.generateInstanceKmipServerCertificate({
...req.body
});
}
});
};

View File

@@ -1,6 +1,12 @@
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import z from "zod";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TKmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal";
import { TKmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
@@ -10,6 +16,9 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
import { createSerialNumber, keyAlgorithmToAlgCfg } from "../certificate-authority/certificate-authority-fns";
import { hostnameRegex } from "../certificate-authority/certificate-authority-validators";
import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
@@ -19,7 +28,13 @@ import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import {
LoginMethod,
TAdminGetUsersDTO,
TAdminSignUpDTO,
TGenerateInstanceKmipServerCertificateDTO,
TSetupInstanceKmipDTO
} from "./super-admin-types";
type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory;
@@ -31,6 +46,8 @@ type TSuperAdminServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory;
kmipInstanceServerCertificateDAL: TKmipInstanceServerCertificateDALFactory;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -57,7 +74,9 @@ export const superAdminServiceFactory = ({
keyStore,
kmsRootConfigDAL,
kmsService,
licenseService
licenseService,
kmipInstanceConfigDAL,
kmipInstanceServerCertificateDAL
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -369,6 +388,302 @@ export const superAdminServiceFactory = ({
await kmsService.updateEncryptionStrategy(strategy);
};
const setupInstanceKmip = async ({ caKeyAlgorithm }: TSetupInstanceKmipDTO) => {
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
if (kmipInstanceConfig) {
throw new BadRequestError({
message: "KMIP has already been configured for the instance"
});
}
const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
// generate root CA
const rootCaSerialNumber = createSerialNumber();
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey);
const rootCaIssuedAt = new Date();
const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20));
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
name: "CN=KMIP Root CA",
serialNumber: rootCaSerialNumber,
notBefore: rootCaIssuedAt,
notAfter: rootCaExpiration,
signingAlgorithm: alg,
keys: rootCaKeys,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
]
});
// generate intermediate server CA
const serverIntermediateCaSerialNumber = createSerialNumber();
const serverIntermediateCaIssuedAt = new Date();
const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey);
const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: serverIntermediateCaSerialNumber,
subject: "CN=KMIP Server Intermediate CA",
issuer: rootCaCert.subject,
notBefore: serverIntermediateCaIssuedAt,
notAfter: serverIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: serverIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey)
]
});
// generate intermediate client CA
const clientIntermediateCaSerialNumber = createSerialNumber();
const clientIntermediateCaIssuedAt = new Date();
const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey);
const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: clientIntermediateCaSerialNumber,
subject: "CN=KMIP Client Intermediate CA",
issuer: rootCaCert.subject,
notBefore: clientIntermediateCaIssuedAt,
notAfter: clientIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: clientIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey)
]
});
const encryptWithRoot = kmsService.encryptWithRootKey();
await kmipInstanceConfigDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: ADMIN_CONFIG_DB_UUID,
caKeyAlgorithm,
rootCaIssuedAt,
rootCaExpiration,
rootCaSerialNumber,
encryptedRootCaCertificate: encryptWithRoot(Buffer.from(rootCaCert.rawData)),
encryptedRootCaPrivateKey: encryptWithRoot(
rootCaSkObj.export({
type: "pkcs8",
format: "der"
})
),
serverIntermediateCaIssuedAt,
serverIntermediateCaExpiration,
serverIntermediateCaSerialNumber,
encryptedServerIntermediateCaCertificate: encryptWithRoot(
Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData))
),
encryptedServerIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))),
encryptedServerIntermediateCaPrivateKey: encryptWithRoot(
serverIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
),
clientIntermediateCaIssuedAt,
clientIntermediateCaExpiration,
clientIntermediateCaSerialNumber,
encryptedClientIntermediateCaCertificate: encryptWithRoot(
Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData))
),
encryptedClientIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))),
encryptedClientIntermediateCaPrivateKey: encryptWithRoot(
clientIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
)
});
return {
// the order of the cert is intentional - for client chains, ordering should be from intermediate to root
serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim()
};
};
const getInstanceKmip = async () => {
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!kmipInstanceConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the instance"
});
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const rootCaCert = new x509.X509Certificate(decryptWithRoot(kmipInstanceConfig.encryptedRootCaCertificate));
const serverIntermediateCaCert = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate)
);
return {
// the order of the cert is intentional - for client chains, ordering should be from intermediate to root
serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim()
};
};
const generateInstanceKmipServerCertificate = async ({
ttl,
commonName,
altNames,
keyAlgorithm
}: TGenerateInstanceKmipServerCertificateDTO) => {
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!kmipInstanceConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the instance"
});
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const caCertObj = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate)
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true)
];
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm);
const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaChain).toString(
"utf-8"
);
const caSkObj = crypto.createPrivateKey({
key: decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaPrivateKey),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `CN=${commonName}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const encryptWithRoot = kmsService.encryptWithRootKey();
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const serverCert = await kmipInstanceServerCertificateDAL.create({
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber,
commonName,
altNames,
encryptedCertificate: encryptWithRoot(Buffer.from(new Uint8Array(leafCert.rawData))),
encryptedPrivateKey: encryptWithRoot(skLeafObj.export({ format: "der", type: "pkcs8" })),
encryptedChain: encryptWithRoot(Buffer.from(`${decryptedCaCertChain}\n${caCertObj.toString("pem")}`.trim()))
});
return serverCert;
};
return {
initServerCfg,
updateServerCfg,
@@ -377,6 +692,9 @@ export const superAdminServiceFactory = ({
deleteUser,
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies
getConfiguredEncryptionStrategies,
setupInstanceKmip,
getInstanceKmip,
generateInstanceKmipServerCertificate
};
};

View File

@@ -1,3 +1,5 @@
import { CertKeyAlgorithm } from "../certificate/certificate-types";
export type TAdminSignUpDTO = {
email: string;
password: string;
@@ -31,3 +33,14 @@ export enum LoginMethod {
LDAP = "ldap",
OIDC = "oidc"
}
export type TSetupInstanceKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
};
export type TGenerateInstanceKmipServerCertificateDTO = {
commonName: string;
altNames: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
};

View File

@@ -0,0 +1,6 @@
import FileSaver from "file-saver";
export const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};

View File

@@ -1,6 +1,7 @@
export {
useAdminDeleteUser,
useCreateAdminUser,
useSetupInstanceKmip,
useUpdateAdminSlackConfig,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy
@@ -8,6 +9,7 @@ export {
export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetInstanceKmipConfig,
useGetServerConfig,
useGetServerRootKmsEncryptionDetails
} from "./queries";

View File

@@ -10,6 +10,7 @@ import {
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TServerConfig,
TSetupInstanceKmipDTO,
TUpdateAdminSlackConfigDTO
} from "./types";
@@ -98,3 +99,15 @@ export const useUpdateServerEncryptionStrategy = () => {
}
});
};
export const useSetupInstanceKmip = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TSetupInstanceKmipDTO) => {
await apiRequest.post("/api/v1/admin/kmip", payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInstanceKmip() });
}
});
};

View File

@@ -6,6 +6,7 @@ import { User } from "../types";
import {
AdminGetUsersFilters,
AdminSlackConfig,
InstanceKmipConfig,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
@@ -18,7 +19,8 @@ export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInstanceKmip: () => ["instance-kmip"] as const
};
export const fetchServerConfig = async () => {
@@ -93,3 +95,14 @@ export const useGetServerRootKmsEncryptionDetails = () => {
}
});
};
export const useGetInstanceKmipConfig = () => {
return useQuery({
queryKey: adminQueryKeys.getInstanceKmip(),
queryFn: async () => {
const { data } = await apiRequest.get<InstanceKmipConfig>("/api/v1/admin/kmip");
return data;
}
});
};

View File

@@ -1,3 +1,5 @@
import { CertKeyAlgorithm } from "../certificates/enums";
export enum LoginMethod {
EMAIL = "email",
GOOGLE = "google",
@@ -62,7 +64,15 @@ export type TGetServerRootKmsEncryptionDetails = {
}[];
};
export type InstanceKmipConfig = {
serverCertificateChain: string;
};
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export type TSetupInstanceKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
};

View File

@@ -31,6 +31,7 @@ import {
import { AuthPanel } from "./components/AuthPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { KmipPanel } from "./components/KmipPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
@@ -40,7 +41,8 @@ enum TabSections {
Auth = "auth",
RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users"
Users = "users",
Kmip = "kmip"
}
enum SignUpModes {
@@ -149,6 +151,7 @@ export const OverviewPage = () => {
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>Users</Tab>
<Tab value={TabSections.Kmip}>KMIP</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
@@ -347,6 +350,9 @@ export const OverviewPage = () => {
<TabPanel value={TabSections.Users}>
<UserPanel />
</TabPanel>
<TabPanel value={TabSections.Kmip}>
<KmipPanel />
</TabPanel>
</Tabs>
</div>
)}

View File

@@ -0,0 +1,214 @@
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Modal,
ModalContent,
Select,
SelectItem,
Spinner,
TextArea,
Tooltip
} from "@app/components/v2";
import { downloadTxtFile } from "@app/helpers/download";
import { usePopUp, useTimedReset } from "@app/hooks";
import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api";
import { InstanceKmipConfig } from "@app/hooks/api/admin/types";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
const kmipInstanceConfigFormSchema = z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
});
type TKmipInstanceConfigForm = z.infer<typeof kmipInstanceConfigFormSchema>;
const KmipInstanceConfigSection = ({
kmipConfig,
isKmipConfigLoading
}: {
kmipConfig?: InstanceKmipConfig;
isKmipConfigLoading: boolean;
}) => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"configureKmip"
] as const);
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<TKmipInstanceConfigForm>({
resolver: zodResolver(kmipInstanceConfigFormSchema)
});
const { mutateAsync: setupInstanceKmip } = useSetupInstanceKmip();
const onFormSubmit = async (formData: TKmipInstanceConfigForm) => {
await setupInstanceKmip(formData);
createNotification({
type: "success",
text: "Successfully configured KMIP"
});
handlePopUpClose("configureKmip");
};
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
{
initialState: "Copy to clipboard"
}
);
return (
<>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">KMIP configuration</div>
{isKmipConfigLoading && (
<div className="mt-8 flex justify-center">
<Spinner />
</div>
)}
{!isKmipConfigLoading && kmipConfig && (
<div className="mt-2">
<div className="text-lg">KMIP CA Certificate for Clients</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
This certificate chain should be used by KMIP clients to verify the identity of the
KMIP servers and establish a secure TLS connection for encrypted communication.
</div>
<div className="flex max-w-2xl">
<div className="flex w-full justify-end">
<Tooltip content={copyTextCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(kmipConfig.serverCertificateChain);
setCopyTextCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("ca-chain.pem", kmipConfig.serverCertificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<TextArea
value={kmipConfig.serverCertificateChain}
reSize="none"
className="mt-2 h-48 max-w-2xl"
/>
</div>
)}
{!isKmipConfigLoading && !kmipConfig && (
<div className="mt-2">
<div>KMIP has not yet been configured for the instance.</div>
<Button
className="mt-2"
onClick={() => {
handlePopUpOpen("configureKmip");
}}
>
Setup KMIP
</Button>
</div>
)}
</div>
<Modal
isOpen={popUp.configureKmip.isOpen}
onOpenChange={(state) => handlePopUpToggle("configureKmip", state)}
>
<ModalContent title="Configure KMIP for the instance">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="caKeyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm used for generating the KMIP Root CA and Intermediate CAs, which sign all KMIP server and client certificates."
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-6 flex w-full gap-4">
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Continue
</Button>
<Button
className=""
size="sm"
variant="outline_bg"
type="button"
onClick={() => handlePopUpClose("configureKmip")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
</>
);
};
export const KmipServerConfigSection = () => {
return (
<div className="mt-8 flex flex-col justify-start">
<div className="text-lg">KMIP Server Certificates</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
These certificates should be used to configure TLS for the KMIP servers.
</div>
</div>
);
};
export const KmipPanel = () => {
const { data: kmipConfig, isPending } = useGetInstanceKmipConfig();
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<KmipInstanceConfigSection kmipConfig={kmipConfig} isKmipConfigLoading={isPending} />
{kmipConfig && <KmipServerConfigSection />}
</div>
);
};

View File

@@ -128,7 +128,7 @@ const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
name="permissions"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl label="Key Usage" errorText={error?.message} isError={Boolean(error)}>
<FormControl label="Permissions" errorText={error?.message} isError={Boolean(error)}>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KMIP_PERMISSIONS_OPTIONS.map(({ label, value: optionValue }) => {
return (