feat: completed kmip server cert config

This commit is contained in:
Sheen Capadngan
2025-02-06 22:36:31 +08:00
parent 9f9ded5102
commit 0269f57768
9 changed files with 320 additions and 70 deletions
@@ -61,7 +61,6 @@ export async function up(knex: Knex): Promise<void> {
t.datetime("expiration").notNullable(); t.datetime("expiration").notNullable();
t.binary("encryptedCertificate").notNullable(); t.binary("encryptedCertificate").notNullable();
t.binary("encryptedChain").notNullable(); t.binary("encryptedChain").notNullable();
t.binary("encryptedPrivateKey").notNullable();
}); });
} }
} }
@@ -18,8 +18,7 @@ export const KmipInstanceServerCertificatesSchema = z.object({
issuedAt: z.date(), issuedAt: z.date(),
expiration: z.date(), expiration: z.date(),
encryptedCertificate: zodBuffer, encryptedCertificate: zodBuffer,
encryptedChain: zodBuffer, encryptedChain: zodBuffer
encryptedPrivateKey: zodBuffer
}); });
export type TKmipInstanceServerCertificates = z.infer<typeof KmipInstanceServerCertificatesSchema>; export type TKmipInstanceServerCertificates = z.infer<typeof KmipInstanceServerCertificatesSchema>;
+4
View File
@@ -103,6 +103,10 @@ export const isValidIpOrCidr = (ip: string): boolean => {
return false; return false;
}; };
export const isValidIp = (ip: string) => {
return net.isIPv4(ip) || net.isIPv6(ip);
};
export type TIp = { export type TIp = {
ipAddress: string; ipAddress: string;
type: IPType; type: IPType;
+5 -2
View File
@@ -332,7 +332,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
serverCertificateChain: z.string() serverCertificateChain: z.string(),
clientCertificateChain: z.string()
}) })
} }
}, },
@@ -357,7 +358,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
serverCertificateChain: z.string() serverCertificateChain: z.string(),
clientCertificateChain: z.string()
}) })
} }
}, },
@@ -386,6 +388,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
serialNumber: z.string(),
certificateChain: z.string(), certificateChain: z.string(),
certificate: z.string(), certificate: z.string(),
privateKey: z.string() privateKey: z.string()
@@ -1,5 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
const isValidDate = (dateString: string) => { const isValidDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
return !Number.isNaN(date.getTime()); return !Number.isNaN(date.getTime());
@@ -25,7 +27,7 @@ export const validateAltNamesField = z
if (data === "") return true; if (data === "") return true;
// Split and validate each alt name // Split and validate each alt name
return data.split(", ").every((name) => { return data.split(", ").every((name) => {
return hostnameRegex.test(name) || z.string().email().safeParse(name).success; return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
}); });
}, },
{ {
@@ -2,7 +2,6 @@ import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import crypto, { KeyObject } from "crypto"; import crypto, { KeyObject } from "crypto";
import ms from "ms"; import ms from "ms";
import z from "zod";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TKmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; import { TKmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal";
@@ -13,6 +12,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip";
import { TAuthLoginFactory } from "../auth/auth-login-service"; import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
@@ -526,8 +526,8 @@ export const superAdminServiceFactory = ({
}); });
return { 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(),
serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim()
}; };
}; };
@@ -544,10 +544,13 @@ export const superAdminServiceFactory = ({
const serverIntermediateCaCert = new x509.X509Certificate( const serverIntermediateCaCert = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate) decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate)
); );
const clientIntermediateCaCert = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate)
);
return { 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(),
serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim()
}; };
}; };
@@ -604,20 +607,12 @@ export const superAdminServiceFactory = ({
]; ];
const altNamesArray: { const altNamesArray: {
type: "email" | "dns"; type: "email" | "dns" | "ip";
value: string; value: string;
}[] = altNames }[] = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .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 // check if the altName is a valid hostname
if (hostnameRegex.test(altName)) { if (hostnameRegex.test(altName)) {
return { return {
@@ -626,7 +621,14 @@ export const superAdminServiceFactory = ({
}; };
} }
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly // check if the altName is a valid IP
if (isValidIp(altName)) {
return {
type: "ip",
value: altName
};
}
throw new Error(`Invalid altName: ${altName}`); throw new Error(`Invalid altName: ${altName}`);
}); });
@@ -668,8 +670,9 @@ export const superAdminServiceFactory = ({
const encryptWithRoot = kmsService.encryptWithRootKey(); const encryptWithRoot = kmsService.encryptWithRootKey();
const skLeafObj = KeyObject.from(leafKeys.privateKey); const skLeafObj = KeyObject.from(leafKeys.privateKey);
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
const serverCert = await kmipInstanceServerCertificateDAL.create({ await kmipInstanceServerCertificateDAL.create({
keyAlgorithm, keyAlgorithm,
issuedAt: notBeforeDate, issuedAt: notBeforeDate,
expiration: notAfterDate, expiration: notAfterDate,
@@ -677,11 +680,15 @@ export const superAdminServiceFactory = ({
commonName, commonName,
altNames, altNames,
encryptedCertificate: encryptWithRoot(Buffer.from(new Uint8Array(leafCert.rawData))), encryptedCertificate: encryptWithRoot(Buffer.from(new Uint8Array(leafCert.rawData))),
encryptedPrivateKey: encryptWithRoot(skLeafObj.export({ format: "der", type: "pkcs8" })), encryptedChain: encryptWithRoot(Buffer.from(certificateChain))
encryptedChain: encryptWithRoot(Buffer.from(`${decryptedCaCertChain}\n${caCertObj.toString("pem")}`.trim()))
}); });
return serverCert; return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain
};
}; };
return { return {
+13
View File
@@ -7,8 +7,10 @@ import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import { import {
AdminSlackConfig, AdminSlackConfig,
InstanceKmipServerCert,
RootKeyEncryptionStrategy, RootKeyEncryptionStrategy,
TCreateAdminUserDTO, TCreateAdminUserDTO,
TGenerateInstanceKmipServerCertDTO,
TServerConfig, TServerConfig,
TSetupInstanceKmipDTO, TSetupInstanceKmipDTO,
TUpdateAdminSlackConfigDTO TUpdateAdminSlackConfigDTO
@@ -111,3 +113,14 @@ export const useSetupInstanceKmip = () => {
} }
}); });
}; };
export const useGenerateInstanceKmipServerCert = () => {
return useMutation({
mutationFn: async (payload: TGenerateInstanceKmipServerCertDTO) => {
return apiRequest.post<InstanceKmipServerCert>(
"/api/v1/admin/kmip/server-certificates",
payload
);
}
});
};
+15
View File
@@ -66,6 +66,7 @@ export type TGetServerRootKmsEncryptionDetails = {
export type InstanceKmipConfig = { export type InstanceKmipConfig = {
serverCertificateChain: string; serverCertificateChain: string;
clientCertificateChain: string;
}; };
export enum RootKeyEncryptionStrategy { export enum RootKeyEncryptionStrategy {
@@ -76,3 +77,17 @@ export enum RootKeyEncryptionStrategy {
export type TSetupInstanceKmipDTO = { export type TSetupInstanceKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm; caKeyAlgorithm: CertKeyAlgorithm;
}; };
export type TGenerateInstanceKmipServerCertDTO = {
commonName: string;
keyAlgorithm: CertKeyAlgorithm;
altNames: string;
ttl: string;
};
export type InstanceKmipServerCert = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
@@ -9,6 +9,7 @@ import {
Button, Button,
FormControl, FormControl,
IconButton, IconButton,
Input,
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
@@ -20,9 +21,11 @@ import {
import { downloadTxtFile } from "@app/helpers/download"; import { downloadTxtFile } from "@app/helpers/download";
import { usePopUp, useTimedReset } from "@app/hooks"; import { usePopUp, useTimedReset } from "@app/hooks";
import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api"; import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api";
import { useGenerateInstanceKmipServerCert } from "@app/hooks/api/admin/mutation";
import { InstanceKmipConfig } from "@app/hooks/api/admin/types"; import { InstanceKmipConfig } from "@app/hooks/api/admin/types";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
const kmipInstanceConfigFormSchema = z.object({ const kmipInstanceConfigFormSchema = z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
@@ -59,11 +62,15 @@ const KmipInstanceConfigSection = ({
handlePopUpClose("configureKmip"); handlePopUpClose("configureKmip");
}; };
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>( const [copyTextClientCertificate, isCopyingClientCertificate, setCopyTextClientCertificate] =
{ useTimedReset<string>({
initialState: "Copy to clipboard" initialState: "Copy to clipboard"
} });
);
const [copyTextServerCertificate, isCopyingServerCertificate, setCopyTextServerCertificate] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
return ( return (
<> <>
@@ -75,48 +82,92 @@ const KmipInstanceConfigSection = ({
</div> </div>
)} )}
{!isKmipConfigLoading && kmipConfig && ( {!isKmipConfigLoading && kmipConfig && (
<div className="mt-2"> <>
<div className="text-lg">KMIP CA Certificate for Clients</div> <div className="mt-2">
<div className="mt-2 max-w-lg text-sm text-mineshaft-400"> <div className="text-lg">Certificate Chain for KMIP Clients</div>
This certificate chain should be used by KMIP clients to verify the identity of the <div className="mt-2 max-w-lg text-sm text-mineshaft-400">
KMIP servers and establish a secure TLS connection for encrypted communication. This certificate chain is used by KMIP clients to verify the identity of the KMIP
</div> server. It should be presented by the server during TLS authentication to establish
<div className="flex max-w-2xl"> a secure and encrypted connection.
<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>
<div className="flex max-w-2xl">
<div className="flex w-full justify-end">
<Tooltip content={copyTextClientCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(kmipConfig.serverCertificateChain);
setCopyTextClientCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingClientCertificate ? 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> </div>
<div className="mt-8">
<TextArea <div className="text-lg">Certificate Chain for KMIP Server</div>
value={kmipConfig.serverCertificateChain} <div className="mt-2 max-w-lg text-sm text-mineshaft-400">
reSize="none" This certificate chain is used by the KMIP server to verify the identity of KMIP
className="mt-2 h-48 max-w-2xl" clients. It should be configured on the server to establish trust in client
/> certificates during mutual TLS authentication.
</div> </div>
<div className="flex max-w-2xl">
<div className="flex w-full justify-end">
<Tooltip content={copyTextServerCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(kmipConfig.clientCertificateChain);
setCopyTextServerCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingServerCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("ca-chain.pem", kmipConfig.clientCertificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<TextArea
value={kmipConfig.clientCertificateChain}
reSize="none"
className="mt-2 h-48 max-w-2xl"
/>
</div>
</>
)} )}
{!isKmipConfigLoading && !kmipConfig && ( {!isKmipConfigLoading && !kmipConfig && (
<div className="mt-2"> <div className="mt-2">
@@ -191,13 +242,170 @@ const KmipInstanceConfigSection = ({
); );
}; };
const kmipInstanceServerCertFormSchema = z.object({
commonName: z.string(),
altNames: z.string(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string()
});
type TKmipInstanceServerCertForm = z.infer<typeof kmipInstanceServerCertFormSchema>;
export const KmipServerConfigSection = () => { export const KmipServerConfigSection = () => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"configureKmipServerCert",
"showCertificate"
] as const);
const certificateData = popUp.showCertificate?.data as {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<TKmipInstanceServerCertForm>({
resolver: zodResolver(kmipInstanceServerCertFormSchema)
});
const { mutateAsync: generateKmipServerCert } = useGenerateInstanceKmipServerCert();
const onFormSubmit = async (formData: TKmipInstanceServerCertForm) => {
const { data: certificate } = await generateKmipServerCert(formData);
handlePopUpOpen("showCertificate", certificate);
createNotification({
type: "success",
text: "Successfully created KMIP server certificate"
});
handlePopUpClose("configureKmipServerCert");
};
return ( return (
<div className="mt-8 flex flex-col justify-start"> <div className="mt-8 flex flex-col justify-start">
<div className="text-lg">KMIP Server Certificates</div> <div className="text-lg">KMIP Server Certificate</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400"> <div className="mt-2 max-w-lg text-sm text-mineshaft-400">
These certificates should be used to configure TLS for the KMIP servers. These certificates should be used to configure TLS for the KMIP servers.
</div> </div>
<Button
className="mt-2 w-fit"
onClick={() => {
handlePopUpOpen("configureKmipServerCert");
}}
>
Generate KMIP server certificate
</Button>
<Modal
isOpen={popUp.configureKmipServerCert.isOpen}
onOpenChange={(state) => handlePopUpToggle("configureKmipServerCert", state)}
>
<ModalContent title="Configure KMIP for the instance">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="service.acme.com" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="altNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Alternative Names (SANs)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="app1.acme.com, app2.acme.com, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm to use for signing the server certificate."
>
<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("configureKmipServerCert")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<Modal
isOpen={popUp.showCertificate.isOpen}
onOpenChange={(state) => handlePopUpToggle("showCertificate", state)}
>
<ModalContent title="Configure KMIP for the instance">
<CertificateContent {...certificateData} />
</ModalContent>
</Modal>
</div> </div>
); );
}; };