ReviewOS

also looking at this

stacks/ts-cloud

fix(tls): wait for DNS before ACME issuance

#166
Merged chrisbbreuer wants to merge agent/wait-for-rpx-dns into main
4 files +42 -4
packages/ts-cloud/src/drivers/shared/compute-deploy.tsmodified+14-2
Changes to packages/ts-cloud/src/drivers/shared/compute-deploy.ts
@@ -16,7 +16,7 @@ import { buildNginxVhostScript, resolveNginxSnippet } from './nginx-vhost'
1616import { resolveNotifications, sendNotifications } from './notifications'
1717import { buildPhpFpmPoolScript, phpFpmPoolListen } from './php-fpm-pool'
1818import { buildDeployHistoryHeader, buildSiteOwnerGuard } from './releases'
19import { buildRpxConfig, buildRpxFragmentRefreshScript, buildRpxLbConfig, buildRpxProvisionScript, rpxCertRenewServiceName, usesRpxProxy } from './rpx-gateway'
19import { buildRpxConfig, buildRpxFragmentRefreshScript, buildRpxLbConfig, buildRpxProvisionScript, certDomainsForConfig, rpxCertRenewServiceName, usesRpxProxy } from './rpx-gateway'
2020
2121export interface ComputeDeployLogger {
2222 info(message: string): void
@@ -34,6 +34,10 @@ const noopLogger: ComputeDeployLogger = {
3434 success: () => {},
3535}
3636
37function shellQuote(value: string): string {
38 return `'${value.replace(/'/g, `'"'"'`)}'`
39}
40
3741/**
3842 * Deploy a single site release tarball to compute targets via the active driver.
3943 */
@@ -812,6 +816,8 @@ export async function renewRpxCertificates(
812816
813817 const rpxConfig = buildRpxConfig(routeSource.sites || {}, { proxy, slug: config.project.slug })
814818 if (rpxConfig.proxies.length === 0) return true
819 const certDomains = certDomainsForConfig(rpxConfig)
820 if (certDomains.length === 0) return true
815821
816822 const slug = config.project.slug
817823 const stackName = resolveProjectStackName(config, environment)
@@ -827,9 +833,15 @@ export async function renewRpxCertificates(
827833 }
828834
829835 logger.step('Issuing rpx TLS certificates after DNS reconciliation...')
836 const certChecks = certDomains
837 .map(domain => `test -s ${shellQuote(`${rpxConfig.productionCerts.certsDir}/${domain}.crt`)}`)
838 .join(' && ')
830839 const result = await driver.runRemoteDeploy({
831840 targets,
832 commands: [`systemctl start ${rpxCertRenewServiceName(slug)}`],
841 commands: [
842 `systemctl start ${rpxCertRenewServiceName(slug)}`,
843 certChecks,
844 ],
833845 comment: `ts-cloud rpx TLS issuance ${slug}`,
834846 tags: {
835847 Project: slug,
packages/ts-cloud/src/drivers/shared/rpx-gateway.tsmodified+20-1
Changes to packages/ts-cloud/src/drivers/shared/rpx-gateway.ts
@@ -746,10 +746,29 @@ export function buildCertManagementCommands(options: BuildRpxProvisionOptions):
746746 `EMAIL='${email}'`,
747747 `TLSX="${tlsxCli}"`,
748748 `DOMAINS='${csv}'`,
749 "DNS_ATTEMPTS='24'",
750 "DNS_DELAY_SECONDS='5'",
751 'wait_for_dns() {',
752 ' d="$1"',
753 ' attempt=1',
754 ' while ! getent ahosts "$d" >/dev/null 2>&1; do',
755 ' if [ "$attempt" -ge "$DNS_ATTEMPTS" ]; then',
756 ' echo "DNS for $d did not become resolvable after $DNS_ATTEMPTS attempts" >&2',
757 ' return 1',
758 ' fi',
759 ' echo "Waiting for public DNS before ACME: $d (attempt $attempt/$DNS_ATTEMPTS)"',
760 ' sleep "$DNS_DELAY_SECONDS"',
761 ' attempt=$((attempt + 1))',
762 ' done',
763 '}',
749764 'before=$(cat "$CERTS"/*.crt 2>/dev/null | sha256sum)',
750765 `for d in ${spaced}; do`,
751766 ' if [ ! -s "$CERTS/$d.crt" ]; then',
752 ' $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
767 ' if wait_for_dns "$d"; then',
768 ' $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
769 ' else',
770 ' echo "issue $d skipped until DNS resolves (non-fatal)"',
771 ' fi',
753772 ' # rpx reloads its SNI set when a PEM appears. Complete that reload',
754773 ' # before the next hostname starts http-01, or :80 can disappear in',
755774 ' # the middle of the following challenge on a multi-domain deploy.',
packages/ts-cloud/test/drivers/compute-deploy.test.tsmodified+4-1
Changes to packages/ts-cloud/test/drivers/compute-deploy.test.ts
@@ -547,7 +547,10 @@ describe('renewRpxCertificates', () => {
547547 expect(ok).toBe(true)
548548 const call = (driver.runRemoteDeploy as ReturnType<typeof mock>).mock.calls[0][0]
549549 expect(call.targets[0].id).toBe('i-abc123')
550 expect(call.commands).toEqual(['systemctl start rpx-cert-renew-my-app.service'])
550 expect(call.commands).toEqual([
551 'systemctl start rpx-cert-renew-my-app.service',
552 "test -s '/etc/rpx/certs/my-app.example.com.crt'",
553 ])
551554 expect(call.tags.Role).toBe('app')
552555 })
553556
packages/ts-cloud/test/drivers/rpx-gateway.test.tsmodified+4-0
Changes to packages/ts-cloud/test/drivers/rpx-gateway.test.ts
@@ -768,6 +768,10 @@ describe('managed TLS (acmeChallengeWebroot + cert renewal)', () => {
768768 expect(joined).toContain('acme:renew')
769769 expect(joined).toContain('--webroot')
770770 expect(joined).toContain(DEFAULT_ACME_WEBROOT)
771 expect(joined).toContain('while ! getent ahosts "$d"')
772 expect(joined).toContain("DNS_ATTEMPTS='24'")
773 expect(joined).toContain("DNS_DELAY_SECONDS='5'")
774 expect(joined.indexOf('wait_for_dns "$d"')).toBeLessThan(joined.indexOf('$TLSX acme:issue'))
771775 expect(joined).toContain(`[ -s "$CERTS/$d.crt" ] && systemctl restart ${RPX_SERVICE_NAME}`)
772776 expect(joined.indexOf(`systemctl restart ${RPX_SERVICE_NAME}`)).toBeLessThan(joined.indexOf('acme:renew'))
773777 // Per-app renewal units (slug defaults to 'app').