Improvements on Azure ADCS PKI feature

This commit is contained in:
Carlos Monastyrski
2025-08-27 21:20:10 -03:00
parent 0762de93d6
commit 13b20806ba
19 changed files with 922 additions and 600 deletions
+2 -27
View File
@@ -1,15 +1,7 @@
import { AppConnection } from "../appConnections/enums";
import { SshCaStatus } from "../sshCa";
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
import {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaCapability,
CaStatus,
CaType,
InternalCaType
} from "./enums";
import { AcmeDnsProvider, CaCapability, CaStatus, CaType, InternalCaType } from "./enums";
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
[InternalCaType.ROOT]: "Root",
@@ -32,19 +24,6 @@ export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppCo
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
};
export const AZURE_AD_CS_TEMPLATE_NAME_MAP: Record<AzureAdCsTemplateType, string> = {
[AzureAdCsTemplateType.WEB_SERVER]: "Web Server",
[AzureAdCsTemplateType.COMPUTER]: "Computer",
[AzureAdCsTemplateType.USER]: "User",
[AzureAdCsTemplateType.DOMAIN_CONTROLLER]: "Domain Controller",
[AzureAdCsTemplateType.SUBORDINATE_CA]: "Subordinate CA"
};
export const AZURE_AD_CS_AUTH_METHOD_NAME_MAP: Record<AzureAdCsAuthMethod, string> = {
[AzureAdCsAuthMethod.CLIENT_CERTIFICATE]: "Client Certificate",
[AzureAdCsAuthMethod.KERBEROS]: "Kerberos"
};
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
[CaType.INTERNAL]: [
CaCapability.ISSUE_CERTIFICATES,
@@ -56,11 +35,7 @@ export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
CaCapability.REVOKE_CERTIFICATES,
CaCapability.RENEW_CERTIFICATES
],
[CaType.AZURE_AD_CS]: [
CaCapability.ISSUE_CERTIFICATES,
CaCapability.RENEW_CERTIFICATES
// Note: REVOKE_CERTIFICATES intentionally omitted - not supported by ADCS connector
]
[CaType.AZURE_AD_CS]: [CaCapability.ISSUE_CERTIFICATES, CaCapability.RENEW_CERTIFICATES]
};
/**
-13
View File
@@ -24,19 +24,6 @@ export enum AcmeDnsProvider {
Cloudflare = "cloudflare"
}
export enum AzureAdCsTemplateType {
WEB_SERVER = "WebServer",
COMPUTER = "Computer",
USER = "User",
DOMAIN_CONTROLLER = "DomainController",
SUBORDINATE_CA = "SubordinateCA"
}
export enum AzureAdCsAuthMethod {
CLIENT_CERTIFICATE = "client-certificate",
KERBEROS = "kerberos"
}
export enum CaCapability {
ISSUE_CERTIFICATES = "issue-certificates",
REVOKE_CERTIFICATES = "revoke-certificates",
+1 -9
View File
@@ -1,12 +1,4 @@
export {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaRenewalType,
CaStatus,
CaType,
InternalCaType
} from "./enums";
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
export {
useCreateCa,
useCreateCertificate,
+2 -11
View File
@@ -1,13 +1,5 @@
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
import {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaRenewalType,
CaStatus,
CaType,
InternalCaType
} from "./enums";
import { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
export type TAcmeCertificateAuthority = {
id: string;
@@ -36,8 +28,7 @@ export type TAzureAdCsCertificateAuthority = {
enableDirectIssuance: boolean;
configuration: {
azureAdcsConnectionId: string;
templateName: AzureAdCsTemplateType;
authMethod: AzureAdCsAuthMethod;
templateName: string;
};
};
@@ -79,14 +79,79 @@ const schema = z
enableAutoRenewal: z.boolean().optional().default(false),
renewalBefore: z.number().min(1).optional(),
renewalUnit: z.nativeEnum(TimeUnit).optional(),
// Properties for Azure ADCS and additional subject fields
// Properties for Azure ADCS only
azureTemplateType: z.string().optional(),
organization: z.string().optional(),
organizationalUnit: z.string().optional(),
country: z.string().length(2).optional().or(z.literal("")),
state: z.string().optional(),
locality: z.string().optional(),
emailAddress: z.string().email().optional().or(z.literal(""))
organization: z
.string()
.trim()
.max(64, "Organization cannot exceed 64 characters")
.regex(
/^[^,=+<>#;\\"/\r\n\t]*$/,
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
)
.regex(
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
)
.optional()
.or(z.literal("")),
organizationalUnit: z
.string()
.trim()
.max(64, "Organizational Unit cannot exceed 64 characters")
.regex(
/^[^,=+<>#;\\"/\r\n\t]*$/,
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
)
.regex(
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
)
.optional()
.or(z.literal("")),
country: z
.string()
.trim()
.length(2, "Country must be exactly 2 characters")
.regex(/^[A-Z]{2}$/, "Country must be exactly 2 uppercase letters")
.optional()
.or(z.literal("")),
state: z
.string()
.trim()
.max(64, "State cannot exceed 64 characters")
.regex(
/^[^,=+<>#;\\"/\r\n\t]*$/,
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
)
.regex(
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
"State cannot start or end with spaces, hyphens, underscores, or periods"
)
.optional()
.or(z.literal("")),
locality: z
.string()
.trim()
.max(64, "Locality cannot exceed 64 characters")
.regex(
/^[^,=+<>#;\\"/\r\n\t]*$/,
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
)
.regex(
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
)
.optional()
.or(z.literal("")),
emailAddress: z
.string()
.trim()
.email("Email Address must be a valid email format")
.min(6, "Email Address must be at least 6 characters")
.max(64, "Email Address cannot exceed 64 characters")
.optional()
.or(z.literal(""))
})
.required();
@@ -444,100 +509,118 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
)}
/>
{/* Additional Subject Fields - Available for all CA types */}
<Accordion type="single" collapsible className="mb-4 w-full">
<AccordionItem value="subject-fields" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Additional Subject Fields</div>
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 gap-4">
<Controller
control={control}
name="organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example Corp" />
</FormControl>
)}
/>
<Controller
control={control}
name="organizationalUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organizational Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="IT Department" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-4">
{/* Additional Subject Fields - Only for Azure ADCS CAs */}
{selectedCa?.type === CaType.AZURE_AD_CS && (
<Accordion type="single" collapsible className="mb-4 w-full">
<AccordionItem value="subject-fields" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Additional Subject Fields</div>
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 gap-4">
<Controller
control={control}
name="country"
name="organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country (C)"
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
>
<Input {...field} placeholder="US" maxLength={2} />
<Input {...field} placeholder="Example Corp" maxLength={64} />
</FormControl>
)}
/>
<Controller
control={control}
name="state"
name="organizationalUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State/Province (ST)"
label="Organizational Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
>
<Input {...field} placeholder="California" />
<Input {...field} placeholder="IT Department" maxLength={64} />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="country"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country (C)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Exactly 2 uppercase letters (ISO 3166-1 alpha-2 code). Examples: US, CA, GB, DE"
>
<Input
{...field}
placeholder="US"
maxLength={2}
style={{ textTransform: "uppercase" }}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="state"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State/Province (ST)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
>
<Input {...field} placeholder="California" maxLength={64} />
</FormControl>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality (L)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
>
<Input {...field} placeholder="San Francisco" maxLength={64} />
</FormControl>
)}
/>
<Controller
control={control}
name="emailAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Email Address"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Valid email format, 6-64 characters. Example: [email protected]"
>
<Input
{...field}
type="email"
placeholder="[email protected]"
maxLength={64}
/>
</FormControl>
)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality (L)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" />
</FormControl>
)}
/>
<Controller
control={control}
name="emailAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Email Address"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="email" placeholder="[email protected]" />
</FormControl>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
{selectedCa?.type !== CaType.ACME && (
<Controller
@@ -1,4 +1,8 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -9,7 +13,10 @@ import {
ModalClose,
SecretInput,
Select,
SelectItem
SelectItem,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AzureADCSConnectionMethod, TAzureADCSConnection } from "@app/hooks/api/appConnections";
@@ -33,9 +40,19 @@ const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
credentials: z.object({
adcsUrl: z.string().url().trim().min(1, "ADCS URL required"),
adcsUrl: z
.string()
.trim()
.min(1, "ADCS URL required")
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS"),
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required")
password: z.string().trim().min(1, "Password required"),
sslRejectUnauthorized: z.boolean().optional(),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
})
})
]);
@@ -44,6 +61,7 @@ type FormData = z.infer<typeof formSchema>;
export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -55,7 +73,9 @@ export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
credentials: {
adcsUrl: "",
username: "",
password: ""
password: "",
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
@@ -63,12 +83,20 @@ export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
formState: { isSubmitting, isDirty },
watch
} = form;
const sslEnabled = watch("credentials.adcsUrl")?.startsWith("https://") ?? false;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(onSubmit)(e);
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
@@ -101,51 +129,148 @@ export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
</FormControl>
)}
/>
<Controller
name="credentials.adcsUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="ADCS URL"
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List
className={`-pb-1 ${selectedTabIndex === 1 ? "mb-3" : "mb-6"} w-full border-b-2 border-mineshaft-600`}
>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
<Input {...field} placeholder="https://your-adcs-server.com" />
</FormControl>
Configuration
</Tab>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
SSL ({sslEnabled ? "Enabled" : "Disabled"})
</Tab>
</Tab.List>
{selectedTabIndex === 1 && (
<div className="mb-2 text-xs text-mineshaft-300">
SSL configuration for HTTPS connections
</div>
)}
/>
<div className="grid grid-cols-2 gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} placeholder="domain\\username" />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
<Tab.Panel>
<Controller
name="credentials.adcsUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="ADCS URL"
>
<Input {...field} placeholder="https://your-adcs-server.com/certsrv" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} placeholder="DOMAIN\\username or [email protected]" />
</FormControl>
)}
/>
</FormControl>
)}
/>
</div>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</Tab.Panel>
<Tab.Panel>
<Controller
name="credentials.sslCertificate"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
isOptional
>
<TextArea
className="h-[3.6rem] !resize-none"
{...field}
isDisabled={!sslEnabled}
placeholder="-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----"
/>
</FormControl>
)}
/>
<Controller
name="credentials.sslRejectUnauthorized"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className={sslEnabled ? "" : "opacity-50"}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-reject-unauthorized"
thumbClassName="bg-mineshaft-800"
isChecked={sslEnabled ? value : false}
onCheckedChange={onChange}
isDisabled={!sslEnabled}
>
<p className="w-[9.5rem]">
Reject Unauthorized
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will only connect to the ADCS server if it has a
valid, trusted SSL certificate. Disable only in test environments with
self-signed certificates.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<div className="mt-8 flex items-center">
<Button
className="mr-4"