Skip to content

Commit a4eca35

Browse files
Poll the challenge endpoint to make sure it's ready before requesting ACME challenge
1 parent b50fcd0 commit a4eca35

3 files changed

Lines changed: 33 additions & 1 deletion

File tree

backend/src/handlers/acmeChallenge.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const handleAcmeChallengeHandler: HandlerMap["acmeChallenge"] = async (
1010
req,
1111
res,
1212
) => {
13+
if (ctx.request.query.probe) {
14+
// This request is coming from AnvilOps in backend/src/service/verifyDomain.ts to make sure the challenge is ready before telling Let's Encrypt to complete it.
15+
return unsafeGenericResponse(res.send("200 OK").end());
16+
}
1317
try {
1418
const keyAuthorization = await certGenerationService.handleAcmeChallenge(
1519
ctx.request.params.token,

backend/src/service/certGeneration.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,20 +141,42 @@ export class CertGenerationService {
141141
);
142142
}
143143
const challenge = http01Challenges[0];
144+
const keyAuth = await challenge.keyAuthorization();
144145

145146
await this.domainRepo.updateCertOrderDetails(
146147
domainId,
147148
challenge.token,
148-
await challenge.keyAuthorization(),
149+
keyAuth,
149150
order.url,
150151
);
151152

153+
// Wait for the challenge to be ready (sometimes, the ingress controller needs time to update its routes)
154+
await this.pollChallengeEndpoint(domain.name, challenge.token);
155+
152156
// Tell the CA that we're ready to run our HTTP challenge
153157
await challenge.submit();
154158

155159
// The rest of the process continues in handleAcmeChallenge, then in finalizeCertOrder
156160
}
157161

162+
private async pollChallengeEndpoint(domain: string, token: string) {
163+
const deadline = new Date().getTime() + 60 * 1000; // 1 minute from now
164+
while (new Date().getTime() < deadline) {
165+
// eslint-disable-next-line no-await-in-loop
166+
const response = await fetch(
167+
`http://${domain}/.well-known/acme-challenge/${token}?probe=true`,
168+
);
169+
if (response.ok) {
170+
return;
171+
}
172+
}
173+
174+
logger.warn(
175+
{ domain },
176+
"Timed out waiting for domain's ACME challenge endpoint to become ready",
177+
);
178+
}
179+
158180
async handleAcmeChallenge(token: string) {
159181
const domain = await this.domainRepo.getByToken(token);
160182
if (!domain) {

openapi/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ paths:
2424
description: A random string that uniquely identifies the challenge
2525
schema:
2626
type: string
27+
- name: probe
28+
in: query
29+
required: false
30+
description: If this parameter is set, then the request is coming from AnvilOps while it's waiting for the Ingress configuration to update. These "probe" requests shouldn't trigger the next step of the certificate generation flow.
31+
schema:
32+
type: boolean
2733
responses:
2834
"200":
2935
description: Success

0 commit comments

Comments
 (0)