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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user