Merge pull request #4975 from Infisical/PKI-38-better-http-01-challenge

improvement(api): perform http01-challenge in the background with retries
This commit is contained in:
Fang-Pen Lin
2025-12-02 11:48:54 -08:00
committed by GitHub
8 changed files with 317 additions and 138 deletions
+32 -46
View File
@@ -22,6 +22,28 @@ Feature: Challenge
And I parse the full-chain certificate from order finalized_order as cert And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost" And the value cert with jq ".subject.common_name" should be equal to "localhost"
Scenario: Validate challenge with retry
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I wait 45 seconds and serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
And I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost"
Scenario: Validate challenges for multiple domains Scenario: Validate challenges for multiple domains
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
@@ -63,13 +85,12 @@ Feature: Challenge
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
""" """
{ {}
"COMMON_NAME": "localhost"
}
""" """
And I add subject alternative name to certificate signing request csr And I add subject alternative name to certificate signing request csr
""" """
[ [
"localhost",
"infisical.com" "infisical.com"
] ]
""" """
@@ -82,56 +103,19 @@ Feature: Challenge
# the localhost auth should be valid # the localhost auth should be valid
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth
And I peak and memorize the next nonce as nonce And I wait until the status of authorization localhost_auth becomes valid
When I send a raw ACME request to "{localhost_auth}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{localhost_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "valid"
# the infisical.com auth should still be pending # the infisical.com auth should still be pending
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth
And I memorize response.headers with jq ".["replay-nonce"]" as nonce And I post-as-get {infisical_auth} as infisical_auth_resp
When I send a raw ACME request to "{infisical_auth}" And the value infisical_auth_resp with jq ".status" should be equal to "pending"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{infisical_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# the order should be pending as well # the order should be pending as well
And I memorize response.headers with jq ".["replay-nonce"]" as nonce And I post-as-get {order.uri} as order_resp
When I send a raw ACME request to "{order.uri}" And the value order_resp with jq ".status" should be equal to "pending"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{order.uri}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# finalize should not be allowed when all auths are not valid yet # finalize should not be allowed when all auths are not valid yet
And I memorize response.headers with jq ".["replay-nonce"]" as nonce And I get a new-nonce as nonce
When I send a raw ACME request to "{order.body.finalize}" When I send a raw ACME request to "{order.body.finalize}"
""" """
{ {
@@ -185,8 +169,10 @@ Feature: Challenge
Then the value response.status_code should be equal to 201 Then the value response.status_code should be equal to 201
And I memorize response with jq ".finalize" as finalize_url And I memorize response with jq ".finalize" as finalize_url
And I memorize response.headers with jq ".["replay-nonce"]" as nonce And I memorize response.headers with jq ".["replay-nonce"]" as nonce
And I memorize response.headers with jq ".["location"]" as order_uri
And I memorize response as order And I memorize response as order
And I pass all challenges with type http-01 for order in order And I pass all challenges with type http-01 for order in order
And I wait until the status of order order_uri becomes ready
And I encode CSR csr_pem as JOSE Base-64 DER as base64_csr_der And I encode CSR csr_pem as JOSE Base-64 DER as base64_csr_der
When I send a raw ACME request to "{finalize_url}" When I send a raw ACME request to "{finalize_url}"
""" """
+86 -6
View File
@@ -2,6 +2,8 @@ import json
import logging import logging
import re import re
import urllib.parse import urllib.parse
import time
import threading
import acme.client import acme.client
import jq import jq
@@ -724,6 +726,15 @@ def step_impl(context: Context, var_path: str, jq_query, var_name: str):
context.vars[var_name] = value context.vars[var_name] = value
@then("I get a new-nonce as {var_name}")
def step_impl(context: Context, var_name: str):
acme_client = context.acme_client
nonce = acme_client.net._get_nonce(
url=None, new_nonce_url=acme_client.directory.newNonce
)
context.vars[var_name] = json_util.encode_b64jose(nonce)
@then("I peak and memorize the next nonce as {var_name}") @then("I peak and memorize the next nonce as {var_name}")
def step_impl(context: Context, var_name: str): def step_impl(context: Context, var_name: str):
acme_client = context.acme_client acme_client = context.acme_client
@@ -797,22 +808,39 @@ def select_challenge(
return challenges[0] return challenges[0]
def serve_challenge( def serve_challenges(
context: Context, context: Context,
challenge: messages.ChallengeBody, challenges: list[messages.ChallengeBody],
wait_time: int | None = None,
): ):
if hasattr(context, "web_server"): if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close() context.web_server.shutdown_and_server_close()
resources = set()
for challenge in challenges:
response, validation = challenge.response_and_validation( response, validation = challenge.response_and_validation(
context.acme_client.net.key context.acme_client.net.key
) )
resource = standalone.HTTP01RequestHandler.HTTP01Resource( resources.add(
standalone.HTTP01RequestHandler.HTTP01Resource(
chall=challenge.chall, response=response, validation=validation chall=challenge.chall, response=response, validation=validation
) )
)
# TODO: make port configurable # TODO: make port configurable
servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource}) servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), resources)
if wait_time is None:
servers.serve_forever() servers.serve_forever()
else:
def wait_and_start():
logger.info("Waiting %s seconds before we start serving.", wait_time)
time.sleep(wait_time)
logger.info("Start server now")
servers.serve_forever()
thread = threading.Thread(target=wait_and_start)
thread.daemon = True
thread.start()
context.web_server = servers context.web_server = servers
@@ -865,6 +893,7 @@ def step_impl(
f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}" f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
) )
challenges = {}
for domain in order.body.identifiers: for domain in order.body.identifiers:
logger.info( logger.info(
"Selecting challenge for domain %s with type %s ...", "Selecting challenge for domain %s with type %s ...",
@@ -889,18 +918,28 @@ def step_impl(
domain.value, domain.value,
challenge_type, challenge_type,
) )
serve_challenge(context=context, challenge=challenge) challenges[domain] = challenge
serve_challenges(context=context, challenges=list(challenges.values()))
for domain, challenge in challenges.items():
logger.info( logger.info(
"Notifying challenge for domain %s with type %s ...", domain, challenge_type "Notifying challenge for domain %s with type %s ...", domain, challenge_type
) )
notify_challenge_ready(context=context, challenge=challenge) notify_challenge_ready(context=context, challenge=challenge)
@then(
"I wait {wait_time} seconds and serve challenge response for {var_path} at {hostname}"
)
def step_impl(context: Context, wait_time: str, var_path: str, hostname: str):
challenge = eval_var(context, var_path, as_json=False)
serve_challenges(context=context, challenges=[challenge], wait_time=int(wait_time))
@then("I serve challenge response for {var_path} at {hostname}") @then("I serve challenge response for {var_path} at {hostname}")
def step_impl(context: Context, var_path: str, hostname: str): def step_impl(context: Context, var_path: str, hostname: str):
challenge = eval_var(context, var_path, as_json=False) challenge = eval_var(context, var_path, as_json=False)
serve_challenge(context=context, challenge=challenge) serve_challenges(context=context, challenges=[challenge])
@then("I tell ACME server that {var_path} is ready to be verified") @then("I tell ACME server that {var_path} is ready to be verified")
@@ -909,6 +948,47 @@ def step_impl(context: Context, var_path: str):
notify_challenge_ready(context=context, challenge=challenge) notify_challenge_ready(context=context, challenge=challenge)
@then("I wait until the status of order {order_var} becomes {status}")
def step_impl(context: Context, order_var: str, status: str):
acme_client = context.acme_client
attempt_count = 6
while attempt_count:
order = eval_var(context, order_var, as_json=False)
response = acme_client._post_as_get(
order.uri if isinstance(order, messages.OrderResource) else order
)
order = messages.Order.from_json(response.json())
if order.status.name == status:
return
attempt_count -= 1
time.sleep(10)
raise TimeoutError(f"The status of order doesn't become {status} before timeout")
@then("I wait until the status of authorization {auth_var} becomes {status}")
def step_impl(context: Context, auth_var: str, status: str):
acme_client = context.acme_client
attempt_count = 6
while attempt_count:
auth = eval_var(context, auth_var, as_json=False)
response = acme_client._post_as_get(
auth.uri if isinstance(auth, messages.Authorization) else auth
)
auth = messages.Authorization.from_json(response.json())
if auth.status.name == status:
return
attempt_count -= 1
time.sleep(10)
raise TimeoutError(f"The status of auth doesn't become {status} before timeout")
@then("I post-as-get {uri} as {resp_var}")
def step_impl(context: Context, uri: str, resp_var: str):
acme_client = context.acme_client
response = acme_client._post_as_get(replace_vars(uri, vars=context.vars))
context.vars[resp_var] = response.json()
@then("I poll and finalize the ACME order {var_path} as {finalized_var}") @then("I poll and finalize the ACME order {var_path} as {finalized_var}")
def step_impl(context: Context, var_path: str, finalized_var: str): def step_impl(context: Context, var_path: str, finalized_var: str):
order = eval_var(context, var_path, as_json=False) order = eval_var(context, var_path, as_json=False)
@@ -1,5 +1,6 @@
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange"; import { isPrivateIp } from "@app/lib/ip/ipRange";
@@ -18,7 +19,11 @@ import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type TPkiAcmeChallengeServiceFactoryDep = { type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick< acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory, TPkiAcmeChallengeDALFactory,
"transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById" | "transaction"
| "findByIdForChallengeValidation"
| "markAsValidCascadeById"
| "markAsInvalidCascadeById"
| "updateById"
>; >;
}; };
@@ -26,9 +31,8 @@ export const pkiAcmeChallengeServiceFactory = ({
acmeChallengeDAL acmeChallengeDAL
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig(); const appCfg = getConfig();
const markChallengeAsReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
const validateChallengeResponse = async (challengeId: string): Promise<void> => { return acmeChallengeDAL.transaction(async (tx) => {
const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => {
logger.info({ challengeId }, "Validating ACME challenge response"); logger.info({ challengeId }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
if (!challenge) { if (!challenge) {
@@ -52,11 +56,27 @@ export const pkiAcmeChallengeServiceFactory = ({
if (challenge.type !== AcmeChallengeType.HTTP_01) { if (challenge.type !== AcmeChallengeType.HTTP_01) {
throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" });
} }
let host = challenge.auth.identifierValue; const host = challenge.auth.identifierValue;
// check if host is a private ip address // check if host is a private ip address
if (isPrivateIp(host)) { if (isPrivateIp(host)) {
throw new BadRequestError({ message: "Private IP addresses are not allowed" }); throw new BadRequestError({ message: "Private IP addresses are not allowed" });
} }
return acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Processing }, tx);
});
};
const validateChallengeResponse = async (challengeId: string, retryCount: number): Promise<void> => {
logger.info({ challengeId, retryCount }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId);
if (!challenge) {
throw new NotFoundError({ message: "ACME challenge not found" });
}
if (challenge.status !== AcmeChallengeStatus.Processing) {
throw new BadRequestError({
message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}`
});
}
let host = challenge.auth.identifierValue;
if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) {
host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]; host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host];
logger.warn( logger.warn(
@@ -91,10 +111,17 @@ export const pkiAcmeChallengeServiceFactory = ({
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) { if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
} }
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); logger.info({ challengeId }, "ACME challenge response is correct, marking challenge as valid");
await acmeChallengeDAL.markAsValidCascadeById(challengeId);
} catch (exp) { } catch (exp) {
// TODO: we should retry the challenge validation a few times, but let's keep it simple for now if (retryCount >= 2) {
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx); logger.error(
exp,
`Last attempt to validate ACME challenge response failed, marking ${challengeId} challenge as invalid`
);
// This is the last attempt to validate the challenge response, if it fails, we mark the challenge as invalid
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId);
}
// Properly type and inspect the error // Properly type and inspect the error
if (axios.isAxiosError(exp)) { if (axios.isAxiosError(exp)) {
const axiosError = exp as AxiosError; const axiosError = exp as AxiosError;
@@ -102,31 +129,29 @@ export const pkiAcmeChallengeServiceFactory = ({
const errorMessage = axiosError.message; const errorMessage = axiosError.message;
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) { if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
return new AcmeConnectionError({ message: "Connection refused" }); throw new AcmeConnectionError({ message: "Connection refused" });
} }
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) { if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) {
throw new AcmeConnectionError({ message: "Connection reset by peer" });
} }
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) { if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
logger.error(exp, "Connection timed out while validating ACME challenge response"); logger.error(exp, "Connection timed out while validating ACME challenge response");
return new AcmeConnectionError({ message: "Connection timed out" }); throw new AcmeConnectionError({ message: "Connection timed out" });
} }
logger.error(exp, "Unknown error validating ACME challenge response"); logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} }
if (exp instanceof Error) { if (exp instanceof Error) {
logger.error(exp, "Error validating ACME challenge response"); logger.error(exp, "Error validating ACME challenge response");
} else { throw exp;
}
logger.error(exp, "Unknown error validating ACME challenge response"); logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
return exp;
}
});
if (error) {
throw error;
} }
}; };
return { validateChallengeResponse }; return { markChallengeAsReady, validateChallengeResponse };
}; };
@@ -0,0 +1,67 @@
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type TPkiAcmeQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
acmeChallengeService: TPkiAcmeChallengeServiceFactory;
};
export type TPkiAcmeQueueServiceFactory = Awaited<ReturnType<typeof pkiAcmeQueueServiceFactory>>;
export const pkiAcmeQueueServiceFactory = async ({
queueService,
acmeChallengeService
}: TPkiAcmeQueueServiceFactoryDep) => {
const appCfg = getConfig();
// Initialize the worker to process challenge validation jobs
await queueService.startPg<QueueName.PkiAcmeChallengeValidation>(
QueueJobs.PkiAcmeChallengeValidation,
async ([job]) => {
const { challengeId } = job.data;
const retryCount = job.retryCount || 0;
try {
logger.info({ challengeId, retryCount }, "Processing ACME challenge validation job");
await acmeChallengeService.validateChallengeResponse(challengeId, retryCount);
logger.info({ challengeId, retryCount }, "ACME challenge validation completed successfully");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(
error,
`Failed to validate ACME challenge ${challengeId} (retryCount ${retryCount}): ${errorMessage}`
);
// Re-throw to let pg-boss handle retries with exponential backoff
throw error;
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
const queueChallengeValidation = async (challengeId: string): Promise<void> => {
if (appCfg.isSecondaryInstance) {
return;
}
logger.info({ challengeId }, "Queueing ACME challenge validation");
await queueService.queuePg(
QueueJobs.PkiAcmeChallengeValidation,
{ challengeId },
{
retryLimit: 3,
retryDelay: 30, // Base delay of 30 seconds
retryBackoff: true // Exponential backoff: 30s, 60s, 120s
}
);
};
return {
queueChallengeValidation
};
};
@@ -62,6 +62,7 @@ import {
import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns"; import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { TPkiAcmeQueueServiceFactory } from "./pki-acme-queue";
import { import {
AcmeAuthStatus, AcmeAuthStatus,
AcmeChallengeStatus, AcmeChallengeStatus,
@@ -126,7 +127,8 @@ type TPkiAcmeServiceFactoryDep = {
>; >;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">; certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeChallengeService: TPkiAcmeChallengeServiceFactory; acmeChallengeService: Pick<TPkiAcmeChallengeServiceFactory, "markChallengeAsReady">;
pkiAcmeQueueService: Pick<TPkiAcmeQueueServiceFactory, "queueChallengeValidation">;
}; };
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
@@ -147,7 +149,8 @@ export const pkiAcmeServiceFactory = ({
kmsService, kmsService,
licenseService, licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService acmeChallengeService,
pkiAcmeQueueService
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => { const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
@@ -975,7 +978,8 @@ export const pkiAcmeServiceFactory = ({
if (!result) { if (!result) {
throw new NotFoundError({ message: "ACME challenge not found" }); throw new NotFoundError({ message: "ACME challenge not found" });
} }
await acmeChallengeService.validateChallengeResponse(challengeId); await acmeChallengeService.markChallengeAsReady(challengeId);
await pkiAcmeQueueService.queueChallengeValidation(challengeId);
const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!; const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!;
return { return {
status: 200, status: 200,
@@ -1,6 +1,8 @@
import { JWSHeaderParameters } from "jose"; import { JWSHeaderParameters } from "jose";
import { z } from "zod"; import { z } from "zod";
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import { import {
AcmeOrderResourceSchema, AcmeOrderResourceSchema,
CreateAcmeAccountBodySchema, CreateAcmeAccountBodySchema,
@@ -176,5 +178,6 @@ export type TPkiAcmeServiceFactory = {
}; };
export type TPkiAcmeChallengeServiceFactory = { export type TPkiAcmeChallengeServiceFactory = {
validateChallengeResponse: (challengeId: string) => Promise<void>; markChallengeAsReady: (challengeId: string) => Promise<TPkiAcmeChallenges>;
validateChallengeResponse: (challengeId: string, retryCount: number) => Promise<void>;
}; };
+8 -2
View File
@@ -81,7 +81,8 @@ export enum QueueName {
UserNotification = "user-notification", UserNotification = "user-notification",
HealthAlert = "health-alert", HealthAlert = "health-alert",
CertificateV3AutoRenewal = "certificate-v3-auto-renewal", CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
PamAccountRotation = "pam-account-rotation" PamAccountRotation = "pam-account-rotation",
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -134,7 +135,8 @@ export enum QueueJobs {
UserNotification = "user-notification-job", UserNotification = "user-notification-job",
HealthAlert = "health-alert", HealthAlert = "health-alert",
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
PamAccountRotation = "pam-account-rotation" PamAccountRotation = "pam-account-rotation",
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -385,6 +387,10 @@ export type TQueueJobTypes = {
name: QueueJobs.PamAccountRotation; name: QueueJobs.PamAccountRotation;
payload: undefined; payload: undefined;
}; };
[QueueName.PkiAcmeChallengeValidation]: {
name: QueueJobs.PkiAcmeChallengeValidation;
payload: { challengeId: string };
};
}; };
const SECRET_SCANNING_JOBS = [ const SECRET_SCANNING_JOBS = [
+9 -1
View File
@@ -81,6 +81,7 @@ import { pkiAcmeChallengeDALFactory } from "@app/ee/services/pki-acme/pki-acme-c
import { pkiAcmeChallengeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-challenge-service"; import { pkiAcmeChallengeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-challenge-service";
import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal"; import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal";
import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal";
import { pkiAcmeQueueServiceFactory } from "@app/ee/services/pki-acme/pki-acme-queue";
import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service";
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
@@ -2254,6 +2255,12 @@ export const registerRoutes = async (
const acmeChallengeService = pkiAcmeChallengeServiceFactory({ const acmeChallengeService = pkiAcmeChallengeServiceFactory({
acmeChallengeDAL acmeChallengeDAL
}); });
const pkiAcmeQueueService = await pkiAcmeQueueServiceFactory({
queueService,
acmeChallengeService
});
const pkiAcmeService = pkiAcmeServiceFactory({ const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL, projectDAL,
appConnectionDAL, appConnectionDAL,
@@ -2272,7 +2279,8 @@ export const registerRoutes = async (
kmsService, kmsService,
licenseService, licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService acmeChallengeService,
pkiAcmeQueueService
}); });
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({