diff --git a/box.js b/box.js index 779f0a8af..cf52e7ca3 100755 --- a/box.js +++ b/box.js @@ -51,7 +51,8 @@ async function main() { process.on('SIGHUP', async function () { debug('Received SIGHUP. Re-reading configs.'); - await directoryServer.handleCertChanged(); + const conf = await settings.getDirectoryServerConfig(); + if (conf.enabled) await directoryServer.handleCertChanged(); }); process.on('SIGINT', async function () { diff --git a/setup/start.sh b/setup/start.sh index 1b24d0a13..2a830bf87 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -56,6 +56,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql" mkdir -p "${PLATFORM_DATA_DIR}/postgresql" mkdir -p "${PLATFORM_DATA_DIR}/mongodb" mkdir -p "${PLATFORM_DATA_DIR}/redis" +mkdir -p "${PLATFORM_DATA_DIR}/tls" mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner" \ "${PLATFORM_DATA_DIR}/addons/mail/dkim" mkdir -p "${PLATFORM_DATA_DIR}/collectd" @@ -228,7 +229,7 @@ log "Changing ownership" # note, change ownership after db migrate. this allow db migrate to move files around as root and then we can fix it up here # be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change chown -R "${USER}" /etc/cloudron -chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update" "${PLATFORM_DATA_DIR}/sftp" "${PLATFORM_DATA_DIR}/firewall" "${PLATFORM_DATA_DIR}/sshfs" "${PLATFORM_DATA_DIR}/cifs" +chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update" "${PLATFORM_DATA_DIR}/sftp" "${PLATFORM_DATA_DIR}/firewall" "${PLATFORM_DATA_DIR}/sshfs" "${PLATFORM_DATA_DIR}/cifs" "${PLATFORM_DATA_DIR}/tls" chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}" chown "${USER}:${USER}" "${APPS_DATA_DIR}" diff --git a/src/apps.js b/src/apps.js index f0023be88..271d4c2ea 100644 --- a/src/apps.js +++ b/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; -} diff --git a/src/blobs.js b/src/blobs.js index 35cc13e39..ae40e936b 100644 --- a/src/blobs.js +++ b/src/blobs.js @@ -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); +} diff --git a/src/cloudron.js b/src/cloudron.js index 0b5346756..7aa944d63 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -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) { diff --git a/src/directoryserver.js b/src/directoryserver.js index bcac36c26..8fd960677 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -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(); } diff --git a/src/docker.js b/src/docker.js index afb7574e1..120cc56a6 100644 --- a/src/docker.js +++ b/src/docker.js @@ -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 }); diff --git a/src/domains.js b/src/domains.js index f93d48843..3e72d406c 100644 --- a/src/domains.js +++ b/src/domains.js @@ -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 }); } diff --git a/src/eventlog.js b/src/eventlog.js index 3fbc6d791..6bdcc7f64 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -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', diff --git a/src/mail.js b/src/mail.js index ec64f86c7..5ea36adc9 100644 --- a/src/mail.js +++ b/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(); } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 509fbfe73..0fe6c0610 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -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'); -} diff --git a/src/services.js b/src/services.js index 7f40bb1a7..391c820e2 100644 --- a/src/services.js +++ b/src/services.js @@ -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 };