feat(acme): add DB-backed ACME configuration management and OpsServer certificate settings UI
This commit is contained in:
@@ -28,6 +28,7 @@ import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, Targe
|
||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
||||
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||
import { DnsManager } from './dns/manager.dns.js';
|
||||
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||
@@ -276,6 +277,9 @@ export class DcRouter {
|
||||
// Domain / DNS management (DB-backed providers, domains, records)
|
||||
public dnsManager?: DnsManager;
|
||||
|
||||
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
|
||||
public acmeConfigManager?: AcmeConfigManager;
|
||||
|
||||
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
||||
public detectedPublicIp: string | null = null;
|
||||
|
||||
@@ -412,11 +416,35 @@ export class DcRouter {
|
||||
);
|
||||
}
|
||||
|
||||
// SmartProxy: critical, depends on DcRouterDb + DnsManager (if enabled)
|
||||
// AcmeConfigManager: optional, depends on DcRouterDb — owns the singleton
|
||||
// ACME configuration (accountEmail, useProduction, etc.). Must run before
|
||||
// SmartProxy so setupSmartProxy() can read the ACME config from the DB.
|
||||
// On first boot, seeds from legacy `tls.contactEmail` / `smartProxyConfig.acme`.
|
||||
if (this.options.dbConfig?.enabled !== false) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('AcmeConfigManager')
|
||||
.optional()
|
||||
.dependsOn('DcRouterDb')
|
||||
.withStart(async () => {
|
||||
this.acmeConfigManager = new AcmeConfigManager(this.options);
|
||||
await this.acmeConfigManager.start();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.acmeConfigManager) {
|
||||
await this.acmeConfigManager.stop();
|
||||
this.acmeConfigManager = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 1, baseDelayMs: 500 }),
|
||||
);
|
||||
}
|
||||
|
||||
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
|
||||
const smartProxyDeps: string[] = [];
|
||||
if (this.options.dbConfig?.enabled !== false) {
|
||||
smartProxyDeps.push('DcRouterDb');
|
||||
smartProxyDeps.push('DnsManager');
|
||||
smartProxyDeps.push('AcmeConfigManager');
|
||||
}
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('SmartProxy')
|
||||
@@ -837,45 +865,62 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
||||
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
||||
|
||||
// If user provides full SmartProxy config, use it directly
|
||||
|
||||
// If user provides full SmartProxy config, use its routes.
|
||||
// NOTE: `smartProxyConfig.acme` is now seed-only — consumed by
|
||||
// AcmeConfigManager on first boot. The live ACME config always comes
|
||||
// from the DB via `this.acmeConfigManager.getConfig()`.
|
||||
if (this.options.smartProxyConfig) {
|
||||
routes = this.options.smartProxyConfig.routes || [];
|
||||
acmeConfig = this.options.smartProxyConfig.acme;
|
||||
logger.log('info', `Found ${routes.length} routes in config, ACME config present: ${!!acmeConfig}`);
|
||||
logger.log('info', `Found ${routes.length} routes in config`);
|
||||
}
|
||||
|
||||
|
||||
// If email config exists, automatically add email routes
|
||||
if (this.options.emailConfig) {
|
||||
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
||||
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(emailRoutes) });
|
||||
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
||||
}
|
||||
|
||||
|
||||
// If DNS is configured, add DNS routes
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
||||
const dnsRoutes = this.generateDnsRoutes();
|
||||
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(dnsRoutes) });
|
||||
routes = [...routes, ...dnsRoutes];
|
||||
}
|
||||
|
||||
// Merge TLS/ACME configuration if provided at root level
|
||||
if (this.options.tls && !acmeConfig) {
|
||||
acmeConfig = {
|
||||
accountEmail: this.options.tls.contactEmail,
|
||||
enabled: true,
|
||||
useProduction: true,
|
||||
autoRenew: true,
|
||||
renewThresholdDays: 30
|
||||
};
|
||||
|
||||
// Build the ACME options for SmartProxy from the DB-backed AcmeConfigManager.
|
||||
// If no config exists or it's disabled, SmartProxy's own ACME is turned off
|
||||
// and dcrouter's SmartAcme / certProvisionFunction are not wired.
|
||||
const dbAcme = this.acmeConfigManager?.getConfig();
|
||||
const acmeConfig: plugins.smartproxy.IAcmeOptions | undefined =
|
||||
dbAcme && dbAcme.enabled
|
||||
? {
|
||||
accountEmail: dbAcme.accountEmail,
|
||||
enabled: true,
|
||||
useProduction: dbAcme.useProduction,
|
||||
autoRenew: dbAcme.autoRenew,
|
||||
renewThresholdDays: dbAcme.renewThresholdDays,
|
||||
}
|
||||
: undefined;
|
||||
if (acmeConfig) {
|
||||
logger.log(
|
||||
'info',
|
||||
`ACME config: accountEmail=${acmeConfig.accountEmail}, useProduction=${acmeConfig.useProduction}, autoRenew=${acmeConfig.autoRenew}`,
|
||||
);
|
||||
} else {
|
||||
logger.log('info', 'ACME config: disabled or not yet configured in DB');
|
||||
}
|
||||
|
||||
// Configure DNS-01 challenge if any DnsProviderDoc exists in the DB.
|
||||
// The DnsManager dispatches each challenge to the right provider client
|
||||
// based on the FQDN being certificated.
|
||||
|
||||
// Configure DNS-01 challenge if any DnsProviderDoc exists in the DB AND
|
||||
// ACME is enabled. The DnsManager dispatches each challenge to the right
|
||||
// provider client based on the FQDN being certificated.
|
||||
let challengeHandlers: any[] = [];
|
||||
if (this.dnsManager && (await this.dnsManager.hasAcmeCapableProvider())) {
|
||||
if (
|
||||
acmeConfig &&
|
||||
this.dnsManager &&
|
||||
(await this.dnsManager.hasAcmeCapableProvider())
|
||||
) {
|
||||
logger.log('info', 'Configuring DNS-01 challenge for ACME via DnsManager (DB providers)');
|
||||
const convenientDnsProvider = this.dnsManager.buildAcmeConvenientDnsProvider();
|
||||
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(convenientDnsProvider);
|
||||
@@ -977,10 +1022,12 @@ export class DcRouter {
|
||||
logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
|
||||
);
|
||||
}
|
||||
// Safe non-null: challengeHandlers.length > 0 implies both dnsManager
|
||||
// and acmeConfig exist (enforced above).
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||
accountEmail: dbAcme!.accountEmail,
|
||||
certManager: new StorageBackedCertManager(),
|
||||
environment: 'production',
|
||||
environment: dbAcme!.useProduction ? 'production' : 'integration',
|
||||
challengeHandlers: challengeHandlers,
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user