Skip to content

Commit c547eaa

Browse files
committed
Better domain verification
- deletes certificates, if any, when a domain is in HOLD - better initial domain structure - also deletes related jobs on domain deletion - fix ACM validation values gathering - put domain in HOLD after 3 consecutive critical errors - independent verification steps - fix typo on domain update via domain verification
1 parent 9eb8d96 commit c547eaa

File tree

4 files changed

+73
-20
lines changed

4 files changed

+73
-20
lines changed

api/acm/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ export async function getCertificateStatus (certificateArn) {
4141
const certificate = await describeCertificate(certificateArn)
4242
return certificate.Certificate.Status
4343
}
44+
45+
export async function deleteCertificate (certificateArn) {
46+
if (process.env.NODE_ENV === 'development') {
47+
config.endpoint = process.env.LOCALSTACK_ENDPOINT
48+
}
49+
const acm = new AWS.ACM(config)
50+
const result = await acm.deleteCertificate({ CertificateArn: certificateArn }).promise()
51+
console.log(`delete certificate attempt for ${certificateArn}, result: ${JSON.stringify(result)}`)
52+
return result
53+
}

api/resolvers/domain.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,25 @@ export default {
3333
throw new GqlInputError('invalid domain format')
3434
}
3535

36+
const existing = await models.customDomain.findUnique({ where: { subName } })
37+
3638
if (domain) {
37-
const existing = await models.customDomain.findUnique({ where: { subName } })
3839
if (existing && existing.domain === domain && existing.status !== 'HOLD') {
3940
throw new GqlInputError('domain already set')
4041
}
4142

4243
const initializeDomain = {
4344
domain,
45+
createdAt: new Date(),
4446
status: 'PENDING',
4547
verification: {
4648
dns: {
4749
state: 'PENDING',
4850
cname: 'stacker.news',
4951
// generate a random txt record only if it's a new domain
50-
txt: existing?.domain === domain ? existing.verification.dns.txt : randomBytes(32).toString('base64')
52+
txt: existing?.domain === domain && existing.verification.dns.txt
53+
? existing.verification.dns.txt
54+
: randomBytes(32).toString('base64')
5155
},
5256
ssl: {
5357
state: 'WAITING',
@@ -72,15 +76,24 @@ export default {
7276
})
7377

7478
// schedule domain verification in 30 seconds
75-
await models.$executeRaw`INSERT INTO pgboss.job (name, data, startafter, keepuntil)
76-
VALUES ('domainVerification',
77-
jsonb_build_object('domainId', ${updatedDomain.id}::INTEGER),
78-
now() + interval '30 seconds',
79-
now() + interval '2 days')`
79+
await models.$executeRaw`
80+
INSERT INTO pgboss.job (name, data, retrylimit, retrydelay, startafter, keepuntil)
81+
VALUES ('domainVerification',
82+
jsonb_build_object('domainId', ${updatedDomain.id}::INTEGER),
83+
3,
84+
30,
85+
now() + interval '30 seconds',
86+
now() + interval '2 days')`
8087

8188
return updatedDomain
8289
} else {
8390
try {
91+
// delete any existing domain verification jobs
92+
await models.$queryRaw`
93+
DELETE FROM pgboss.job
94+
WHERE name = 'domainVerification'
95+
AND data->>'domainId' = ${existing.id}::TEXT`
96+
8497
return await models.customDomain.delete({ where: { subName } })
8598
} catch (error) {
8699
console.error(error)

lib/domain-verification.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ export async function certDetails (certificateArn) {
5151
// TODO: Test with real values, localstack don't have this info until the certificate is issued
5252
export async function getValidationValues (certificateArn) {
5353
const certificate = await certDetails(certificateArn)
54-
console.log(certificate.DomainValidationOptions)
54+
if (!certificate || !certificate.Certificate || !certificate.Certificate.DomainValidationOptions) {
55+
return { cname: null, value: null }
56+
}
57+
58+
console.log(certificate.Certificate.DomainValidationOptions)
5559
return {
56-
cname: certificate.DomainValidationOptions[0].ResourceRecord.Name,
57-
value: certificate.DomainValidationOptions[0].ResourceRecord.Value
60+
cname: certificate.Certificate.DomainValidationOptions[0]?.ResourceRecord?.Name || null,
61+
value: certificate.Certificate.DomainValidationOptions[0]?.ResourceRecord?.Value || null
5862
}
5963
}
6064

worker/domainVerification.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import createPrisma from '@/lib/create-prisma'
2-
import { verifyDomainDNS, issueDomainCertificate, checkCertificateStatus, getValidationValues } from '@/lib/domain-verification'
2+
import { verifyDomainDNS, issueDomainCertificate, checkCertificateStatus, getValidationValues, deleteCertificate } from '@/lib/domain-verification'
33

4-
export async function domainVerification ({ data: { domainId }, boss }) {
4+
export async function domainVerification ({ id: jobId, data: { domainId }, boss }) {
55
// establish connection to database
66
const models = createPrisma({ connectionParams: { connection_limit: 1 } })
77
try {
@@ -13,25 +13,44 @@ export async function domainVerification ({ data: { domainId }, boss }) {
1313
}
1414

1515
// start verification process
16-
const result = await verifyDomain(domain, boss)
16+
const result = await verifyDomain(domain)
17+
console.log(`domain verification result: ${JSON.stringify(result)}`)
1718

1819
// update the domain with the result
1920
await models.customDomain.update({
2021
where: { id: domainId },
21-
data: { ...domain, ...result }
22+
data: result
2223
})
2324

2425
// if the result is PENDING it means we still have to verify the domain
2526
// if it's not PENDING, we stop the verification process.
2627
if (result.status === 'PENDING') {
2728
// we still need to verify the domain, schedule the job to run again in 5 minutes
2829
const jobId = await boss.send('domainVerification', { domainId }, {
29-
startAfter: 1000 * 60 * 5 // start the job after 5 minutes
30+
startAfter: 60 * 5, // start the job after 5 minutes
31+
retryLimit: 3,
32+
retryDelay: 30 // on critical errors, retry every 5 minutes
3033
})
3134
console.log(`domain ${domain.domain} is still pending verification, created job with ID ${jobId} to run in 5 minutes`)
3235
}
3336
} catch (error) {
3437
console.error(`couldn't verify domain with ID ${domainId}: ${error.message}`)
38+
39+
// get the job details to get the retry count
40+
const jobDetails = await boss.getJobById(jobId)
41+
console.log(`job details: ${JSON.stringify(jobDetails)}`)
42+
// if we couldn't verify the domain, put it on hold if it exists and delete any related verification jobs
43+
if (jobDetails?.retrycount >= 3) {
44+
console.log(`couldn't verify domain with ID ${domainId} for the third time, putting it on hold if it exists and deleting any related domain verification jobs`)
45+
await models.customDomain.update({ where: { id: domainId }, data: { status: 'HOLD' } })
46+
// delete any related domain verification jobs
47+
await models.$queryRaw`
48+
DELETE FROM pgboss.job
49+
WHERE name = 'domainVerification'
50+
AND data->>'domainId' = ${domainId}::TEXT`
51+
}
52+
53+
throw error
3554
}
3655
}
3756

@@ -67,7 +86,7 @@ async function verifyDomain (domain) {
6786
if (dnsState === 'VERIFIED' && !sslArn) {
6887
sslArn = await issueDomainCertificate(domain.domain)
6988
if (sslArn) {
70-
console.log(`SSL certificate issued for ${domain.domain} with ARN ${sslArn}, will verify with ACM in 5 minutes`)
89+
console.log(`SSL certificate issued for ${domain.domain} with ARN ${sslArn}, will verify with ACM`)
7190
} else {
7291
console.log(`couldn't issue SSL certificate for ${domain.domain}, will retry certificate issuance in 5 minutes`)
7392
}
@@ -78,9 +97,9 @@ async function verifyDomain (domain) {
7897
let acmValidationCname = verification.ssl.cname || null
7998
let acmValidationValue = verification.ssl.value || null
8099
if (sslArn && !acmValidationCname && !acmValidationValue) {
81-
const { cname, value } = await getValidationValues(sslArn)
82-
acmValidationCname = cname
83-
acmValidationValue = value
100+
const values = await getValidationValues(sslArn)
101+
acmValidationCname = values?.cname || null
102+
acmValidationValue = values?.value || null
84103
if (acmValidationCname && acmValidationValue) {
85104
console.log(`Validation values retrieved for ${domain.domain}, will check ACM validation status`)
86105
} else {
@@ -91,7 +110,7 @@ async function verifyDomain (domain) {
91110
// step 4: check if the certificate is validated by ACM
92111
// if DNS is verified and we have a SSL certificate
93112
// it can happen that we just issued the certificate and it's not yet validated by ACM
94-
if (dnsState === 'VERIFIED' && sslArn && sslState !== 'VERIFIED') {
113+
if (sslArn && sslState !== 'VERIFIED') {
95114
sslState = await checkCertificateStatus(sslArn)
96115
switch (sslState) {
97116
case 'VERIFIED':
@@ -114,13 +133,20 @@ async function verifyDomain (domain) {
114133
// if the domain has failed in some way and it's been 48 hours, put it on hold
115134
if (status !== 'ACTIVE' && domain.createdAt < new Date(Date.now() - 1000 * 60 * 60 * 24 * 2)) {
116135
status = 'HOLD'
136+
// we stopped domain verification, delete the certificate as it will expire after 72 hours anyway
137+
if (sslArn) {
138+
console.log(`domain ${domain.domain} is on hold, deleting certificate as it will expire after 72 hours`)
139+
const result = await deleteCertificate(sslArn)
140+
console.log(`delete certificate attempt for ${domain.domain}, result: ${JSON.stringify(result)}`)
141+
}
117142
}
118143

119144
return {
120145
lastVerifiedAt,
121146
status,
122147
verification: {
123148
dns: {
149+
...verification.dns,
124150
state: dnsState
125151
},
126152
ssl: {

0 commit comments

Comments
 (0)