mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat: add support for no bootstrap cert EST
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasSkipBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||
TableName.CertificateTemplateEstConfig,
|
||||
"skipBootstrapCertValidation"
|
||||
);
|
||||
|
||||
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||
if (!hasSkipBootstrapCertValidationCol) {
|
||||
t.boolean("skipBootstrapCertValidation").defaultTo(false).notNullable();
|
||||
}
|
||||
|
||||
if (hasCaChainCol) {
|
||||
t.binary("encryptedCaChain").nullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSkipBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||
TableName.CertificateTemplateEstConfig,
|
||||
"skipBootstrapCertValidation"
|
||||
);
|
||||
|
||||
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||
if (hasSkipBootstrapCertValidationCol) {
|
||||
t.dropColumn("skipBootstrapCertValidation");
|
||||
}
|
||||
|
||||
if (hasCaChainCol) {
|
||||
t.binary("encryptedCaChain").notNullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
encryptedCaChain: zodBuffer,
|
||||
encryptedCaChain: zodBuffer.nullable().optional(),
|
||||
hashedPassphrase: z.string(),
|
||||
isEnabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
skipBootstrapCertValidation: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||
|
||||
@@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
if (!estConfig.skipBootstrapCertValidation) {
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
|
||||
@@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
skipBootstrapCertValidation: true
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true),
|
||||
skipBootstrapCertValidation: z.boolean().default(false)
|
||||
})
|
||||
.refine(
|
||||
({ caChain, skipBootstrapCertValidation }) =>
|
||||
skipBootstrapCertValidation || (!skipBootstrapCertValidation && caChain),
|
||||
"CA chain is required"
|
||||
),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
@@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1).optional(),
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
skipBootstrapCertValidation: z.boolean().optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
|
||||
@@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
skipBootstrapCertValidation
|
||||
}: TCreateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
@@ -266,39 +267,45 @@ export const certificateTemplateServiceFactory = ({
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
let encryptedCaChain: Buffer | undefined;
|
||||
if (caChain) {
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
encryptedCaChain = cipherTextBlob;
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||
certificateTemplateId,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain,
|
||||
isEnabled
|
||||
isEnabled,
|
||||
skipBootstrapCertValidation
|
||||
});
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
@@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
skipBootstrapCertValidation
|
||||
}: TUpdateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
@@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
|
||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||
isEnabled
|
||||
isEnabled,
|
||||
skipBootstrapCertValidation
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
@@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaChain = await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
});
|
||||
let decryptedCaChain = "";
|
||||
if (estConfig.encryptedCaChain) {
|
||||
decryptedCaChain = (
|
||||
await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
})
|
||||
).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
certificateTemplateId,
|
||||
id: estConfig.id,
|
||||
isEnabled: estConfig.isEnabled,
|
||||
caChain: decryptedCaChain.toString(),
|
||||
caChain: decryptedCaChain,
|
||||
hashedPassphrase: estConfig.hashedPassphrase,
|
||||
projectId: certTemplate.projectId,
|
||||
orgId: certTemplate.orgId
|
||||
orgId: certTemplate.orgId,
|
||||
skipBootstrapCertValidation: estConfig.skipBootstrapCertValidation
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
|
||||
|
||||
export type TCreateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
caChain?: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
skipBootstrapCertValidation: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateEstConfigurationDTO = {
|
||||
@@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
skipBootstrapCertValidation?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetEstConfigurationDTO =
|
||||
|
||||
@@ -46,9 +46,10 @@ export type TDeleteCertificateTemplateDTO = {
|
||||
|
||||
export type TCreateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
caChain?: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
skipBootstrapCertValidation: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateEstConfigDTO = {
|
||||
@@ -56,11 +57,13 @@ export type TUpdateEstConfigDTO = {
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
skipBootstrapCertValidation?: boolean;
|
||||
};
|
||||
|
||||
export type TEstConfig = {
|
||||
id: string;
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
isEnabled: false;
|
||||
isEnabled: boolean;
|
||||
skipBootstrapCertValidation: boolean;
|
||||
};
|
||||
|
||||
@@ -33,9 +33,10 @@ type Props = {
|
||||
|
||||
const schema = z.object({
|
||||
method: z.nativeEnum(EnrollmentMethod),
|
||||
caChain: z.string(),
|
||||
caChain: z.string().optional(),
|
||||
passphrase: z.string().optional(),
|
||||
isEnabled: z.boolean()
|
||||
isEnabled: z.boolean(),
|
||||
skipBootstrapCertValidation: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
@@ -53,6 +54,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
@@ -62,16 +65,26 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
const { mutateAsync: updateEstConfig } = useUpdateEstConfig();
|
||||
const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false);
|
||||
|
||||
const skipBootstrapCertValidation = watch("skipBootstrapCertValidation");
|
||||
|
||||
useEffect(() => {
|
||||
if (skipBootstrapCertValidation) {
|
||||
setValue("caChain", "");
|
||||
}
|
||||
}, [skipBootstrapCertValidation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
caChain: data.caChain,
|
||||
isEnabled: data.isEnabled
|
||||
isEnabled: data.isEnabled,
|
||||
skipBootstrapCertValidation: data.skipBootstrapCertValidation
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caChain: "",
|
||||
isEnabled: false
|
||||
isEnabled: false,
|
||||
skipBootstrapCertValidation: false
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -83,7 +96,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled
|
||||
isEnabled,
|
||||
skipBootstrapCertValidation
|
||||
});
|
||||
} else {
|
||||
if (!passphrase) {
|
||||
@@ -95,7 +109,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled
|
||||
isEnabled,
|
||||
skipBootstrapCertValidation
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,22 +167,43 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="caChain"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Authority Chain"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
|
||||
reSize="none"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
name="skipBootstrapCertValidation"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
id="skip-bootstrap-cert-validation"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="ml-1 w-full">Skip Bootstrap Certificate Validation</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!skipBootstrapCertValidation && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="caChain"
|
||||
disabled={skipBootstrapCertValidation}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Authority Chain"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired={!skipBootstrapCertValidation}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
isDisabled={skipBootstrapCertValidation}
|
||||
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
|
||||
reSize="none"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="passphrase"
|
||||
|
||||
Reference in New Issue
Block a user