Skip dashboard SSL hosts without DNS and shorten sync errors.
Dashboard cert renewal no longer fails when unrelated tenants lack business/customer DNS records; Issue SSL errors summarize blocked hostnames instead of dumping certbot logs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ead16080b8
commit
b8f60c5141
@@ -0,0 +1,43 @@
|
|||||||
|
import { lookup } from 'node:dns/promises';
|
||||||
|
|
||||||
|
/** True when the hostname has at least one A or AAAA record. */
|
||||||
|
export async function hostDnsResolves(hostRaw: string): Promise<boolean> {
|
||||||
|
const host = hostRaw.trim().toLowerCase();
|
||||||
|
if (!host || host.includes(':') || host.endsWith('.local')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await lookup(host);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep only hosts that resolve in public DNS (parallel, chunked). */
|
||||||
|
export async function filterHostsWithDns(
|
||||||
|
hosts: Array<string | null | undefined>,
|
||||||
|
chunkSize = 20,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const unique = [
|
||||||
|
...new Set(
|
||||||
|
hosts
|
||||||
|
.map((h) => h?.trim().toLowerCase())
|
||||||
|
.filter((h): h is string => !!h),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const resolved: string[] = [];
|
||||||
|
for (let i = 0; i < unique.length; i += chunkSize) {
|
||||||
|
const chunk = unique.slice(i, i + chunkSize);
|
||||||
|
const probed = await Promise.all(
|
||||||
|
chunk.map(async (host) => ({ host, ok: await hostDnsResolves(host) })),
|
||||||
|
);
|
||||||
|
for (const { host, ok } of probed) {
|
||||||
|
if (ok) resolved.push(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved.sort();
|
||||||
|
}
|
||||||
@@ -131,13 +131,34 @@ export class DomainAdminService {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text().catch(() => '');
|
const text = await response.text().catch(() => '');
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
`SSL sync agent rejected request (${response.status})${text ? `: ${text}` : ''}`,
|
`SSL sync agent rejected request (${response.status})${text ? `: ${this.summarizeSslSyncFailure(text)}` : ''}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json().catch(() => ({ status: opts.wait ? 'ok' : 'accepted' }));
|
return response.json().catch(() => ({ status: opts.wait ? 'ok' : 'accepted' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private summarizeSslSyncFailure(raw: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as { message?: string; log?: string };
|
||||||
|
const log = String(parsed.log ?? '');
|
||||||
|
const blocked = [
|
||||||
|
...new Set(
|
||||||
|
[...log.matchAll(/Domain: ((?:business|customer)\.[^\n]+)/g)].map((m) => m[1].trim()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (blocked.length > 0) {
|
||||||
|
return (
|
||||||
|
`Dashboard cert blocked by missing DNS for: ${blocked.join(', ')}. ` +
|
||||||
|
'Add business/customer CNAMEs for those tenants or deactivate the domain, then retry.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return String(parsed.message ?? raw).slice(0, 400);
|
||||||
|
} catch {
|
||||||
|
return raw.slice(0, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue Let's Encrypt certs on the websites VM for storefront domains
|
* Issue Let's Encrypt certs on the websites VM for storefront domains
|
||||||
* whose live TLS probe fails. Do not trust ssl_enabled alone — a wrong
|
* whose live TLS probe fails. Do not trust ssl_enabled alone — a wrong
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { filterHostsWithDns } from '../common/dns-resolve';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { probeTlsHosts } from '../common/tls-probe';
|
import { probeTlsHosts } from '../common/tls-probe';
|
||||||
|
|
||||||
@@ -39,15 +40,26 @@ export class InternalSslService implements OnModuleInit {
|
|||||||
return hosts;
|
return hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dashboards VPS: manage + business./customer. per active apex. */
|
/** Dashboards VPS: manage + business./customer. per active apex (DNS-resolvable only). */
|
||||||
async listDashboardHosts(): Promise<{ hosts: string[] }> {
|
async listDashboardHosts(): Promise<{ hosts: string[] }> {
|
||||||
const adminHost =
|
const adminHost =
|
||||||
this.config.get<string>('DASHBOARD_ADMIN_HOST')?.trim() || 'manage.meshkee.com';
|
this.config.get<string>('DASHBOARD_ADMIN_HOST')?.trim() || 'manage.meshkee.com';
|
||||||
|
|
||||||
const hosts = new Set<string>([adminHost]);
|
const candidates = new Set<string>([adminHost]);
|
||||||
for (const apex of await this.listActiveApexHosts()) {
|
for (const apex of await this.listActiveApexHosts()) {
|
||||||
hosts.add(`business.${apex}`);
|
candidates.add(`business.${apex}`);
|
||||||
hosts.add(`customer.${apex}`);
|
candidates.add(`customer.${apex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await filterHostsWithDns([...candidates]);
|
||||||
|
const hosts = new Set<string>(resolved);
|
||||||
|
hosts.add(adminHost);
|
||||||
|
|
||||||
|
const skipped = [...candidates].filter((h) => !hosts.has(h));
|
||||||
|
if (skipped.length > 0) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Dashboard SSL host list skipped ${skipped.length} host(s) without DNS: ${skipped.join(', ')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { hosts: [...hosts].sort() };
|
return { hosts: [...hosts].sort() };
|
||||||
|
|||||||
Reference in New Issue
Block a user