reverseproxy: rework cert logic

9c8f78a059 already fixed many of the cert issues.

However, some issues were caught in the CI:

* The TLS addon has to be rebuilt and not just restarted. For this reason, we now
  move things to a directory instead of mounting files. This way the container is just restarted.

* Cleanups must be driven by the database and not the filesystem . Deleting files on disk or after a restore,
  the certs are left dangling forever in the db.

* Separate the db cert logic and disk cert logic. This way we can sync as many times as we want and whenever we want.
This commit is contained in:
Girish Ramakrishnan
2022-11-28 22:32:34 +01:00
parent c844be5be1
commit 89127e1df7
12 changed files with 279 additions and 348 deletions

View File

@@ -8,10 +8,10 @@ exports = module.exports = {
validateCertificate,
getCertificatePath, // resolved cert path
ensureCertificate,
getMailCertificate,
getDirectoryServerCertificate,
handleCertificateProviderChanged,
ensureCertificate,
checkCerts,
@@ -37,6 +37,7 @@ const acme2 = require('./acme2.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
dns = require('./dns.js'),
docker = require('./docker.js'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
@@ -61,7 +62,7 @@ function nginxLocation(s) {
return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
}
function getExpiryDate(cert) {
function getExpiryDateSync(cert) {
assert(Buffer.isBuffer(cert));
const result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-enddate', '-noout' ], { input: cert });
@@ -202,7 +203,6 @@ async function setFallbackCertificate(domain, certificate) {
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), certificate.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), certificate.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
// TODO: maybe the cert is being used by the mail container
await reload();
}
@@ -215,22 +215,13 @@ async function restoreFallbackCertificates() {
}
}
function getFallbackCertificatePathSync(domain) {
assert.strictEqual(typeof domain, 'string');
function getAppLocationsSync(app) {
assert.strictEqual(typeof app, 'object');
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
return { certFilePath, keyFilePath };
}
function getUserCertificatePathSync(fqdn) {
assert.strictEqual(typeof fqdn, 'string');
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`);
return { certFilePath, keyFilePath };
return [{ 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 }; }));
}
function getAcmeCertificateNameSync(fqdn, domainObject) {
@@ -244,139 +235,126 @@ function getAcmeCertificateNameSync(fqdn, domainObject) {
}
}
function getAcmeCertificatePathSync(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
function needsRenewalSync(cert) {
assert(Buffer.isBuffer(cert));
const certName = getAcmeCertificateNameSync(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`);
return { certFilePath, keyFilePath, csrFilePath };
const notAfter = getExpiryDateSync(cert);
const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month
debug(`needsRenewal: ${isExpiring}`);
return isExpiring;
}
async function getCertificatePath(fqdn, domain) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
async function getCertificate(location) {
assert.strictEqual(typeof location, 'object');
const { domain, fqdn } = location;
const domainObject = await domains.get(domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${domain} not found`);
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 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 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 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 true;
}
async function getAcmeCertificate(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
if (location.certificate) return location.certificate;
if (domainObject.tlsConfig.provider === 'fallback') return domainObject.fallbackCertificate;
const certName = getAcmeCertificateNameSync(fqdn, domainObject);
const key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
if (!key || !cert) return null;
const key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
if (!key || !cert) return domainObject.fallbackCertificate;
return { key, cert };
}
async function writeAcmeCertificate(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
assert.strictEqual(typeof domainObject, 'object');
async function getMailCertificate() {
return await getCertificate({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), certificate: null, type: apps.LOCATION_TYPE_MAIL });
}
async function getDirectoryServerCertificate() {
return await getCertificate({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), certificate: null, type: apps.LOCATION_TYPE_DIRECTORY_SERVER });
}
// write if contents mismatch
function writeFileSync(filePath, data) {
assert.strictEqual(typeof filePath, 'string');
assert(Buffer.isBuffer(data) || typeof data === 'string'); // domain and location stores certs as json but not acme
const curData = safe.fs.readFileSync(filePath);
if (curData && curData.equals(Buffer.from(data))) return;
if (!safe.fs.writeFileSync(filePath, data)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
}
async function setupTlsAddon(app) {
assert.strictEqual(typeof app, 'object');
const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`;
for (const location of getAppLocationsSync(app)) {
const certificate = await getCertificate(location);
writeFileSync(`${certificateDir}/${location.fqdn}.cert`, certificate.cert);
writeFileSync(`${certificateDir}/${location.fqdn}.key`, certificate.key);
if (location.type === apps.LOCATION_TYPE_PRIMARY) { // backward compat
writeFileSync(`${certificateDir}/tls_cert.pem`, certificate.cert);
writeFileSync(`${certificateDir}/tls_key.pem`, certificate.key);
}
}
await docker.restartContainer(app.id);
}
// writes latest certificate to disk and returns the path
async function writeCertificate(location) {
assert.strictEqual(typeof location, 'object');
const { domain, fqdn } = location;
const domainObject = await domains.get(domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${domain} not found`);
if (location.certificate) {
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`);
writeFileSync(certFilePath, location.certificate.cert);
writeFileSync(keyFilePath, location.certificate.key);
return { certFilePath, keyFilePath };
}
if (domainObject.tlsConfig.provider === 'fallback') {
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
debug(`writeCertificate: ${fqdn} will use fallback certs`);
writeFileSync(certFilePath, domainObject.fallbackCertificate.cert);
writeFileSync(keyFilePath, domainObject.fallbackCertificate.key);
return { certFilePath, keyFilePath };
}
const certName = getAcmeCertificateNameSync(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`);
let cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
let key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
if (!privateKey || !cert) return false;
if (!key || !cert) { // use fallback certs if we didn't manage to get acme certs
debug(`writeCertificate: ${fqdn} will use fallback certs because acme is missing`);
cert = domainObject.fallbackCertificate.cert;
key = domainObject.fallbackCertificate.key;
}
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`);
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);
writeFileSync(certFilePath, cert);
writeFileSync(keyFilePath, key);
return true;
return { certFilePath, keyFilePath };
}
async function needsRenewal(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domainObject, 'object');
const certificate = await getAcmeCertificate(fqdn, domainObject);
if (!certificate) return true;
const notAfter = getExpiryDate(certificate.cert);
const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month
if (!isExpiring && providerMatchesSync(domainObject, certificate.cert)) return false;
debug(`needsRenewal: ${fqdn} cert requires renewal`);
return true;
}
async function renewCert(fqdn, domainObject) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domainObject, 'object');
const acmePaths = getAcmeCertificatePathSync(fqdn, domainObject);
const [error, result] = await safe(acme2.getCertificate(fqdn, domainObject));
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 {
const { certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject);
if (!safe.fs.writeFileSync(keyFilePath, result.key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write private key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(certFilePath, result.cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write cert: ${safe.error.message}`);
if (!safe.fs.writeFileSync(csrFilePath, result.csr)) throw new BoxError(BoxError.FS_ERROR, `Failed to write csr: ${safe.error.message}`);
}
if (domainObject.domain === settings.mailDomain() && getAcmeCertificatePathSync(settings.mailFqdn(), domainObject).certFilePath === acmePaths.certFilePath) {
debug('renewCert: mail certificate changed');
const [restartError] = await safe(mail.handleCertChanged());
if (restartError) debug(`renewCert: error updating mail container on cert change: ${restartError.message}`);
}
if (domainObject.domain === settings.dashboardDomain() && getAcmeCertificatePathSync(settings.dashboardFqdn(), domainObject).certFilePath === acmePaths.certFilePath) {
debug('renewCert: directory server certificate changed');
const [reloadError] = await safe(shell.promises.sudo('renewCert', [ RESTART_SERVICE_CMD, 'box' ], {}));
if (reloadError) debug(`renewCert: error updating directory server on cert change: ${reloadError.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');
async function ensureCertificate(location, auditSource) {
assert.strictEqual(typeof location, 'object');
assert.strictEqual(typeof auditSource, 'object');
if (await writeUserCertificate(fqdn, domainObject)) {
const domainObject = await domains.get(location.domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${location.domain} not found`);
const fqdn = location.fqdn;
if (location.certificate) { // user certificate
debug(`ensureCertificate: ${fqdn} will use user certs`);
return;
}
@@ -386,22 +364,23 @@ async function ensureCertificate(fqdn, domainObject, options, auditSource) {
return;
}
let renewal = false;
const certName = getAcmeCertificateNameSync(fqdn, domainObject);
const key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
if (await writeAcmeCertificate(fqdn, domainObject)) {
if (!await needsRenewal(fqdn, domainObject)) return;
renewal = true;
debug(`ensureCertificate: ${fqdn} cert requires renewal`);
} else {
debug(`ensureCertificate: ${fqdn} cert does not exist`);
if (key && cert) {
if (providerMatchesSync(domainObject, cert) && !needsRenewalSync(cert)) {
debug(`ensureCertificate: ${fqdn} acme cert exists and is up to date`);
return;
}
debug(`ensureCertificate: ${fqdn} acme cert exists but provider mismatch or needs renewal`);
}
if (options.skipRenewal) return;
debug(`ensureCertificate: ${fqdn} needs acme cert`);
const [error] = await safe(acme2.getCertificate(fqdn, domainObject));
debug(`ensureCertificate: error: ${error ? error.message : 'null'}`);
const [renewError] = await safe(renewCert(fqdn, domainObject));
debug(`ensureCertificate: error: ${renewError ? renewError.message : 'null'}`);
await safe(eventlog.add(renewal ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: renewError?.message || '' }));
await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: error?.message || '' }));
}
async function writeDashboardNginxConfig(vhost, certificatePath) {
@@ -422,27 +401,29 @@ async function writeDashboardNginxConfig(vhost, certificatePath) {
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `dashboard/${vhost}.conf`);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error);
writeFileSync(nginxConfigFilename, nginxConf);
}
async function writeDashboardConfig(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
// also syncs the certs to disk
async function writeDashboardConfig(domain) {
assert.strictEqual(typeof domain, 'string');
debug(`writeDashboardConfig: writing admin config for ${domainObject.domain}`);
const dashboardFqdn = dns.fqdn(constants.DASHBOARD_SUBDOMAIN, domainObject.domain);
const certificatePath = await getCertificatePath(dashboardFqdn, domainObject.domain);
debug(`writeDashboardConfig: writing admin config for ${domain}`);
const dashboardFqdn = dns.fqdn(constants.DASHBOARD_SUBDOMAIN, domain);
const location = { domain, fqdn: dashboardFqdn, certificate: null };
const certificatePath = await writeCertificate(location);
await writeDashboardNginxConfig(dashboardFqdn, certificatePath);
await reload();
}
async function writeAppNginxConfig(app, vhost, type, certificatePath) {
async function writeAppLocationNginxConfig(app, location, certificatePath) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof location, 'object');
assert.strictEqual(typeof certificatePath, 'object');
const type = location.type, vhost = location.fqdn;
const data = {
sourceDir: path.resolve(__dirname, '..'),
vhost,
@@ -498,58 +479,47 @@ async function writeAppNginxConfig(app, vhost, type, certificatePath) {
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const filename = path.join(paths.NGINX_APPCONFIG_DIR, app.id, `${vhost.replace('*', '_')}.conf`);
debug(`writeAppNginxConfig: writing config for "${vhost}" to ${filename} with options ${JSON.stringify(data)}`);
if (!safe.fs.writeFileSync(filename, nginxConf)) {
debug(`Error creating nginx config for "${app.fqdn}" : ${safe.error.message}`);
throw new BoxError(BoxError.FS_ERROR, safe.error);
}
debug(`writeAppLocationNginxConfig: writing config for "${vhost}" to ${filename} with options ${JSON.stringify(data)}`);
writeFileSync(filename, nginxConf);
}
async function writeAppConfigs(app) {
assert.strictEqual(typeof app, 'object');
const locations = apps.getLocationsSync(app);
const locations = getAppLocationsSync(app);
if (!safe.fs.mkdirSync(path.join(paths.NGINX_APPCONFIG_DIR, app.id), { recursive: true })) throw new BoxError(BoxError.FS_ERROR, `Could not create nginx config directory: ${safe.error.message}`);
for (const location of locations) {
const certificatePath = await getCertificatePath(location.fqdn, location.domain);
await writeAppNginxConfig(app, location.fqdn, location.type, certificatePath);
const certificatePath = await writeCertificate(location);
await writeAppLocationNginxConfig(app, location, certificatePath);
}
await reload();
}
async function setUserCertificate(app, fqdn, certificate) {
async function setUserCertificate(app, location) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof certificate, 'object');
assert.strictEqual(typeof location, 'object');
const { certFilePath, keyFilePath } = getUserCertificatePathSync(fqdn);
if (certificate !== null) {
if (!safe.fs.writeFileSync(certFilePath, certificate.cert)) throw safe.error;
if (!safe.fs.writeFileSync(keyFilePath, certificate.key)) throw safe.error;
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(certFilePath)) debug(`Error removing cert: ${safe.error.message}`);
if (!safe.fs.unlinkSync(keyFilePath)) debug(`Error removing key: ${safe.error.message}`);
}
await writeAppConfigs(app);
const certificatePath = await writeCertificate(location);
await writeAppLocationNginxConfig(app, location, certificatePath);
await reload();
}
async function configureApp(app, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
const locations = await apps.getLocationsSync(app);
const domainObjectMap = await domains.getDomainObjectMap();
const locations = getAppLocationsSync(app);
for (const location of locations) {
await ensureCertificate(location.fqdn, domainObjectMap[location.domain], {}, auditSource);
await ensureCertificate(location, auditSource);
}
await writeAppConfigs(app);
if (app.manifest.addons?.tls) await setupTlsAddon(app);
}
async function unconfigureApp(app) {
@@ -559,142 +529,91 @@ async function unconfigureApp(app) {
await reload();
}
async function renewCerts(auditSource, progressCallback) {
async function ensureCertificates(locations, auditSource, progressCallback) {
assert(Array.isArray(locations));
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 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
locations = locations.concat(apps.getLocationsSync(app));
}
let percent = 1, renewedCertificateNames = [];
let percent = 1;
for (const location of locations) {
percent += Math.round(100/locations.length);
progressCallback({ percent, message: `Ensuring certs of ${location.fqdn}` });
const domainObject = domainObjectMap[location.domain];
if (location.certificate?.key && location.certificate?.cert) continue; // user cert
if (domainObject.tlsConfig.provider === 'fallback') {
progressCallback({ message: `${location.fqdn} is using fallback certs` });
continue;
}
if (await needsRenewal(location.fqdn, domainObject)) {
await renewCert(location.fqdn, domainObject);
renewedCertificateNames.push(getAcmeCertificateNameSync(location.fqdn, domainObject));
} else {
progressCallback({ message: `Cert of ${location.fqdn} does not require renewal` });
}
}
if (renewedCertificateNames.length === 0) return;
progressCallback({ message: `Reloading nginx after renewing ${JSON.stringify(renewedCertificateNames)}` });
await reload();
for (const app of allApps) {
if (!app.manifest.addons?.tls) continue;
const addonCertificateName = getAcmeCertificateNameSync(app.fqdn, domainObjectMap[app.domain]);
if (renewedCertificateNames.includes(addonCertificateName)) await apps.restart(app, auditSource);
await ensureCertificate(location, auditSource);
}
}
async function cleanupCerts(auditSource, progressCallback) {
async function cleanupCerts(locations, auditSource, progressCallback) {
assert(Array.isArray(locations));
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(apps.getLocationsSync(app));
}
const certsInUse = [ 'default.cert' ];
const domainObjectMap = await domains.getDomainObjectMap();
const certNamesInUse = new Set();
for (const location of locations) {
const certificatePath = await getCertificatePath(location.fqdn, location.domain);
certsInUse.push(path.basename(certificatePath.certFilePath));
certNamesInUse.add(await getAcmeCertificateNameSync(location.fqdn, domainObjectMap[location.domain]));
}
const filenames = await fs.promises.readdir(paths.NGINX_CERT_DIR);
const certFilenames = filenames.filter(f => f.endsWith('.cert') && !f.endsWith('.user.cert') && !f.endsWith('host.cert') && !certsInUse.includes(f));
const now = new Date();
const certIds = await blobs.listCertIds();
const removedCertNames = [];
for (const certId of certIds) {
const certName = certId.match(new RegExp(`${blobs.CERT_PREFIX}-(.*).cert`))[0];
if (certNamesInUse.has(certName)) continue;
debug(`cleanupCerts: considering ${JSON.stringify(certFilenames)} for cleanup`);
const fqdns = [];
for (const certFilename of certFilenames) {
const certFilePath = path.join(paths.NGINX_CERT_DIR, certFilename);
const notAfter = getExpiryDate(certFilePath);
const cert = await blobs.get(certId);
const notAfter = getExpiryDateSync(cert);
if (!notAfter) continue; // some error
if (now - notAfter >= (60 * 60 * 24 * 30 * 6 * 1000)) { // expired 6 months ago
const fqdn = certFilename.replace(/\.cert$/, '');
progressCallback({ message: `deleting certs of ${fqdn}` });
if (now - notAfter >= (60 * 60 * 24 * 30 * 6 * 1000)) { // expired 6 months ago and not in use
progressCallback({ message: `deleting certs of ${certName}` });
// it is safe to delete the certs of stopped apps because their nginx configs are removed
safe.fs.unlinkSync(certFilePath);
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.key`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.csr`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.cert`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.key`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.csr`));
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.key`);
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.cert`);
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.csr`);
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.key`);
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.cert`);
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.csr`);
fqdns.push(fqdn);
removedCertNames.push(certName);
}
}
if (fqdns.length) await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_CLEANUP, auditSource, { domains: fqdns }));
if (removedCertNames.length) await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_CLEANUP, auditSource, { domains: removedCertNames }));
debug('cleanupCerts: done');
}
async function rebuildConfigs(auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
debug('rebuildConfigs: rebuilding all configs');
progressCallback( { message: 'Rebuilding app configs' });
for (const app of await apps.list()) {
if (app.runState === apps.RSTATE_STOPPED) continue; // not in use
await writeAppConfigs(app);
}
await writeDashboardConfig(await domains.get(settings.dashboardDomain()));
await shell.promises.sudo('rebuildConfigs', [ RESTART_SERVICE_CMD, 'box' ], {});
progressCallback( { message: 'Rebuilding mail config' });
await mail.handleCertChanged();
}
async function checkCerts(auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
await renewCerts(auditSource, progressCallback);
if (fs.existsSync(paths.REVERSE_PROXY_REBUILD_FILE)) {
await rebuildConfigs(auditSource, progressCallback);
safe.fs.unlinkSync(paths.REVERSE_PROXY_REBUILD_FILE);
let locations = [];
if (settings.dashboardDomain() !== settings.mailDomain()) locations.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), certificate: null, type: apps.LOCATION_TYPE_MAIL });
locations.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), certificate: null, type: apps.LOCATION_TYPE_DASHBOARD });
const allApps = (await apps.list()).filter(app => app.runState !== apps.RSTATE_STOPPED);
for (const app of allApps) {
locations = locations.concat(getAppLocationsSync(app));
}
await cleanupCerts(auditSource, progressCallback);
await ensureCertificates(locations, auditSource, progressCallback);
progressCallback( { message: 'Rebuilding app configs' });
for (const app of allApps) {
await writeAppConfigs(app);
}
await writeDashboardConfig(settings.dashboardDomain());
await mail.handleCertChanged();
await shell.promises.sudo('rebuildConfigs', [ RESTART_SERVICE_CMD, 'box' ], {});
for (const app of allApps) {
if (app.manifest.addons?.tls) await setupTlsAddon(app);
}
await cleanupCerts(locations, auditSource, progressCallback);
}
function removeAppConfigs() {
@@ -751,7 +670,3 @@ async function writeDefaultConfig(options) {
await reload();
}
async function handleCertificateProviderChanged() {
safe.fs.writeFileSync(paths.REVERSE_PROXY_REBUILD_FILE, 'cert provider changed\n', 'utf8');
}