diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 47f5ca5cd..504b9019d 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -2148,7 +2148,9 @@ export const CertificateAuthorities = {
directoryUrl: `The directory URL for the ACME Certificate Authority.`,
accountEmail: `The email address for the ACME Certificate Authority.`,
provider: `The DNS provider for the ACME Certificate Authority.`,
- hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`
+ hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`,
+ eabKid: `The External Account Binding (EAB) Key ID for the ACME Certificate Authority. Required if the ACME provider uses EAB.`,
+ eabHmacKey: `The External Account Binding (EAB) HMAC key for the ACME Certificate Authority. Required if the ACME provider uses EAB.`
},
INTERNAL: {
type: "The type of CA to create.",
diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
index 86beadffe..830378ca8 100644
--- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
+++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
@@ -64,6 +64,8 @@ type DBConfigurationColumn = {
directoryUrl: string;
accountEmail: string;
hostedZoneId: string;
+ eabKid?: string;
+ eabHmacKey?: string;
};
export const castDbEntryToAcmeCertificateAuthority = (
@@ -89,7 +91,9 @@ export const castDbEntryToAcmeCertificateAuthority = (
hostedZoneId: dbConfigurationCol.hostedZoneId
},
directoryUrl: dbConfigurationCol.directoryUrl,
- accountEmail: dbConfigurationCol.accountEmail
+ accountEmail: dbConfigurationCol.accountEmail,
+ eabKid: dbConfigurationCol.eabKid,
+ eabHmacKey: dbConfigurationCol.eabHmacKey
},
status: ca.status as CaStatus
};
@@ -128,7 +132,7 @@ export const AcmeCertificateAuthorityFns = ({
});
}
- const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
+ const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig, eabKid, eabHmacKey } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) {
@@ -171,7 +175,9 @@ export const AcmeCertificateAuthorityFns = ({
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
- hostedZoneId: dnsProviderConfig.hostedZoneId
+ hostedZoneId: dnsProviderConfig.hostedZoneId,
+ eabKid,
+ eabHmacKey
}
},
tx
@@ -214,7 +220,7 @@ export const AcmeCertificateAuthorityFns = ({
}) => {
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (configuration) {
- const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
+ const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig, eabKid, eabHmacKey } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) {
@@ -254,7 +260,9 @@ export const AcmeCertificateAuthorityFns = ({
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
- hostedZoneId: dnsProviderConfig.hostedZoneId
+ hostedZoneId: dnsProviderConfig.hostedZoneId,
+ eabKid,
+ eabHmacKey
}
},
tx
@@ -354,10 +362,19 @@ export const AcmeCertificateAuthorityFns = ({
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
- const acmeClient = new acme.Client({
+ const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
- });
+ };
+
+ if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
+ acmeClientOptions.externalAccountBinding = {
+ kid: acmeCa.configuration.eabKid,
+ hmacKey: acmeCa.configuration.eabHmacKey
+ };
+ }
+
+ const acmeClient = new acme.Client(acmeClientOptions);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts
index 56b3118cf..70b0cb0e1 100644
--- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts
+++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts
@@ -18,7 +18,9 @@ export const AcmeCertificateAuthorityConfigurationSchema = z.object({
hostedZoneId: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.hostedZoneId)
}),
directoryUrl: z.string().url().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.directoryUrl),
- accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail)
+ accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail),
+ eabKid: z.string().trim().max(64).optional().describe(CertificateAuthorities.CONFIGURATIONS.ACME.eabKid),
+ eabHmacKey: z.string().trim().max(512).optional().describe(CertificateAuthorities.CONFIGURATIONS.ACME.eabHmacKey)
});
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
diff --git a/docs/documentation/platform/pki/acme-ca.mdx b/docs/documentation/platform/pki/acme-ca.mdx
index 8b1fa10db..189c1045a 100644
--- a/docs/documentation/platform/pki/acme-ca.mdx
+++ b/docs/documentation/platform/pki/acme-ca.mdx
@@ -147,6 +147,8 @@ In the following steps, we explore how to set up ACME Certificate Authority inte
- **Directory URL**: Enter the ACME v2 directory URL for your chosen CA provider (e.g., `https://acme-v02.api.letsencrypt.org/directory` for Let's Encrypt).
- **Account Email**: Email address to associate with your ACME account. This email will receive important notifications about your certificates.
- **Enable Direct Issuance**: Toggle on to allow direct certificate issuance without requiring subscribers.
+ - **EAB Key Identifier (KID)**: (Optional) The Key Identifier (KID) provided by your ACME CA for External Account Binding (EAB). This is required by some ACME providers (e.g., ZeroSSL, DigiCert) to link your ACME account to an external account you've pre-registered with them.
+ - **EAB HMAC Key**: (Optional) The HMAC Key provided by your ACME CA for External Account Binding (EAB). This key is used in conjunction with the KID to prove ownership of the external account during ACME account registration.
Finally, press **Create** to register the ACME CA with Infisical.
@@ -277,6 +279,19 @@ Let's Encrypt is a free, automated, and open Certificate Authority that provides
Always test your ACME integration using Let's Encrypt's staging environment first. This allows you to verify your DNS configuration and certificate issuance process without consuming your production rate limits.
+## Example: DigiCert Integration
+
+DigiCert is a leading commercial Certificate Authority providing a wide range of trusted SSL/TLS certificates. Infisical can integrate with DigiCert's ACME service to automate the provisioning and management of these certificates.
+
+- **Directory URL**: `https://acme.digicert.com/v2/acme/directory`
+- **External Account Binding (EAB)**: Required. You will need a Key Identifier (KID) and HMAC Key from your DigiCert account to register the ACME CA in Infisical.
+- **Certificate Validity**: Typically 90 days, with automatic renewal through Infisical.
+- **Trusted By**: All major browsers and operating systems.
+
+
+ When integrating with DigiCert ACME, ensure you have obtained the necessary External Account Binding (EAB) Key Identifier (KID) and HMAC Key from your DigiCert account.
+
+
## FAQ
diff --git a/docs/images/platform/pki/ca/external-ca/create-external-ca-form.png b/docs/images/platform/pki/ca/external-ca/create-external-ca-form.png
index ef572bfa7..bc32c23f6 100644
Binary files a/docs/images/platform/pki/ca/external-ca/create-external-ca-form.png and b/docs/images/platform/pki/ca/external-ca/create-external-ca-form.png differ
diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts
index 8dd874a75..336688ca4 100644
--- a/frontend/src/hooks/api/ca/types.ts
+++ b/frontend/src/hooks/api/ca/types.ts
@@ -16,6 +16,8 @@ export type TAcmeCertificateAuthority = {
};
directoryUrl: string;
accountEmail: string;
+ eabKid?: string;
+ eabHmacKey?: string;
};
};
diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx
index 8b0a88bd0..c360047da 100644
--- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx
+++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx
@@ -42,6 +42,17 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
+const REQUIRED_EAB_DIRECTORIES = [
+ "https://acme.digicert.com/v2/acme/directory",
+ "https://acme.zerossl.com/v2/DV90",
+ "https://acme.ssl.com/sslcom-dv-rsa",
+ "https://acme.ssl.com/sslcom-dv-ecc",
+ "https://dv.acme-v02.api.pki.goog/directory",
+ "https://acme.sectigo.com/v2/OV",
+ "https://acme.sectigo.com/v2/EV",
+ "https://acme.cisco.com/ACMEv2/directory"
+];
+
const baseSchema = z.object({
type: z.nativeEnum(CaType),
name: slugSchema({
@@ -51,18 +62,39 @@ const baseSchema = z.object({
status: z.nativeEnum(CaStatus)
});
-const acmeConfigurationSchema = z.object({
- dnsAppConnection: z.object({
- id: z.string(),
- name: z.string()
- }),
- dnsProviderConfig: z.object({
- provider: z.nativeEnum(AcmeDnsProvider),
- hostedZoneId: z.string()
- }),
- directoryUrl: z.string(),
- accountEmail: z.string()
-});
+const acmeConfigurationSchema = z
+ .object({
+ dnsAppConnection: z.object({
+ id: z.string(),
+ name: z.string()
+ }),
+ dnsProviderConfig: z.object({
+ provider: z.nativeEnum(AcmeDnsProvider),
+ hostedZoneId: z.string()
+ }),
+ directoryUrl: z.string(),
+ accountEmail: z.string(),
+ eabKid: z.string().optional(),
+ eabHmacKey: z.string().optional()
+ })
+ .superRefine((data, ctx) => {
+ if (REQUIRED_EAB_DIRECTORIES.includes(data.directoryUrl)) {
+ if (!data.eabKid || data.eabKid.trim() === "") {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "EAB Key Identifier (KID) is required for this directory URL",
+ path: ["eabKid"]
+ });
+ }
+ if (!data.eabHmacKey || data.eabHmacKey.trim() === "") {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "EAB HMAC Key is required for this directory URL",
+ path: ["eabHmacKey"]
+ });
+ }
+ }
+ });
const azureAdCsConfigurationSchema = z.object({
azureAdcsConnection: z.object({
@@ -122,6 +154,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
caType === CaType.ACME && configuration && "dnsProviderConfig" in configuration
? configuration.dnsProviderConfig.provider
: undefined;
+ const directoryUrl =
+ caType === CaType.ACME && configuration && "directoryUrl" in configuration
+ ? configuration.directoryUrl
+ : undefined;
useEffect(() => {
const initialType = (popUp?.ca?.data as { type: CaType })?.type;
@@ -155,7 +191,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
hostedZoneId: ""
},
directoryUrl: "",
- accountEmail: ""
+ accountEmail: "",
+ eabKid: "",
+ eabHmacKey: ""
}
});
}
@@ -192,7 +230,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
availableAzureConnections
]);
- const isPending = isRoute53Pending || isCloudflarePending || isAzurePending;
+ const isPending =
+ isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
const dnsAppConnection =
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
@@ -227,7 +266,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
hostedZoneId: ca.configuration.dnsProviderConfig.hostedZoneId
},
directoryUrl: ca.configuration.directoryUrl,
- accountEmail: ca.configuration.accountEmail
+ accountEmail: ca.configuration.accountEmail,
+ eabKid: ca.configuration.eabKid,
+ eabHmacKey: ca.configuration.eabHmacKey
}
});
} else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) {
@@ -268,7 +309,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
dnsProviderConfig: formConfiguration.dnsProviderConfig,
directoryUrl: formConfiguration.directoryUrl,
accountEmail: formConfiguration.accountEmail,
- dnsAppConnectionId: formConfiguration.dnsAppConnection.id
+ dnsAppConnectionId: formConfiguration.dnsAppConnection.id,
+ eabKid: formConfiguration.eabKid,
+ eabHmacKey: formConfiguration.eabHmacKey
};
} else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) {
configPayload = {
@@ -499,6 +542,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
)}
/>
+ (
+
+
+
+ )}
+ />
+ (
+
+
+
+ )}
+ />
>
)}
{caType === CaType.AZURE_AD_CS && (