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,25 +82,27 @@ const KmipInstanceConfigSection = ({
</div> </div>
)} )}
{!isKmipConfigLoading && kmipConfig && ( {!isKmipConfigLoading && kmipConfig && (
<>
<div className="mt-2"> <div className="mt-2">
<div className="text-lg">KMIP CA Certificate for Clients</div> <div className="text-lg">Certificate Chain for KMIP Clients</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">
This certificate chain should be used by KMIP clients to verify the identity of the This certificate chain is used by KMIP clients to verify the identity of the KMIP
KMIP servers and establish a secure TLS connection for encrypted communication. server. It should be presented by the server during TLS authentication to establish
a secure and encrypted connection.
</div> </div>
<div className="flex max-w-2xl"> <div className="flex max-w-2xl">
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Tooltip content={copyTextCertificate}> <Tooltip content={copyTextClientCertificate}>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
colorSchema="secondary" colorSchema="secondary"
className="group relative" className="group relative"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(kmipConfig.serverCertificateChain); navigator.clipboard.writeText(kmipConfig.serverCertificateChain);
setCopyTextCertificate("Copied"); setCopyTextClientCertificate("Copied");
}} }}
> >
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} /> <FontAwesomeIcon icon={isCopyingClientCertificate ? faCheck : faCopy} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip content="Download"> <Tooltip content="Download">
@@ -110,13 +119,55 @@ const KmipInstanceConfigSection = ({
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<TextArea <TextArea
value={kmipConfig.serverCertificateChain} value={kmipConfig.serverCertificateChain}
reSize="none" reSize="none"
className="mt-2 h-48 max-w-2xl" className="mt-2 h-48 max-w-2xl"
/> />
</div> </div>
<div className="mt-8">
<div className="text-lg">Certificate Chain for KMIP Server</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
This certificate chain is used by the KMIP server to verify the identity of KMIP
clients. It should be configured on the server to establish trust in client
certificates during mutual TLS authentication.
</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>
); );
}; };