Merge pull request #4925 from Infisical/feat/PKI-28

Add self-sign certs support
This commit is contained in:
carlosmonastyrski
2025-11-24 12:38:20 -03:00
committed by GitHub
22 changed files with 1407 additions and 248 deletions

View File

@@ -10,4 +10,4 @@ export {
useGetProfileCertificates,
useListCertificateProfiles
} from "./queries";
export type * from "./types";
export * from "./types";

View File

@@ -1,11 +1,23 @@
export enum EnrollmentType {
API = "api",
EST = "est",
ACME = "acme"
}
export enum IssuerType {
CA = "ca",
SELF_SIGNED = "self-signed"
}
export type TCertificateProfile = {
id: string;
projectId: string;
caId: string;
caId: string | null;
certificateTemplateId: string;
slug: string;
description?: string;
enrollmentType: "api" | "est" | "acme";
enrollmentType: EnrollmentType;
issuerType: IssuerType;
estConfigId?: string;
apiConfigId?: string;
createdAt: string;
@@ -44,11 +56,12 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
export type TCreateCertificateProfileDTO = {
projectId: string;
caId: string;
caId?: string;
certificateTemplateId: string;
slug: string;
description?: string;
enrollmentType: "api" | "est" | "acme";
enrollmentType: EnrollmentType;
issuerType: IssuerType;
estConfig?: {
disableBootstrapCaValidation?: boolean;
passphrase: string;
@@ -65,6 +78,8 @@ export type TUpdateCertificateProfileDTO = {
profileId: string;
slug?: string;
description?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
estConfig?: {
disableBootstrapCaValidation?: boolean;
passphrase?: string;
@@ -87,7 +102,9 @@ export type TListCertificateProfilesDTO = {
offset?: number;
search?: string;
includeConfigs?: boolean;
enrollmentType?: "api" | "est" | "acme";
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
};
export type TGetCertificateProfileByIdDTO = {

View File

@@ -21,7 +21,7 @@ import {
import { useProject } from "@app/context";
import { useGetCert } from "@app/hooks/api";
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -122,7 +122,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
enrollmentType: "api"
enrollmentType: EnrollmentType.API
});
const { mutateAsync: createCertificate } = useCreateCertificateV3({

View File

@@ -416,7 +416,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
{(() => {
const canRenew =
certificate.profileId &&
(certificate.profileId || certificate.caId) &&
certificate.hasPrivateKey !== false &&
!certificate.renewedByCertificateId &&
!isRevoked &&

View File

@@ -21,6 +21,8 @@ import {
import { useProject, useSubscription } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import {
EnrollmentType,
IssuerType,
TCertificateProfileWithDetails,
TCreateCertificateProfileDTO,
TUpdateCertificateProfileDTO,
@@ -46,8 +48,9 @@ const createSchema = z
.trim()
.max(1000, "Description must be less than 1000 characters")
.optional(),
enrollmentType: z.enum(["api", "est", "acme"]),
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType),
certificateAuthorityId: z.string().nullable().optional(),
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
estConfig: z
.object({
@@ -78,19 +81,101 @@ const createSchema = z
})
.refine(
(data) => {
if (data.enrollmentType === "est" && !data.estConfig) {
return false;
}
if (data.enrollmentType === "api" && !data.apiConfig) {
return false;
}
if (data.enrollmentType === "acme" && !data.acmeConfig) {
return false;
if (data.enrollmentType === EnrollmentType.EST) {
return !!data.estConfig;
}
return true;
},
{
message: "Configuration is required for selected enrollment type"
message: "EST enrollment type requires EST configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.certificateAuthorityId;
}
return true;
},
{
message: "CA issuer type requires a certificate authority"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.certificateAuthorityId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a certificate authority"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -110,8 +195,9 @@ const editSchema = z
.trim()
.max(1000, "Description must be less than 1000 characters")
.optional(),
enrollmentType: z.enum(["api", "est", "acme"]),
certificateAuthorityId: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType),
certificateAuthorityId: z.string().nullable().optional(),
certificateTemplateId: z.string().optional(),
estConfig: z
.object({
@@ -130,19 +216,101 @@ const editSchema = z
})
.refine(
(data) => {
if (data.enrollmentType === "est" && !data.estConfig) {
return false;
}
if (data.enrollmentType === "api" && !data.apiConfig) {
return false;
}
if (data.enrollmentType === "acme" && !data.acmeConfig) {
return false;
if (data.enrollmentType === EnrollmentType.EST) {
return !!data.estConfig;
}
return true;
},
{
message: "Configuration is required for selected enrollment type"
message: "EST enrollment type requires EST configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.certificateAuthorityId;
}
return true;
},
{
message: "CA issuer type requires a certificate authority"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.certificateAuthorityId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a certificate authority"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -193,10 +361,11 @@ export const CreateProfileModal = ({
slug: profile.slug,
description: profile.description || "",
enrollmentType: profile.enrollmentType,
certificateAuthorityId: profile.caId,
issuerType: profile.issuerType,
certificateAuthorityId: profile.caId || undefined,
certificateTemplateId: profile.certificateTemplateId,
estConfig:
profile.enrollmentType === "est"
profile.enrollmentType === EnrollmentType.EST
? {
disableBootstrapCaValidation:
profile.estConfig?.disableBootstrapCaValidation || false,
@@ -205,18 +374,19 @@ export const CreateProfileModal = ({
}
: undefined,
apiConfig:
profile.enrollmentType === "api"
profile.enrollmentType === EnrollmentType.API
? {
autoRenew: profile.apiConfig?.autoRenew || false,
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
}
: {
slug: "",
description: "",
enrollmentType: "api",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
certificateAuthorityId: "",
certificateTemplateId: "",
apiConfig: {
@@ -228,6 +398,7 @@ export const CreateProfileModal = ({
});
const watchedEnrollmentType = watch("enrollmentType");
const watchedIssuerType = watch("issuerType");
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
const watchedAutoRenew = watch("apiConfig.autoRenew");
@@ -237,7 +408,8 @@ export const CreateProfileModal = ({
slug: profile.slug,
description: profile.description || "",
enrollmentType: profile.enrollmentType,
certificateAuthorityId: profile.caId,
issuerType: profile.issuerType,
certificateAuthorityId: profile.caId || undefined,
certificateTemplateId: profile.certificateTemplateId,
estConfig:
profile.enrollmentType === "est"
@@ -255,13 +427,13 @@ export const CreateProfileModal = ({
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
});
}
}, [isEdit, profile, reset]);
const onFormSubmit = async (data: FormData) => {
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === "acme") {
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
reset();
onClose();
handlePopUpOpen("upgradePlan", {
@@ -276,14 +448,15 @@ export const CreateProfileModal = ({
const updateData: TUpdateCertificateProfileDTO = {
profileId: profile.id,
slug: data.slug,
description: data.description
description: data.description,
issuerType: data.issuerType
};
if (data.enrollmentType === "est" && data.estConfig) {
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
updateData.estConfig = data.estConfig;
} else if (data.enrollmentType === "api" && data.apiConfig) {
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
updateData.apiConfig = data.apiConfig;
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
updateData.acmeConfig = data.acmeConfig;
}
@@ -298,19 +471,23 @@ export const CreateProfileModal = ({
slug: data.slug,
description: data.description,
enrollmentType: data.enrollmentType,
caId: data.certificateAuthorityId,
issuerType: data.issuerType,
caId:
data.issuerType === IssuerType.SELF_SIGNED
? undefined
: data.certificateAuthorityId || undefined,
certificateTemplateId: data.certificateTemplateId
};
if (data.enrollmentType === "est" && data.estConfig) {
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
createData.estConfig = {
passphrase: data.estConfig.passphrase,
caChain: data.estConfig.caChain || undefined,
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
};
} else if (data.enrollmentType === "api" && data.apiConfig) {
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
createData.apiConfig = data.apiConfig;
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
createData.acmeConfig = data.acmeConfig;
}
@@ -372,34 +549,73 @@ export const CreateProfileModal = ({
<Controller
control={control}
name="certificateAuthorityId"
name="issuerType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
label="Issuer Type"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
onValueChange={onChange}
placeholder="Select a certificate authority"
onValueChange={(value) => {
if (value === "self-signed") {
setValue("certificateAuthorityId", "");
setValue("enrollmentType", EnrollmentType.API);
setValue("apiConfig", {
autoRenew: false,
renewBeforeDays: 30
});
setValue("estConfig", undefined);
setValue("acmeConfig", undefined);
}
onChange(value);
}}
className="w-full"
position="popper"
isDisabled={Boolean(isEdit)}
>
{certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}>
{ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem>
))}
<SelectItem value="ca">Certificate Authority</SelectItem>
<SelectItem value="self-signed">Self-signed</SelectItem>
</Select>
</FormControl>
)}
/>
{watchedIssuerType === "ca" && (
<Controller
control={control}
name="certificateAuthorityId"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
value={value || undefined}
onValueChange={onChange}
placeholder="Select a certificate authority"
className="w-full"
position="popper"
isDisabled={Boolean(isEdit)}
>
{certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}>
{ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="certificateTemplateId"
@@ -488,8 +704,12 @@ export const CreateProfileModal = ({
isDisabled={Boolean(isEdit)}
>
<SelectItem value="api">API</SelectItem>
<SelectItem value="est">EST</SelectItem>
<SelectItem value="acme">ACME</SelectItem>
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
<SelectItem value="est">EST</SelectItem>
)}
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
<SelectItem value="acme">ACME</SelectItem>
)}
</Select>
</FormControl>
)}

View File

@@ -54,7 +54,7 @@ export const ProfileList = ({
</THead>
<TBody>
<Tr>
<Td colSpan={6}>
<Td colSpan={5}>
<EmptyState title="No Project Selected" />
</Td>
</Tr>
@@ -77,10 +77,10 @@ export const ProfileList = ({
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={6} innerKey="certificate-profiles" />}
{isLoading && <TableSkeleton columns={5} innerKey="certificate-profiles" />}
{!isLoading && (!profiles || profiles.length === 0) && (
<Tr>
<Td colSpan={6}>
<Td colSpan={5}>
<EmptyState title="No Certificate Profiles" />
</Td>
</Tr>

View File

@@ -30,7 +30,7 @@ import {
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetCaById } from "@app/hooks/api/ca/queries";
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { IssuerType, TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
@@ -49,7 +49,7 @@ export const ProfileRow = ({
}: Props) => {
const { permission } = useProjectPermission();
const { data: caData } = useGetCaById(profile.caId);
const { data: caData } = useGetCaById(profile.caId ?? "");
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
@@ -121,7 +121,9 @@ export const ProfileRow = ({
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-start">
<span className="text-sm text-mineshaft-300">
{caData?.friendlyName || caData?.commonName || profile.caId}
{profile.issuerType === IssuerType.SELF_SIGNED
? "Self-signed"
: caData?.friendlyName || caData?.commonName || profile.caId}
</span>
</Td>
<Td>