reverseproxy: simplify certificate renewal

An issue was that mail container was not getting refreshed with the up to
date certs. The root cause is that it is refreshed only in the renewCerts()
cron job. If cert renewal was caused by an app task, then the cron job will
skip the restart (since cert is fresh).

The other issue is that we keep hitting 0 length certs when we run out of disk
space. The root cause is that when out of disk space, a cert renewal will
cause cert to be written but since it has no space it is 0 length. Then, when
the user tries to restart the server, the box code does not write the cert again.

This change fixes the above two including:
* To simplify, we use the fallback cert only if we failed to get a LE cert. Expired LE certs
  will continue to be used. nginx is fine with this.

* restart directory as well on renewal
This commit is contained in:
Girish Ramakrishnan
2022-11-11 18:09:10 +01:00
parent f917eb8f13
commit 9c8f78a059
8 changed files with 216 additions and 162 deletions

View File

@@ -26,7 +26,7 @@ exports = module.exports = {
restoreFallbackCertificates,
// exported for testing
_getAcmeApi: getAcmeApi
_getAcmeApiOptions: getAcmeApiOptions
};
const acme2 = require('./acme2.js'),
@@ -37,6 +37,7 @@ const acme2 = require('./acme2.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
directoryServer = require('./directoryserver.js'),
dns = require('./dns.js'),
domains = require('./domains.js'),
ejs = require('ejs'),
@@ -63,7 +64,7 @@ function nginxLocation(s) {
return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
}
async function getAcmeApi(domainObject) {
async function getAcmeApiOptions(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
const apiOptions = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
@@ -78,7 +79,7 @@ async function getAcmeApi(domainObject) {
const [error, owner] = await safe(users.getOwner());
apiOptions.email = (error || !owner) ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
return { acme2, apiOptions };
return apiOptions;
}
function getExpiryDate(certFilePath) {
@@ -255,89 +256,102 @@ function getUserCertificatePathSync(fqdn) {
return { certFilePath, keyFilePath };
}
function getAcmeCertificateName(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
if (fqdn !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
return dns.makeWildcard(fqdn).replace('*.', '_.');
} else {
return fqdn;
}
}
function getAcmeCertificatePathSync(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
let certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir = paths.ACME_CHALLENGES_DIR;
const certName = getAcmeCertificateName(fqdn, domainObject);
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`);
const csrFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.csr`);
if (fqdn !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
certName = dns.makeWildcard(fqdn).replace('*.', '_.');
certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`);
keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`);
csrFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.csr`);
} else {
certName = fqdn;
certFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.cert`);
keyFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.key`);
csrFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.csr`);
}
return { certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir };
return { certFilePath, keyFilePath, csrFilePath };
}
async function getCertificatePath(fqdn, domain) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
// 1. user cert always wins
// 2. if using fallback provider, return that cert
// 3. look for LE certs
const domainObject = await domains.get(domain);
const userPath = getUserCertificatePathSync(fqdn); // user cert always wins
if (fs.existsSync(userPath.certFilePath) && fs.existsSync(userPath.keyFilePath)) return userPath;
const subdomain = fqdn.substr(0, fqdn.length - domainObject.domain.length - 1);
const userCertificate = await apps.getCertificate(subdomain, domainObject.domain);
if (userCertificate) return getUserCertificatePathSync(fqdn);
if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificatePathSync(domain);
const acmePath = getAcmeCertificatePathSync(fqdn, domainObject);
if (fs.existsSync(acmePath.certFilePath) && fs.existsSync(acmePath.keyFilePath)) return acmePath;
const acmeCertificate = await getAcmeCertificate(fqdn, domainObject);
if (acmeCertificate) return getAcmeCertificatePathSync(fqdn, domainObject);
// only use fallback certs if acme cert was never got. expired acme certs will continue to be used
return getFallbackCertificatePathSync(domain);
}
async function syncUserCertificate(fqdn, domainObject) {
async function writeUserCertificate(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
const subdomain = fqdn.substr(0, fqdn.length - domainObject.domain.length - 1);
const userCertificate = await apps.getCertificate(subdomain, domainObject.domain);
if (!userCertificate) return null;
if (!userCertificate) return false;
const { certFilePath, keyFilePath } = getUserCertificatePathSync(fqdn);
if (!safe.fs.writeFileSync(certFilePath, userCertificate.cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write certificate: ${safe.error.message}`);
if (!safe.fs.writeFileSync(keyFilePath, userCertificate.key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write key: ${safe.error.message}`);
return { certFilePath, keyFilePath };
return true;
}
async function syncAcmeCertificate(fqdn, domainObject) {
async function getAcmeCertificate(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
const { certName, certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
const certName = getAcmeCertificateName(fqdn, domainObject);
const privateKey = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
if (!privateKey || !cert) return null;
return { privateKey, cert };
}
async function writeAcmeCertificate(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
const certName = getAcmeCertificateName(fqdn, domainObject);
const privateKey = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
const csr = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.csr`);
if (!privateKey || !cert) return null;
if (!privateKey || !cert) return false;
const { keyFilePath, certFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
if (!safe.fs.writeFileSync(keyFilePath, privateKey)) throw new BoxError(BoxError.FS_ERROR, `Failed to write private key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(certFilePath, cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write certificate: ${safe.error.message}`);
if (csr) safe.fs.writeFileSync(csrFilePath, csr);
return { certFilePath, keyFilePath };
return true;
}
async function updateCertBlobs(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
const { certName, certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
const { certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
const privateKey = safe.fs.readFileSync(keyFilePath);
if (!privateKey) throw new BoxError(BoxError.FS_ERROR, `Failed to read private key: ${safe.error.message}`);
@@ -348,62 +362,81 @@ async function updateCertBlobs(fqdn, domainObject) {
const csr = safe.fs.readFileSync(csrFilePath);
if (!csr) throw new BoxError(BoxError.FS_ERROR, `Failed to read csr: ${safe.error.message}`);
const certName = getAcmeCertificateName(fqdn, domainObject);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.key`, privateKey);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.cert`, cert);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csr);
}
async function ensureCertificate(subdomain, domain, auditSource) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
async function needsRenewal(fqdn, domainObject) {
const apiOptions = await getAcmeApiOptions(domainObject);
const { certFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
const notAfter = getExpiryDate(certFilePath);
const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month
if (!isExpiring && providerMatchesSync(domainObject, certFilePath, apiOptions)) return false;
debug(`needsRenewal: ${certFilePath} cert requires renewal`);
return true;
}
async function renewCert(fqdn, domainObject) {
const apiOptions = await getAcmeApiOptions(domainObject);
const acmePaths = getAcmeCertificatePathSync(fqdn, domainObject);
const [error] = await safe(acme2.getCertificate(fqdn, domainObject.domain, acmePaths, apiOptions));
if (error) { // write the fallback cert to keep the nginx configs consistent
fs.writeFileSync(acmePaths.certFilePath, domainObject.certificate.cert);
fs.writeFileSync(acmePaths.keyFilePath, domainObject.certificate.key);
} else {
await safe(updateCertBlobs(fqdn, domainObject));
}
if (settings.mailFqdn() === fqdn) {
debug('renewCert: restarting mail container');
const [restartError] = await safe(mail.handleCertChanged());
if (restartError) debug(`renewCert: error restarting mail container on cert change: ${restartError.message}`);
}
if (settings.dashboardFqdn() === fqdn) {
debug('renewCert: restarting directory server');
const [restartError] = await safe(directoryServer.handleCertChanged());
if (restartError) debug(`renewCert: error restarting directory server on cert change: ${restartError.message}`);
}
}
// ensures the cert of fqdn is available on disk. returns the path to the cert
async function ensureCertificate(fqdn, domainObject, options, auditSource) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
const domainObject = await domains.get(domain);
const userCertificatePath = await syncUserCertificate(subdomain, domainObject);
if (userCertificatePath) return { certificatePath: userCertificatePath, renewed: false };
if (await writeUserCertificate(fqdn, domainObject)) {
debug(`ensureCertificate: ${fqdn} will use user certs`);
return;
}
if (domainObject.tlsConfig.provider === 'fallback') {
debug(`ensureCertificate: ${subdomain} will use fallback certs`);
return { certificatePath: getFallbackCertificatePathSync(domain), renewed: false };
debug(`ensureCertificate: ${fqdn} will use fallback certs`);
return;
}
const { acme2, apiOptions } = await getAcmeApi(domainObject);
let notAfter = null;
let renewal = false;
const [, acmeCertificatePath] = await safe(syncAcmeCertificate(subdomain, domainObject));
if (acmeCertificatePath) {
debug(`ensureCertificate: ${subdomain} certificate already exists at ${acmeCertificatePath.keyFilePath}`);
notAfter = getExpiryDate(acmeCertificatePath.certFilePath);
const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month
if (!isExpiring && providerMatchesSync(domainObject, acmeCertificatePath.certFilePath, apiOptions)) return { certificatePath: acmeCertificatePath, renewed: false };
debug(`ensureCertificate: ${subdomain} cert requires renewal`);
if (await writeAcmeCertificate(fqdn, domainObject)) {
if (!await needsRenewal(fqdn, domainObject)) return;
renewal = true;
debug(`ensureCertificate: ${fqdn} cert requires renewal`);
} else {
debug(`ensureCertificate: ${subdomain} cert does not exist`);
debug(`ensureCertificate: ${fqdn} cert does not exist`);
}
debug(`ensureCertificate: getting certificate for ${subdomain} with options ${JSON.stringify(apiOptions)}`);
if (options.skipRenewal) return;
const acmePaths = getAcmeCertificatePathSync(subdomain, domainObject);
const [error] = await safe(acme2.getCertificate(subdomain, domain, acmePaths, apiOptions));
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`);
const [renewError] = await safe(renewCert(fqdn, domainObject));
debug(`ensureCertificate: error: ${renewError ? renewError.message : 'null'}`);
await safe(eventlog.add(acmeCertificatePath ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: subdomain, errorMessage: error ? error.message : '', notAfter }));
if (error && acmeCertificatePath && (notAfter - new Date() > 0)) { // still some life left in this certificate
debug('ensureCertificate: continue using existing certificate since renewal failed');
return { certificatePath: acmeCertificatePath, renewed: false };
}
if (!error) {
const [updateCertError] = await safe(updateCertBlobs(subdomain, domainObject));
if (!updateCertError) return { certificatePath: { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, renewed: true };
}
debug(`ensureCertificate: renewal of ${subdomain} failed. using fallback certificates for ${domain}`);
return { certificatePath: getFallbackCertificatePathSync(domain), renewed: false };
await safe(eventlog.add(renewal ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: renewError?.message || '' }));
}
async function writeDashboardNginxConfig(fqdn, certificatePath) {
@@ -535,14 +568,11 @@ async function writeAppNginxConfig(app, fqdn, type, certificatePath) {
async function writeAppConfigs(app) {
assert.strictEqual(typeof app, 'object');
const appDomains = [{ domain: app.domain, fqdn: app.fqdn, certificate: app.certificate, type: apps.LOCATION_TYPE_PRIMARY }]
.concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, certificate: sd.certificate, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY }; }))
.concat(app.redirectDomains.map(rd => { return { domain: rd.domain, certificate: rd.certificate, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT }; }))
.concat(app.aliasDomains.map(ad => { return { domain: ad.domain, certificate: ad.certificate, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS }; }));
const locations = appLocationsSync(app);
for (const appDomain of appDomains) {
const certificatePath = await getCertificatePath(appDomain.fqdn, appDomain.domain);
await writeAppNginxConfig(app, appDomain.fqdn, appDomain.type, certificatePath);
for (const location of locations) {
const certificatePath = await getCertificatePath(location.fqdn, location.domain);
await writeAppNginxConfig(app, location.fqdn, location.type, certificatePath);
}
}
@@ -560,17 +590,26 @@ async function setUserCertificate(app, fqdn, certificate) {
await writeAppConfigs(app);
}
function appLocationsSync(app) {
assert.strictEqual(typeof app, 'object');
const locations = [{ domain: app.domain, fqdn: app.fqdn, certificate: app.certificate, type: apps.LOCATION_TYPE_PRIMARY }]
.concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, certificate: sd.certificate, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY }; }))
.concat(app.redirectDomains.map(rd => { return { domain: rd.domain, certificate: rd.certificate, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT }; }))
.concat(app.aliasDomains.map(ad => { return { domain: ad.domain, certificate: ad.certificate, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS }; }));
return locations;
}
async function configureApp(app, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
const appDomains = [{ domain: app.domain, fqdn: app.fqdn }]
.concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, fqdn: sd.fqdn }; }))
.concat(app.redirectDomains.map(rd => { return { domain: rd.domain, fqdn: rd.fqdn }; }))
.concat(app.aliasDomains.map(ad => { return { domain: ad.domain, fqdn: ad.fqdn }; }));
const locations = await appLocationsSync(app);
const domainObjectMap = await domains.getDomainObjectMap();
for (const appDomain of appDomains) {
await ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource);
for (const location of locations) {
await ensureCertificate(location.fqdn, domainObjectMap[location.domain], {}, auditSource);
}
await writeAppConfigs(app);
@@ -596,66 +635,48 @@ async function renewCerts(options, auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const domainObjects = await domains.list();
let domainObjectMap = {};
for (const d of domainObjects) { domainObjectMap[d.domain] = d; }
const allApps = await apps.list();
let appDomains = [];
// add webadmin and mail domain
if (settings.mailFqdn() === settings.dashboardFqdn()) {
appDomains.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), type: 'webadmin+mail', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.dashboardFqdn()}.conf`) });
} else {
appDomains.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.dashboardFqdn()}.conf`) });
appDomains.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), type: 'mail' });
}
let locations = [];
// important: mail domain goes first because restart is only done on renewal. when using wildcard, renewal will skip if it appears later
if (settings.dashboardDomain() !== settings.mailDomain()) locations.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), certificate: null });
locations.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), certificate: null });
for (const app of allApps) {
if (app.runState === apps.RSTATE_STOPPED) continue; // do not renew certs of stopped apps
appDomains = appDomains.concat([{ app, domain: app.domain, fqdn: app.fqdn, type: apps.LOCATION_TYPE_PRIMARY, nginxConfigFilename: getNginxConfigFilename(app, app.fqdn, apps.LOCATION_TYPE_PRIMARY) }])
.concat(app.secondaryDomains.map(sd => { return { app, domain: sd.domain, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY, nginxConfigFilename: getNginxConfigFilename(app, sd.fqdn, apps.LOCATION_TYPE_SECONDARY) }; }))
.concat(app.redirectDomains.map(rd => { return { app, domain: rd.domain, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT, nginxConfigFilename: getNginxConfigFilename(app, rd.fqdn, apps.LOCATION_TYPE_REDIRECT) }; }))
.concat(app.aliasDomains.map(ad => { return { app, domain: ad.domain, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS, nginxConfigFilename: getNginxConfigFilename(app, ad.fqdn, apps.LOCATION_TYPE_ALIAS) }; }));
locations = locations.concat(appLocationsSync(app));
}
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
let percent = 1, renewedCertificateNames = [];
for (const location of locations) {
percent += Math.round(100/locations.length);
progressCallback({ percent, message: `Ensuring certs of ${location.fqdn}` });
let progress = 1, renewedCerts = [];
const domainObject = domainObjectMap[location.domain];
if (location.certificate?.key && location.certificate?.cert) continue; // user cert
if (domainObject.tlsConfig.provider === 'fallback') continue; // fallback certs
for (const appDomain of appDomains) {
progressCallback({ percent: progress, message: `Ensuring certs of ${appDomain.fqdn}` });
progress += Math.round(100/appDomains.length);
const { certificatePath, renewed } = await ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource);
if (renewed) renewedCerts.push(appDomain.fqdn);
if (appDomain.type === 'mail') continue; // mail has no nginx config to check current cert
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
if (currentNginxConfig.includes(certificatePath.certFilePath)) continue;
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${certificatePath.certFilePath}`);
// reconfigure since the cert changed
if (appDomain.type === 'webadmin' || appDomain.type === 'webadmin+mail') {
await writeDashboardNginxConfig(settings.dashboardFqdn(), certificatePath);
if (await needsRenewal(location.fqdn, domainObject)) {
await renewCert(location.fqdn, domainObject);
renewedCertificateNames.push(getAcmeCertificateName(location.fqdn, domainObject));
} else {
await writeAppNginxConfig(appDomain.app, appDomain.fqdn, appDomain.type, certificatePath);
progressCallback({ percent, message: `Cert of ${location.fqdn} does not require renewal` });
}
}
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewedCerts)}`);
if (renewedCerts.length === 0) return;
if (renewedCertificateNames.length === 0) return;
if (renewedCerts.includes(settings.mailFqdn())) await mail.handleCertChanged();
progressCallback({ message: `Reloading nginx after renewing ${JSON.stringify(renewedCertificateNames)}` });
await reload(); // reload nginx if any certs were updated but the config was not rewritten
await reload();
// restart tls apps on cert change
const tlsApps = allApps.filter(app => app.manifest.addons && app.manifest.addons.tls && renewedCerts.includes(app.fqdn));
for (const app of tlsApps) {
await apps.restart(app, auditSource);
for (const app of allApps) {
if (!app.manifest.addons?.tls) continue;
const addonCertificateName = getAcmeCertificateName(app.fqdn, domainObjectMap[app.domain]);
if (renewedCertificateNames.includes(addonCertificateName)) await apps.restart(app, auditSource);
}
}
@@ -663,11 +684,28 @@ async function cleanupCerts(auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
progressCallback({ message: 'Checking expired certs for removal' });
let locations = [];
if (settings.dashboardDomain() !== settings.mailDomain()) locations.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), certificate: null });
locations.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), certificate: null });
for (const app of await apps.list()) {
if (app.runState === apps.RSTATE_STOPPED) continue; // not in use
locations = locations.concat(appLocationsSync(app));
}
const certsInUse = [];
for (const location of locations) {
const certificatePath = await getCertificatePath(location.fqdn, location.domain);
certsInUse.push(path.basename(certificatePath.certFilePath));
}
const filenames = await fs.promises.readdir(paths.NGINX_CERT_DIR);
const certFilenames = filenames.filter(f => f.endsWith('.cert'));
const certFilenames = filenames.filter(f => f.endsWith('.cert') && !f.endsWith('.user.cert') && !f.endsWith('host.cert') && !certsInUse.includes(f));
const now = new Date();
progressCallback({ message: 'Checking expired certs for removal' });
debug(`cleanupCerts: considering ${JSON.stringify(certFilenames)} for cleanup`);
const fqdns = [];