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:
45
src/apps.js
45
src/apps.js
@@ -64,9 +64,6 @@ exports = module.exports = {
|
||||
|
||||
appendLogLine,
|
||||
|
||||
getCertificate,
|
||||
getLocationsSync,
|
||||
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
@@ -139,6 +136,11 @@ exports = module.exports = {
|
||||
LOCATION_TYPE_REDIRECT: 'redirect',
|
||||
LOCATION_TYPE_ALIAS: 'alias',
|
||||
|
||||
// should probably be in table as well
|
||||
LOCATION_TYPE_DASHBOARD: 'dashboard',
|
||||
LOCATION_TYPE_MAIL: 'mail',
|
||||
LOCATION_TYPE_DIRECTORY_SERVER: 'directoryserver',
|
||||
|
||||
// respositories, match with appstore
|
||||
REPOSITORY_CORE: 'core',
|
||||
REPOSITORY_COMMUNITY: 'community',
|
||||
@@ -204,6 +206,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS
|
||||
'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
|
||||
|
||||
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
const LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJson' ];
|
||||
|
||||
const CHECKVOLUME_CMD = path.join(__dirname, 'scripts/checkvolume.sh');
|
||||
|
||||
@@ -1797,11 +1800,23 @@ async function setCertificate(app, data, auditSource) {
|
||||
const result = await database.query('UPDATE locations SET certificateJson=? WHERE location=? AND domain=?', [ certificate ? JSON.stringify(certificate) : null, subdomain, domain ]);
|
||||
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Location not found');
|
||||
|
||||
app = await get(app.id); // refresh app object
|
||||
await reverseProxy.setUserCertificate(app, dns.fqdn(subdomain, domain), certificate);
|
||||
const location = await getLocation(subdomain, domain); // fresh location object
|
||||
await reverseProxy.setUserCertificate(app, location);
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: app.id, app, subdomain, domain, cert });
|
||||
}
|
||||
|
||||
async function getLocation(subdomain, domain) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
const result = await database.query(`SELECT ${LOCATION_FIELDS} FROM locations WHERE subdomain=? AND domain=?`, [ subdomain, domain ]);
|
||||
if (result.length === 0) return null;
|
||||
|
||||
result[0].certificate = safe.JSON.parse(result[0].certificateJson);
|
||||
result[0].fqdn = dns.fqdn(subdomain, domain);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async function setLocation(app, data, auditSource) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
@@ -2049,15 +2064,6 @@ async function appendLogLine(app, line) {
|
||||
if (!safe.fs.appendFileSync(logFilePath, line)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
async function getCertificate(subdomain, domain) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
const result = await database.query('SELECT certificateJson FROM locations WHERE subdomain=? AND domain=?', [ subdomain, domain ]);
|
||||
if (result.length === 0) return null;
|
||||
return safe.JSON.parse(result[0].certificateJson);
|
||||
}
|
||||
|
||||
// does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest
|
||||
// re-configure can take a dockerImage but not a manifest because re-configure does not clean up addons
|
||||
async function repair(app, data, auditSource) {
|
||||
@@ -2866,14 +2872,3 @@ async function restoreConfig(app) {
|
||||
|
||||
await update(app.id, data);
|
||||
}
|
||||
|
||||
function getLocationsSync(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
const locations = [{ subdomain: app.subdomain, domain: app.domain, fqdn: app.fqdn, certificate: app.certificate, type: exports.LOCATION_TYPE_PRIMARY }]
|
||||
.concat(app.secondaryDomains.map(sd => { return { subdomain: sd.subdomain, domain: sd.domain, certificate: sd.certificate, fqdn: sd.fqdn, type: exports.LOCATION_TYPE_SECONDARY }; }))
|
||||
.concat(app.redirectDomains.map(rd => { return { subdomain: rd.subdomain, domain: rd.domain, certificate: rd.certificate, fqdn: rd.fqdn, type: exports.LOCATION_TYPE_REDIRECT }; }))
|
||||
.concat(app.aliasDomains.map(ad => { return { subdomain: ad.subdomain, domain: ad.domain, certificate: ad.certificate, fqdn: ad.fqdn, type: exports.LOCATION_TYPE_ALIAS }; }));
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ exports = module.exports = {
|
||||
setString,
|
||||
del,
|
||||
|
||||
listCertIds,
|
||||
|
||||
ACME_ACCOUNT_KEY: 'acme_account_key',
|
||||
ADDON_TURN_SECRET: 'addon_turn_secret',
|
||||
SFTP_PUBLIC_KEY: 'sftp_public_key',
|
||||
@@ -16,6 +18,7 @@ exports = module.exports = {
|
||||
PROXY_AUTH_TOKEN_SECRET: 'proxy_auth_token_secret',
|
||||
|
||||
CERT_PREFIX: 'cert',
|
||||
CERT_SUFFIX: 'cert',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
@@ -62,3 +65,8 @@ async function del(id) {
|
||||
async function clear() {
|
||||
await database.query('DELETE FROM blobs');
|
||||
}
|
||||
|
||||
async function listCertIds() {
|
||||
const result = await database.query('SELECT id FROM blobs WHERE id LIKE ?', [ `${exports.CERT_PREFIX}-%.${exports.CERT_SUFFIX}` ]);
|
||||
return result.map(r => r.id);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ const apps = require('./apps.js'),
|
||||
delay = require('./delay.js'),
|
||||
dns = require('./dns.js'),
|
||||
dockerProxy = require('./dockerproxy.js'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
LogStream = require('./log-stream.js'),
|
||||
@@ -113,10 +112,7 @@ async function runStartupTasks() {
|
||||
tasks.push(async function () {
|
||||
if (!settings.dashboardDomain()) return;
|
||||
|
||||
// always write certs to overcome 0 length certs on disk full
|
||||
const domainObject = await domains.get(settings.dashboardDomain());
|
||||
await reverseProxy.ensureCertificate(settings.dashboardFqdn(), domainObject, { skipRenewal: true }, AuditSource.PLATFORM);
|
||||
await reverseProxy.writeDashboardConfig(domainObject);
|
||||
await reverseProxy.writeDashboardConfig(settings.dashboardDomain());
|
||||
});
|
||||
|
||||
tasks.push(async function () {
|
||||
@@ -267,10 +263,7 @@ async function setDashboardDomain(domain, auditSource) {
|
||||
|
||||
debug(`setDashboardDomain: ${domain}`);
|
||||
|
||||
const domainObject = await domains.get(domain);
|
||||
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'No such domain');
|
||||
|
||||
await reverseProxy.writeDashboardConfig(domainObject);
|
||||
await reverseProxy.writeDashboardConfig(domain);
|
||||
const fqdn = dns.fqdn(constants.DASHBOARD_SUBDOMAIN, domain);
|
||||
|
||||
await settings.setDashboardLocation(domain, fqdn);
|
||||
@@ -308,7 +301,6 @@ async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback)
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
const domainObject = await domains.get(domain);
|
||||
const dashboardFqdn = dns.fqdn(subdomain, domain);
|
||||
|
||||
const ipv4 = await sysinfo.getServerIPv4();
|
||||
@@ -321,7 +313,8 @@ async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback)
|
||||
await dns.waitForDnsRecord(subdomain, domain, 'A', ipv4, { interval: 30000, times: 50000 });
|
||||
if (ipv6) await dns.waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { interval: 30000, times: 50000 });
|
||||
progressCallback({ percent: 60, message: `Getting certificate of ${dashboardFqdn}` });
|
||||
await reverseProxy.ensureCertificate(dns.fqdn(subdomain, domain), domainObject, {}, auditSource);
|
||||
const location = { subdomain, domain, fqdn: dashboardFqdn, type: apps.LOCATION_TYPE_DASHBOARD, certificate: null };
|
||||
await reverseProxy.ensureCertificate(location, auditSource);
|
||||
}
|
||||
|
||||
async function syncDnsRecords(options) {
|
||||
|
||||
@@ -14,10 +14,7 @@ const assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:directoryserver'),
|
||||
dns = require('./dns.js'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
ldap = require('ldapjs'),
|
||||
path = require('path'),
|
||||
@@ -31,7 +28,7 @@ const assert = require('assert'),
|
||||
util = require('util'),
|
||||
validator = require('validator');
|
||||
|
||||
let gServer = null;
|
||||
let gServer = null, gCertificate = null;
|
||||
|
||||
const NOOP = function () {};
|
||||
|
||||
@@ -298,7 +295,6 @@ async function userAuth(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
// FIXME this needs to be restarted if settings changes or dashboard cert got renewed
|
||||
async function start() {
|
||||
if (gServer) return; // already running
|
||||
|
||||
@@ -311,13 +307,11 @@ async function start() {
|
||||
fatal: debug
|
||||
};
|
||||
|
||||
const domainObject = await domains.get(settings.dashboardDomain());
|
||||
const dashboardFqdn = dns.fqdn(constants.DASHBOARD_SUBDOMAIN, settings.dashboardDomain());
|
||||
const certificatePath = await reverseProxy.getCertificatePath(dashboardFqdn, domainObject.domain);
|
||||
gCertificate = await reverseProxy.getDirectoryServerCertificate();
|
||||
|
||||
gServer = ldap.createServer({
|
||||
certificate: fs.readFileSync(certificatePath.certFilePath, 'utf8'),
|
||||
key: fs.readFileSync(certificatePath.keyFilePath, 'utf8'),
|
||||
certificate: gCertificate.cert,
|
||||
key: gCertificate.key,
|
||||
log: logger
|
||||
});
|
||||
|
||||
@@ -373,6 +367,13 @@ async function stop() {
|
||||
}
|
||||
|
||||
async function handleCertChanged() {
|
||||
const certificate = await reverseProxy.getDirectoryServerCertificate();
|
||||
if (certificate.cert.equals(gCertificate.cert)) {
|
||||
debug('handleCertChanged: certificate has not changed');
|
||||
return;
|
||||
}
|
||||
|
||||
debug('handleCertChanged: certificate changed. restarting');
|
||||
await stop();
|
||||
await start();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const apps = require('./apps.js'),
|
||||
debug = require('debug')('box:docker'),
|
||||
delay = require('./delay.js'),
|
||||
Docker = require('dockerode'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
paths = require('./paths.js'),
|
||||
services = require('./services.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
@@ -205,18 +205,11 @@ async function getAddonMounts(app) {
|
||||
break;
|
||||
}
|
||||
case 'tls': {
|
||||
const certificatePath = await reverseProxy.getCertificatePath(app.fqdn, app.domain);
|
||||
const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`;
|
||||
|
||||
mounts.push({
|
||||
Target: '/etc/certs/tls_cert.pem',
|
||||
Source: certificatePath.certFilePath,
|
||||
Type: 'bind',
|
||||
ReadOnly: true
|
||||
});
|
||||
|
||||
mounts.push({
|
||||
Target: '/etc/certs/tls_key.pem',
|
||||
Source: certificatePath.keyFilePath,
|
||||
Target: '/etc/certs',
|
||||
Source: certificateDir,
|
||||
Type: 'bind',
|
||||
ReadOnly: true
|
||||
});
|
||||
|
||||
@@ -243,7 +243,6 @@ async function setConfig(domain, data, auditSource) {
|
||||
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found');
|
||||
|
||||
if (fallbackCertificate) await reverseProxy.setFallbackCertificate(domain, fallbackCertificate);
|
||||
if (!_.isEqual(domainObject.tlsConfig, tlsConfig.provider)) await reverseProxy.handleCertificateProviderChanged();
|
||||
|
||||
await eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ exports = module.exports = {
|
||||
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
|
||||
|
||||
ACTION_CERTIFICATE_NEW: 'certificate.new',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew', // obsolete
|
||||
ACTION_CERTIFICATE_CLEANUP: 'certificate.cleanup',
|
||||
|
||||
ACTION_DASHBOARD_DOMAIN_UPDATE: 'dashboard.domain.update',
|
||||
|
||||
16
src/mail.js
16
src/mail.js
@@ -717,15 +717,15 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
|
||||
const memory = await system.getMemoryAllocation(memoryLimit);
|
||||
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
|
||||
|
||||
const certificatePath = await reverseProxy.getCertificatePath(mailFqdn, mailDomain);
|
||||
const certificate = await reverseProxy.getMailCertificate();
|
||||
|
||||
const dhparamsFilePath = `${paths.MAIL_CONFIG_DIR}/dhparams.pem`;
|
||||
const mailCertFilePath = `${paths.MAIL_CONFIG_DIR}/tls_cert.pem`;
|
||||
const mailKeyFilePath = `${paths.MAIL_CONFIG_DIR}/tls_key.pem`;
|
||||
|
||||
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message);
|
||||
if (!safe.child_process.execSync(`cp ${certificatePath.certFilePath} ${mailCertFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message);
|
||||
if (!safe.child_process.execSync(`cp ${certificatePath.keyFilePath} ${mailKeyFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message);
|
||||
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) throw new BoxError(BoxError.FS_ERROR, `Could not copy dhparams: ${safe.error.message}`);
|
||||
if (!safe.fs.writeFileSync(mailCertFilePath, certificate.cert)) throw new BoxError(BoxError.FS_ERROR, `Could not create cert file: ${safe.error.message}`);
|
||||
if (!safe.fs.writeFileSync(mailKeyFilePath, certificate.key)) throw new BoxError(BoxError.FS_ERROR, `Could not create key file: ${safe.error.message}`);
|
||||
|
||||
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
|
||||
if (!safe.fs.chmodSync(mailKeyFilePath, 0o644)) throw new BoxError(BoxError.FS_ERROR, `Could not chmod key file: ${safe.error.message}`);
|
||||
@@ -812,6 +812,14 @@ async function restartMailIfActivated() {
|
||||
|
||||
async function handleCertChanged() {
|
||||
debug('handleCertChanged: will restart if activated');
|
||||
const certificate = await reverseProxy.getMailCertificate();
|
||||
const cert = safe.fs.readFileSync(`${paths.MAIL_CONFIG_DIR}/tls_cert.pem`);
|
||||
if (cert && cert.equals(certificate.cert)) {
|
||||
debug('handleCertChanged: certificate has not changed');
|
||||
return;
|
||||
}
|
||||
debug('handleCertChanged: certificate has changed');
|
||||
|
||||
await restartMailIfActivated();
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -161,8 +161,8 @@ const ADDONS = {
|
||||
clear: NOOP,
|
||||
},
|
||||
tls: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
setup: setupTls,
|
||||
teardown: teardownTls,
|
||||
backup: NOOP,
|
||||
restore: NOOP,
|
||||
clear: NOOP,
|
||||
@@ -1813,6 +1813,23 @@ async function restoreRedis(app, options) {
|
||||
await pipeFileToRequest(dumpPath('redis', app.id), `http://${result.ip}:3000/restore?access_token=${result.token}`);
|
||||
}
|
||||
|
||||
async function setupTls(app, options) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
if (!safe.fs.mkdirSync(`${paths.PLATFORM_DATA_DIR}/tls/${app.id}`, { recursive: true })) {
|
||||
debug('Error creating tls directory');
|
||||
throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function teardownTls(app, options) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
safe.fs.rmSync(`${paths.PLATFORM_DATA_DIR}/tls/${app.id}`, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function statusTurn() {
|
||||
const [error, container] = await safe(docker.inspect('turn'));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return { status: exports.SERVICE_STATUS_STOPPED };
|
||||
|
||||
Reference in New Issue
Block a user