From 5a5a44a69fc111e1a17e4af1e6180cf2c437b35b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 8 Dec 2025 16:51:52 -0800 Subject: [PATCH 01/14] Queue cert req --- .../ee/services/pki-acme/pki-acme-service.ts | 265 +++++++++++------- 1 file changed, 169 insertions(+), 96 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 57582655a..cbcf88168 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -7,6 +7,7 @@ import { importJWK, JWSHeaderParameters } from "jose"; +import { Knex } from "knex"; import { z, ZodError } from "zod"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; @@ -30,6 +31,10 @@ import { import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + TCertificateIssuanceQueueFactory, + TIssueCertificateFromProfileJobData +} from "@app/services/certificate-authority/certificate-issuance-queue"; import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import { extractAlgorithmsFromCSR, @@ -40,6 +45,8 @@ import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; +import { TCertificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service"; +import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; import { TCertificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; @@ -100,13 +107,9 @@ import { type TPkiAcmeServiceFactoryDep = { projectDAL: Pick; - appConnectionDAL: Pick; - certificateDAL: Pick; certificateAuthorityDAL: Pick; - externalCertificateAuthorityDAL: Pick; certificateProfileDAL: Pick; certificateBodyDAL: Pick; - certificateSecretDAL: Pick; certificateTemplateV2DAL: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, @@ -135,6 +138,8 @@ type TPkiAcmeServiceFactoryDep = { licenseService: Pick; certificateV3Service: Pick; certificateTemplateV2Service: Pick; + certificateRequestService: Pick; + certificateIssuanceQueue: Pick; acmeChallengeService: Pick; pkiAcmeQueueService: Pick; auditLogService: Pick; @@ -142,13 +147,9 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ projectDAL, - appConnectionDAL, - certificateDAL, certificateAuthorityDAL, - externalCertificateAuthorityDAL, certificateProfileDAL, certificateBodyDAL, - certificateSecretDAL, certificateTemplateV2DAL, acmeAccountDAL, acmeOrderDAL, @@ -160,6 +161,8 @@ export const pkiAcmeServiceFactory = ({ licenseService, certificateV3Service, certificateTemplateV2Service, + certificateRequestService, + certificateIssuanceQueue, acmeChallengeService, pkiAcmeQueueService, auditLogService @@ -744,6 +747,125 @@ export const pkiAcmeServiceFactory = ({ }; }; + const processCertificateIssuanceForOrder = async ({ + caType, + accountId, + actorOrgId, + profileId, + orderId, + csr, + finalizingOrder, + certificateRequest, + profile, + ca, + tx + }: { + caType: CaType; + accountId: string; + actorOrgId: string; + profileId: string; + orderId: string; + csr: string; + finalizingOrder: { + notBefore?: Date | null; + notAfter?: Date | null; + }; + certificateRequest: ReturnType; + profile: TCertificateProfileWithConfigs; + ca: Awaited>; + tx?: Knex; + }): Promise<{ certificateId?: string; certIssuanceJobData?: TIssueCertificateFromProfileJobData }> => { + if (caType === CaType.INTERNAL) { + const result = await certificateV3Service.signCertificateFromProfile({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + profileId, + csr, + notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, + notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, + validity: !finalizingOrder.notAfter + ? { + // 47 days, the default TTL comes with Let's Encrypt + // TODO: read config from the profile to get the expiration time instead + ttl: `${47}d` + } + : // ttl is not used if notAfter is provided + ({ ttl: "0d" } as const), + enrollmentType: EnrollmentType.ACME + }); + return { + certificateId: result.certificateId + }; + } + + const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } = + extractAlgorithmsFromCSR(csr); + const updatedCertificateRequest = { + ...certificateRequest, + keyAlgorithm: extractedKeyAlgorithm, + signatureAlgorithm: extractedSignatureAlgorithm, + validity: finalizingOrder.notAfter + ? (() => { + const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date(); + const notAfter = new Date(finalizingOrder.notAfter); + const diffMs = notAfter.getTime() - notBefore.getTime(); + const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24)); + return { ttl: `${diffDays}d` }; + })() + : certificateRequest.validity + }; + + const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId); + if (!template) { + throw new NotFoundError({ message: "Certificate template not found" }); + } + const validationResult = await certificateTemplateV2Service.validateCertificateRequest( + template.id, + updatedCertificateRequest + ); + if (!validationResult.isValid) { + throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` }); + } + + const certRequest = await certificateRequestService.createCertificateRequest({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + projectId: profile.projectId, + caId: ca.id, + profileId: profile.id, + commonName: updatedCertificateRequest.commonName ?? "", + keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [], + extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [], + keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "", + signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "", + altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","), + notBefore: updatedCertificateRequest.notBefore, + notAfter: updatedCertificateRequest.notAfter, + status: CertificateRequestStatus.PENDING, + tx + }); + return { + certIssuanceJobData: { + certificateId: orderId, + profileId: profile.id, + caId: profile.caId || "", + ttl: updatedCertificateRequest.validity?.ttl || "1y", + signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "", + keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "", + commonName: updatedCertificateRequest.commonName || "", + altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value) || [], + keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [], + extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [], + certificateRequestId: certRequest.id, + csr + } + }; + }; + const finalizeAcmeOrder = async ({ profileId, accountId, @@ -768,7 +890,11 @@ export const pkiAcmeServiceFactory = ({ throw new NotFoundError({ message: "ACME order not found" }); } if (order.status === AcmeOrderStatus.Ready) { - const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => { + const { + order: updatedOrder, + error, + certIssuanceJobData + } = await acmeOrderDAL.transaction(async (tx) => { const finalizingOrder = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead? const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; @@ -815,94 +941,33 @@ export const pkiAcmeServiceFactory = ({ } const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; let errorToReturn: Error | undefined; + let certIssuanceJobDataToReturn: TIssueCertificateFromProfileJobData | undefined; try { - const { certificateId } = await (async () => { - if (caType === CaType.INTERNAL) { - const result = await certificateV3Service.signCertificateFromProfile({ - actor: ActorType.ACME_ACCOUNT, - actorId: accountId, - actorAuthMethod: null, - actorOrgId, - profileId, - csr, - notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, - notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, - validity: !finalizingOrder.notAfter - ? { - // 47 days, the default TTL comes with Let's Encrypt - // TODO: read config from the profile to get the expiration time instead - ttl: `${47}d` - } - : // ttl is not used if notAfter is provided - ({ ttl: "0d" } as const), - enrollmentType: EnrollmentType.ACME - }); - return { certificateId: result.certificateId }; - } - const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!; - const csrObj = new x509.Pkcs10CertificateRequest(csr); - const csrPem = csrObj.toString("pem"); - - const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } = - extractAlgorithmsFromCSR(csr); - - certificateRequest.keyAlgorithm = extractedKeyAlgorithm; - certificateRequest.signatureAlgorithm = extractedSignatureAlgorithm; - if (finalizingOrder.notAfter) { - const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date(); - const notAfter = new Date(finalizingOrder.notAfter); - const diffMs = notAfter.getTime() - notBefore.getTime(); - const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24)); - certificateRequest.validity = { ttl: `${diffDays}d` }; - } - - const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId); - if (!template) { - throw new NotFoundError({ message: "Certificate template not found" }); - } - const validationResult = await certificateTemplateV2Service.validateCertificateRequest( - template.id, - certificateRequest - ); - if (!validationResult.isValid) { - throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` }); - } - // TODO: this is pretty slow, and we are holding the transaction open for a long time, - // we should queue the certificate issuance to a background job instead - const cert = await orderCertificate( - { - caId: certificateAuthority!.id, - // It is possible that the CSR does not have a common name, in which case we use an empty string - // (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.) - commonName: certificateRequest.commonName ?? "", - altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value), - csr: Buffer.from(csrPem), - // TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now - keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT], - extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH] - }, - { - appConnectionDAL, - certificateAuthorityDAL, - externalCertificateAuthorityDAL, - certificateDAL, - certificateBodyDAL, - certificateSecretDAL, - kmsService, - projectDAL - } - ); - return { certificateId: cert.id }; - })(); - await acmeOrderDAL.updateById( + const result = await processCertificateIssuanceForOrder({ + caType, + accountId, + actorOrgId, + profileId, orderId, - { - status: AcmeOrderStatus.Valid, - csr, - certificateId - }, + csr, + finalizingOrder, + certificateRequest, + profile, + ca, tx - ); + }); + if (result.certificateId) { + await acmeOrderDAL.updateById( + orderId, + { + status: AcmeOrderStatus.Valid, + csr, + certificateId: result.certificateId + }, + tx + ); + } + certIssuanceJobDataToReturn = result.certIssuanceJobData; } catch (exp) { await acmeOrderDAL.updateById( orderId, @@ -920,17 +985,25 @@ export const pkiAcmeServiceFactory = ({ } else if (exp instanceof AcmeError) { errorToReturn = exp; } else { - errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" }); + errorToReturn = new AcmeServerInternalError({ + message: "Failed to sign certificate with internal error" + }); } } return { order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!, - error: errorToReturn + error: errorToReturn, + certIssuanceJobData: certIssuanceJobDataToReturn }; }); if (error) { throw error; } + if (certIssuanceJobData) { + // TODO: ideally, this should be done inside the transaction, but the pg-boss queue doesn't support external transactions + // as it seems to be. + await certificateIssuanceQueue.queueCertificateIssuance(certIssuanceJobData); + } order = updatedOrder; await auditLogService.createAuditLog({ projectId: profile.projectId, From 120947329b0d7f5c87a316a4de45b0880d711d04 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 8 Dec 2025 17:22:14 -0800 Subject: [PATCH 02/14] Check and sync status --- .../services/pki-acme/pki-acme-order-dal.ts | 38 ++++++++++++ .../ee/services/pki-acme/pki-acme-service.ts | 61 +++++++++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 5aab0be63..002b9f3bf 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; export type TPkiAcmeOrderDALFactory = ReturnType; @@ -19,6 +20,42 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { } }; + const findWithCertificateRequestForSync = async (id: string, tx?: Knex) => { + try { + const order = await (tx || db)(TableName.PkiAcmeOrder) + .leftJoin( + TableName.CertificateRequests, + `${TableName.PkiAcmeOrder}.id`, + `${TableName.CertificateRequests}.certificateId` + ) + .select( + selectAllTableCols(TableName.PkiAcmeOrder), + db.ref("id").withSchema(TableName.CertificateRequests).as("certificateRequestId"), + db.ref("status").withSchema(TableName.CertificateRequests).as("certificateRequestStatus") + ) + .forUpdate(TableName.PkiAcmeOrder) + .where(`${TableName.PkiAcmeOrder}.id`, id) + .first(); + if (!order) { + return null; + } + return { + ...order, + certificateRequest: + order.certificateRequestId && order.certificateRequestStatus + ? { + id: order.certificateRequestId, + status: order.certificateRequestStatus as CertificateRequestStatus, + // The certificate id for async certificate request is the same as the order id + certificateId: order.id + } + : undefined + }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME order by id with certificate request" }); + } + }; + const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeOrder) @@ -72,6 +109,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { return { ...pkiAcmeOrderOrm, findByIdForFinalization, + findWithCertificateRequestForSync, findByAccountAndOrderIdWithAuthorizations, listByAccountId }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index cbcf88168..e6a0c8fc2 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -104,6 +104,7 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TPkiAcmeOrders } from "@app/db/schemas"; type TPkiAcmeServiceFactoryDep = { projectDAL: Pick; @@ -117,11 +118,13 @@ type TPkiAcmeServiceFactoryDep = { >; acmeOrderDAL: Pick< TPkiAcmeOrderDALFactory, + | "findById" | "create" | "transaction" | "updateById" | "findByAccountAndOrderIdWithAuthorizations" | "findByIdForFinalization" + | "findWithCertificateRequestForSync" | "listByAccountId" >; acmeAuthDAL: Pick; @@ -370,6 +373,50 @@ export const pkiAcmeServiceFactory = ({ }; }; + const checkAndSyncAcmeOrderStatus = async ({ orderId }: { orderId: string }): Promise => { + const order = await acmeOrderDAL.findById(orderId); + if (!order) { + throw new NotFoundError({ message: "ACME order not found" }); + } + if (order.status !== AcmeOrderStatus.Ready) { + return order; + } + return acmeOrderDAL.transaction(async (tx) => { + // Lock the order for syncing with async cert request + const orderWithCertificateRequest = await acmeOrderDAL.findWithCertificateRequestForSync(orderId, tx); + if (!orderWithCertificateRequest) { + throw new NotFoundError({ message: "ACME order not found" }); + } + if ( + orderWithCertificateRequest.status !== AcmeOrderStatus.Ready || + !orderWithCertificateRequest.certificateRequest + ) { + return orderWithCertificateRequest; + } + let newStatus: AcmeOrderStatus | undefined; + let newCertificateId: string | undefined; + switch (orderWithCertificateRequest.certificateRequest.status) { + case CertificateRequestStatus.PENDING: + break; + case CertificateRequestStatus.ISSUED: + newStatus = AcmeOrderStatus.Valid; + newCertificateId = orderWithCertificateRequest.certificateRequest.certificateId; + break; + case CertificateRequestStatus.FAILED: + newStatus = AcmeOrderStatus.Invalid; + break; + default: + throw new AcmeServerInternalError({ + message: `Invalid certificate request status: ${orderWithCertificateRequest.certificateRequest.status as string}` + }); + } + if (newStatus) { + return acmeOrderDAL.updateById(orderId, { status: newStatus, certificateId: newCertificateId }, tx); + } + return orderWithCertificateRequest; + }); + }; + const getAcmeDirectory = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); return { @@ -737,9 +784,11 @@ export const pkiAcmeServiceFactory = ({ if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } + // Sync order first in case if there is a certificate request that needs to be processed + const syncedOrder = await checkAndSyncAcmeOrderStatus({ orderId }); return { status: 200, - body: buildAcmeOrderResource({ profileId, order }), + body: buildAcmeOrderResource({ profileId, order: syncedOrder }), headers: { Location: buildUrl(profileId, `/orders/${orderId}`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` @@ -1001,7 +1050,7 @@ export const pkiAcmeServiceFactory = ({ } if (certIssuanceJobData) { // TODO: ideally, this should be done inside the transaction, but the pg-boss queue doesn't support external transactions - // as it seems to be. + // as it seems to be. we need to commit the transaction before queuing the job, otherwise the job will fail (not found error). await certificateIssuanceQueue.queueCertificateIssuance(certIssuanceJobData); } order = updatedOrder; @@ -1049,14 +1098,16 @@ export const pkiAcmeServiceFactory = ({ if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } - if (order.status !== AcmeOrderStatus.Valid) { + // Sync order first in case if there is a certificate request that needs to be processed + const syncedOrder = await checkAndSyncAcmeOrderStatus({ orderId }); + if (syncedOrder.status !== AcmeOrderStatus.Valid) { throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" }); } - if (!order.certificateId) { + if (!syncedOrder.certificateId) { throw new NotFoundError({ message: "The certificate for this ACME order no longer exists" }); } - const certBody = await certificateBodyDAL.findOne({ certId: order.certificateId }); + const certBody = await certificateBodyDAL.findOne({ certId: syncedOrder.certificateId }); const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ projectId: profile.projectId, projectDAL, From 0bb2e2be6c0bbf600f02614cdb6062657a9a1d84 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 8 Dec 2025 18:43:03 -0800 Subject: [PATCH 03/14] Fix wrong pem csr format --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index e6a0c8fc2..df71f9335 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -785,10 +785,11 @@ export const pkiAcmeServiceFactory = ({ throw new NotFoundError({ message: "ACME order not found" }); } // Sync order first in case if there is a certificate request that needs to be processed - const syncedOrder = await checkAndSyncAcmeOrderStatus({ orderId }); + await checkAndSyncAcmeOrderStatus({ orderId }); + const updatedOrder = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); return { status: 200, - body: buildAcmeOrderResource({ profileId, order: syncedOrder }), + body: buildAcmeOrderResource({ profileId, order: updatedOrder }), headers: { Location: buildUrl(profileId, `/orders/${orderId}`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` @@ -897,6 +898,8 @@ export const pkiAcmeServiceFactory = ({ status: CertificateRequestStatus.PENDING, tx }); + const csrObj = new x509.Pkcs10CertificateRequest(csr); + const csrPem = csrObj.toString("pem"); return { certIssuanceJobData: { certificateId: orderId, @@ -910,7 +913,7 @@ export const pkiAcmeServiceFactory = ({ keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [], extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [], certificateRequestId: certRequest.id, - csr + csr: csrPem } }; }; From ff6a45795ea4b7be87076bb54e9b8c767aaa675e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:08:28 -0800 Subject: [PATCH 04/14] Avoid queuing cert issue req many times --- .../ee/services/pki-acme/pki-acme-schemas.ts | 2 +- .../ee/services/pki-acme/pki-acme-service.ts | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 23b86d172..ebd17c26a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -6,8 +6,8 @@ export enum AcmeIdentifierType { export enum AcmeOrderStatus { Pending = "pending", - Processing = "processing", Ready = "ready", + Processing = "processing", Valid = "valid", Invalid = "invalid" } diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index df71f9335..5b163c6f0 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -10,6 +10,7 @@ import { import { Knex } from "knex"; import { z, ZodError } from "zod"; +import { TPkiAcmeOrders } from "@app/db/schemas"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; @@ -104,7 +105,6 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { TPkiAcmeOrders } from "@app/db/schemas"; type TPkiAcmeServiceFactoryDep = { projectDAL: Pick; @@ -1008,17 +1008,15 @@ export const pkiAcmeServiceFactory = ({ ca, tx }); - if (result.certificateId) { - await acmeOrderDAL.updateById( - orderId, - { - status: AcmeOrderStatus.Valid, - csr, - certificateId: result.certificateId - }, - tx - ); - } + await acmeOrderDAL.updateById( + orderId, + { + status: result.certificateId ? AcmeOrderStatus.Valid : AcmeOrderStatus.Processing, + csr, + certificateId: result.certificateId + }, + tx + ); certIssuanceJobDataToReturn = result.certIssuanceJobData; } catch (exp) { await acmeOrderDAL.updateById( From 61faa847a73df6d7c583528975afb1cc89764f90 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:19:02 -0800 Subject: [PATCH 05/14] Add db migration --- ...101_add-acme-order-id-for-cert-requests.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 backend/src/db/migrations/20251209191101_add-acme-order-id-for-cert-requests.ts diff --git a/backend/src/db/migrations/20251209191101_add-acme-order-id-for-cert-requests.ts b/backend/src/db/migrations/20251209191101_add-acme-order-id-for-cert-requests.ts new file mode 100644 index 000000000..3a87e75a4 --- /dev/null +++ b/backend/src/db/migrations/20251209191101_add-acme-order-id-for-cert-requests.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { dropConstraintIfExists } from "./utils/dropConstraintIfExists"; + +const FOREIGN_KEY_CONSTRAINT_NAME = "certificate_requests_acme_order_id_fkey"; +const INDEX_NAME = "certificate_requests_acme_order_id_idx"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateRequests)) { + const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId"); + + if (!hasAcmeOrderId) { + await knex.schema.alterTable(TableName.CertificateRequests, (t) => { + t.uuid("acmeOrderId").nullable(); + t.foreign("acmeOrderId", FOREIGN_KEY_CONSTRAINT_NAME) + .references("id") + .inTable(TableName.PkiAcmeOrder) + .onDelete("SET NULL"); + t.index("acmeOrderId", INDEX_NAME); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateRequests)) { + const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId"); + + if (hasAcmeOrderId) { + await dropConstraintIfExists(TableName.CertificateRequests, FOREIGN_KEY_CONSTRAINT_NAME, knex); + await knex.schema.alterTable(TableName.CertificateRequests, (t) => { + t.dropIndex("acmeOrderId", INDEX_NAME); + t.dropColumn("acmeOrderId"); + }); + } + } +} From 066599de05b5776d312cfd1909fbaeb6d3a014ca Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:25:33 -0800 Subject: [PATCH 06/14] Add acme order id --- backend/src/db/schemas/certificate-requests.ts | 3 ++- .../certificate-request/certificate-request-service.ts | 2 ++ .../services/certificate-request/certificate-request-types.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/db/schemas/certificate-requests.ts b/backend/src/db/schemas/certificate-requests.ts index e01e08bbd..4013ea5bb 100644 --- a/backend/src/db/schemas/certificate-requests.ts +++ b/backend/src/db/schemas/certificate-requests.ts @@ -26,7 +26,8 @@ export const CertificateRequestsSchema = z.object({ keyAlgorithm: z.string().nullable().optional(), signatureAlgorithm: z.string().nullable().optional(), errorMessage: z.string().nullable().optional(), - metadata: z.string().nullable().optional() + metadata: z.string().nullable().optional(), + acmeOrderId: z.string().uuid().nullable().optional() }); export type TCertificateRequests = z.infer; diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 3bce23de8..78bde276b 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -91,6 +91,7 @@ export const certificateRequestServiceFactory = ({ permissionService }: TCertificateRequestServiceFactoryDep) => { const createCertificateRequest = async ({ + acmeOrderId, actor, actorId, actorAuthMethod, @@ -123,6 +124,7 @@ export const certificateRequestServiceFactory = ({ { status, projectId, + acmeOrderId, ...validatedData }, tx diff --git a/backend/src/services/certificate-request/certificate-request-types.ts b/backend/src/services/certificate-request/certificate-request-types.ts index 0c62973b2..9ccf6fbae 100644 --- a/backend/src/services/certificate-request/certificate-request-types.ts +++ b/backend/src/services/certificate-request/certificate-request-types.ts @@ -21,6 +21,7 @@ export type TCreateCertificateRequestDTO = TProjectPermission & { metadata?: string; status: CertificateRequestStatus; certificateId?: string; + acmeOrderId?: string; }; export type TGetCertificateRequestDTO = TProjectPermission & { From 55dbd7d874c753558d93c3580bc1e3abe8d831ca Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:32:43 -0800 Subject: [PATCH 07/14] Handle processing status for order sync --- .../ee/services/pki-acme/pki-acme-order-dal.ts | 17 +++++++++-------- .../ee/services/pki-acme/pki-acme-service.ts | 7 +++++-- backend/src/server/routes/index.ts | 2 ++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 002b9f3bf..d20abe08d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -26,12 +26,13 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { .leftJoin( TableName.CertificateRequests, `${TableName.PkiAcmeOrder}.id`, - `${TableName.CertificateRequests}.certificateId` + `${TableName.CertificateRequests}.acmeOrderId` ) .select( selectAllTableCols(TableName.PkiAcmeOrder), db.ref("id").withSchema(TableName.CertificateRequests).as("certificateRequestId"), - db.ref("status").withSchema(TableName.CertificateRequests).as("certificateRequestStatus") + db.ref("status").withSchema(TableName.CertificateRequests).as("certificateRequestStatus"), + db.ref("certificateId").withSchema(TableName.CertificateRequests).as("certificateId") ) .forUpdate(TableName.PkiAcmeOrder) .where(`${TableName.PkiAcmeOrder}.id`, id) @@ -39,15 +40,15 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { if (!order) { return null; } + const { certificateRequestId, certificateRequestStatus, certificateId, ...details } = order; return { - ...order, + ...details, certificateRequest: - order.certificateRequestId && order.certificateRequestStatus + certificateRequestId && certificateRequestStatus && certificateId ? { - id: order.certificateRequestId, - status: order.certificateRequestStatus as CertificateRequestStatus, - // The certificate id for async certificate request is the same as the order id - certificateId: order.id + id: certificateRequestId, + status: certificateRequestStatus as CertificateRequestStatus, + certificateId } : undefined }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 5b163c6f0..77b68bf53 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -378,7 +378,8 @@ export const pkiAcmeServiceFactory = ({ if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } - if (order.status !== AcmeOrderStatus.Ready) { + if (order.status !== AcmeOrderStatus.Processing) { + // We only care about processing orders, as they are the ones that have async certificate requests return order; } return acmeOrderDAL.transaction(async (tx) => { @@ -387,8 +388,9 @@ export const pkiAcmeServiceFactory = ({ if (!orderWithCertificateRequest) { throw new NotFoundError({ message: "ACME order not found" }); } + // Check the status again after we have acquired the lock, as things may have changed since we last checked if ( - orderWithCertificateRequest.status !== AcmeOrderStatus.Ready || + orderWithCertificateRequest.status !== AcmeOrderStatus.Processing || !orderWithCertificateRequest.certificateRequest ) { return orderWithCertificateRequest; @@ -896,6 +898,7 @@ export const pkiAcmeServiceFactory = ({ notBefore: updatedCertificateRequest.notBefore, notAfter: updatedCertificateRequest.notAfter, status: CertificateRequestStatus.PENDING, + acmeOrderId: orderId, tx }); const csrObj = new x509.Pkcs10CertificateRequest(csr); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3bc6b22e7..adcea1822 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2332,6 +2332,8 @@ export const registerRoutes = async ( licenseService, certificateV3Service, certificateTemplateV2Service, + certificateRequestService, + certificateIssuanceQueue, acmeChallengeService, pkiAcmeQueueService, auditLogService From 9b0d330344b80daddae3bbd67349b89ce24c6bbb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:43:01 -0800 Subject: [PATCH 08/14] Fix import and lint --- .../src/ee/services/pki-acme/pki-acme-service.ts | 13 ++----------- backend/src/server/routes/index.ts | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 77b68bf53..9ba1c6319 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -19,24 +19,15 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { isPrivateIp } from "@app/lib/ip/ipRange"; import { logger } from "@app/lib/logger"; -import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { ActorType } from "@app/services/auth/auth-type"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; -import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; -import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; -import { - CertExtendedKeyUsage, - CertKeyUsage, - CertSubjectAlternativeNameType -} from "@app/services/certificate/certificate-types"; -import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns"; +import { CertSubjectAlternativeNameType } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { TCertificateIssuanceQueueFactory, TIssueCertificateFromProfileJobData } from "@app/services/certificate-authority/certificate-issuance-queue"; -import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import { extractAlgorithmsFromCSR, extractCertificateRequestFromCSR @@ -788,7 +779,7 @@ export const pkiAcmeServiceFactory = ({ } // Sync order first in case if there is a certificate request that needs to be processed await checkAndSyncAcmeOrderStatus({ orderId }); - const updatedOrder = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); + const updatedOrder = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId))!; return { status: 200, body: buildAcmeOrderResource({ profileId, order: updatedOrder }), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index adcea1822..b969c0165 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2314,7 +2314,6 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, - appConnectionDAL, certificateDAL, certificateAuthorityDAL, externalCertificateAuthorityDAL, From d02e6bcf3ea42f5c9e1de3eef91329c3feadb66a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 11:55:36 -0800 Subject: [PATCH 09/14] Fix dep injection --- backend/src/server/routes/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b969c0165..cd4f75e7c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2314,12 +2314,9 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, - certificateDAL, certificateAuthorityDAL, - externalCertificateAuthorityDAL, certificateProfileDAL, certificateBodyDAL, - certificateSecretDAL, certificateTemplateV2DAL, acmeAccountDAL, acmeOrderDAL, From 1608562a2a7d69b1e5d7548f9decb94e28418a7e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 9 Dec 2025 16:17:32 -0800 Subject: [PATCH 10/14] Address ai review feedbacks --- backend/src/ee/services/pki-acme/pki-acme-order-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index d20abe08d..cf7d96e87 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -44,7 +44,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { return { ...details, certificateRequest: - certificateRequestId && certificateRequestStatus && certificateId + certificateRequestId && certificateRequestStatus ? { id: certificateRequestId, status: certificateRequestStatus as CertificateRequestStatus, From d3c32755316185e3b9f9fea553398919451933bf Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 10 Dec 2025 11:59:43 -0800 Subject: [PATCH 11/14] Try to fix linter issue --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9ba1c6319..a437d6cd2 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -393,7 +393,7 @@ export const pkiAcmeServiceFactory = ({ break; case CertificateRequestStatus.ISSUED: newStatus = AcmeOrderStatus.Valid; - newCertificateId = orderWithCertificateRequest.certificateRequest.certificateId; + newCertificateId = orderWithCertificateRequest.certificateRequest.certificateId ?? undefined; break; case CertificateRequestStatus.FAILED: newStatus = AcmeOrderStatus.Invalid; From 4827ca13a75b573059925977495c5b0478c2003c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 12 Dec 2025 19:09:33 -0800 Subject: [PATCH 12/14] Add CSR value for cert request --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a437d6cd2..705cd6c39 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -890,6 +890,7 @@ export const pkiAcmeServiceFactory = ({ notAfter: updatedCertificateRequest.notAfter, status: CertificateRequestStatus.PENDING, acmeOrderId: orderId, + csr, tx }); const csrObj = new x509.Pkcs10CertificateRequest(csr); From 9894773fdbfd532c9411d461c8bfe59fc87c35bf Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 12 Dec 2025 19:24:34 -0800 Subject: [PATCH 13/14] Add missing technitium --- docker-compose.bdd.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml index b73683867..5dc3208ee 100644 --- a/docker-compose.bdd.yml +++ b/docker-compose.bdd.yml @@ -94,6 +94,15 @@ services: volumes: - ./backend/bdd/pebble/:/var/data/pebble:ro + technitium: + image: technitium/dns-server:14.2.0 + ports: + - "5380:5380/tcp" + - "53:53/udp" + - "53:53/tcp" + environment: + - DNS_SERVER_ADMIN_PASSWORD=infisical + volumes: postgres-data: driver: local From cbd10a9f8a068c58685f41f28052d60d680ca780 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 12 Dec 2025 19:25:39 -0800 Subject: [PATCH 14/14] Revert "Add missing technitium" This reverts commit 9894773fdbfd532c9411d461c8bfe59fc87c35bf. --- docker-compose.bdd.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml index 5dc3208ee..b73683867 100644 --- a/docker-compose.bdd.yml +++ b/docker-compose.bdd.yml @@ -94,15 +94,6 @@ services: volumes: - ./backend/bdd/pebble/:/var/data/pebble:ro - technitium: - image: technitium/dns-server:14.2.0 - ports: - - "5380:5380/tcp" - - "53:53/udp" - - "53:53/tcp" - environment: - - DNS_SERVER_ADMIN_PASSWORD=infisical - volumes: postgres-data: driver: local