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

View File

@@ -61,7 +61,6 @@ export async function up(knex: Knex): Promise<void> {
t.datetime("expiration").notNullable();
t.binary("encryptedCertificate").notNullable();
t.binary("encryptedChain").notNullable();
t.binary("encryptedPrivateKey").notNullable();
});
}
}

View File

@@ -18,8 +18,7 @@ export const KmipInstanceServerCertificatesSchema = z.object({
issuedAt: z.date(),
expiration: z.date(),
encryptedCertificate: zodBuffer,
encryptedChain: zodBuffer,
encryptedPrivateKey: zodBuffer
encryptedChain: zodBuffer
});
export type TKmipInstanceServerCertificates = z.infer<typeof KmipInstanceServerCertificatesSchema>;

View File

@@ -103,6 +103,10 @@ export const isValidIpOrCidr = (ip: string): boolean => {
return false;
};
export const isValidIp = (ip: string) => {
return net.isIPv4(ip) || net.isIPv6(ip);
};
export type TIp = {
ipAddress: string;
type: IPType;

View File

@@ -332,7 +332,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
serverCertificateChain: z.string()
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
@@ -357,7 +358,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
serverCertificateChain: z.string()
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
@@ -386,6 +388,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
serialNumber: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()

View File

@@ -1,5 +1,7 @@
import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
@@ -25,7 +27,7 @@ export const validateAltNamesField = z
if (data === "") return true;
// Split and validate each alt 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);
});
},
{

View File

@@ -2,7 +2,6 @@ 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";
@@ -13,6 +12,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
@@ -526,8 +526,8 @@ export const superAdminServiceFactory = ({
});
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(
decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate)
);
const clientIntermediateCaCert = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate)
);
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: {
type: "email" | "dns";
type: "email" | "dns" | "ip";
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 {
@@ -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}`);
});
@@ -668,8 +670,9 @@ export const superAdminServiceFactory = ({
const encryptWithRoot = kmsService.encryptWithRootKey();
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
const serverCert = await kmipInstanceServerCertificateDAL.create({
await kmipInstanceServerCertificateDAL.create({
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
@@ -677,11 +680,15 @@ export const superAdminServiceFactory = ({
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()))
encryptedChain: encryptWithRoot(Buffer.from(certificateChain))
});
return serverCert;
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain
};
};
return {

View File

@@ -7,8 +7,10 @@ import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import {
AdminSlackConfig,
InstanceKmipServerCert,
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TGenerateInstanceKmipServerCertDTO,
TServerConfig,
TSetupInstanceKmipDTO,
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
);
}
});
};

View File

@@ -66,6 +66,7 @@ export type TGetServerRootKmsEncryptionDetails = {
export type InstanceKmipConfig = {
serverCertificateChain: string;
clientCertificateChain: string;
};
export enum RootKeyEncryptionStrategy {
@@ -76,3 +77,17 @@ export enum RootKeyEncryptionStrategy {
export type TSetupInstanceKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
};
export type TGenerateInstanceKmipServerCertDTO = {
commonName: string;
keyAlgorithm: CertKeyAlgorithm;
altNames: string;
ttl: string;
};
export type InstanceKmipServerCert = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};

View File

@@ -9,6 +9,7 @@ import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Select,
@@ -20,9 +21,11 @@ import {
import { downloadTxtFile } from "@app/helpers/download";
import { usePopUp, useTimedReset } from "@app/hooks";
import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api";
import { useGenerateInstanceKmipServerCert } from "@app/hooks/api/admin/mutation";
import { InstanceKmipConfig } from "@app/hooks/api/admin/types";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
const kmipInstanceConfigFormSchema = z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
@@ -59,11 +62,15 @@ const KmipInstanceConfigSection = ({
handlePopUpClose("configureKmip");
};
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
{
const [copyTextClientCertificate, isCopyingClientCertificate, setCopyTextClientCertificate] =
useTimedReset<string>({
initialState: "Copy to clipboard"
}
);
});
const [copyTextServerCertificate, isCopyingServerCertificate, setCopyTextServerCertificate] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
return (
<>
@@ -75,48 +82,92 @@ const KmipInstanceConfigSection = ({
</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 className="mt-2">
<div className="text-lg">Certificate Chain for KMIP Clients</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
This certificate chain is used by KMIP clients to verify the identity of the KMIP
server. It should be presented by the server during TLS authentication to establish
a secure and encrypted connection.
</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>
<TextArea
value={kmipConfig.serverCertificateChain}
reSize="none"
className="mt-2 h-48 max-w-2xl"
/>
</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 && (
<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 = () => {
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 (
<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">
These certificates should be used to configure TLS for the KMIP servers.
</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>
);
};