'use strict'; exports = module.exports = { setUserCertificate, // per location certificate setFallbackCertificate, // per domain certificate generateFallbackCertificate, validateCertificate, getMailCertificate, getDirectoryServerCertificate, ensureCertificate, checkCerts, // the 'configure' functions ensure a certificate and generate nginx config configureApp, unconfigureApp, // these only generate nginx config writeDefaultConfig, writeDashboardConfig, writeAppConfigs, removeAppConfigs, restoreFallbackCertificates, }; const acme2 = require('./acme2.js'), apps = require('./apps.js'), assert = require('assert'), blobs = require('./blobs.js'), BoxError = require('./boxerror.js'), constants = require('./constants.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'), fs = require('fs'), mail = require('./mail.js'), os = require('os'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), shell = require('./shell.js'), sysinfo = require('./sysinfo.js'), util = require('util'); const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); function nginxLocation(s) { if (!s.startsWith('!')) return s; const re = s.replace(/[\^$\\.*+?()[\]{}|]/g, '\\$&'); // https://github.com/es-shims/regexp.escape/blob/master/implementation.js return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex } function getExpiryDateSync(cert) { assert(Buffer.isBuffer(cert)); const result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-enddate', '-noout' ], { input: cert }); if (!result) return null; // some error const notAfter = result.stdout.toString('utf8').trim().split('=')[1]; const notAfterDate = new Date(notAfter); const daysLeft = (notAfterDate - new Date())/(24 * 60 * 60 * 1000); debug(`expiryDate: notAfter=${notAfter} daysLeft=${daysLeft}`); return notAfterDate; } async function isOcspEnabled(certFilePath) { // on some servers, OCSP does not work. see #796 const config = await settings.getReverseProxyConfig(); if (!config.ocsp) return false; // We used to check for the must-staple in the cert using openssl x509 -text -noout -in ${certFilePath} | grep -q status_request // however, we cannot set the must-staple because first request to nginx fails because of it's OCSP caching behavior const result = safe.child_process.execSync(`openssl x509 -in ${certFilePath} -noout -ocsp_uri`, { encoding: 'utf8' }); return result && result.length > 0; // no error and has uri } // checks if the certificate matches the options provided by user (like wildcard, le-staging etc) function providerMatchesSync(domainObject, cert) { assert.strictEqual(typeof domainObject, 'object'); assert(Buffer.isBuffer(cert)); const subjectAndIssuer = safe.child_process.execSync('/usr/bin/openssl x509 -noout -subject -issuer', { encoding: 'utf8', input: cert }); if (!subjectAndIssuer) return false; // something bad happenned const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1]; const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1]; const isWildcardCert = domain.includes('*'); const isLetsEncryptProd = issuer.includes('Let\'s Encrypt') && !issuer.includes('STAGING'); const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' const wildcard = !!domainObject.tlsConfig.wildcard; const issuerMismatch = (prod && !isLetsEncryptProd) || (!prod && isLetsEncryptProd); // bare domain is not part of wildcard SAN const wildcardMismatch = (domain !== domainObject.domain) && (wildcard && !isWildcardCert) || (!wildcard && isWildcardCert); const mismatch = issuerMismatch || wildcardMismatch; debug(`providerMatchesSync: subject=${subject} domain=${domain} issuer=${issuer} ` + `wildcard=${isWildcardCert}/${wildcard} prod=${isLetsEncryptProd}/${prod} ` + `issuerMismatch=${issuerMismatch} wildcardMismatch=${wildcardMismatch} match=${!mismatch}`); return !mismatch; } // note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the // servers certificate appears first (and not the intermediate cert) function validateCertificate(subdomain, domain, certificate) { assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); assert(certificate && typeof certificate, 'object'); const { cert, key } = certificate; // check for empty cert and key strings if (!cert && key) return new BoxError(BoxError.BAD_FIELD, 'missing cert'); if (cert && !key) return new BoxError(BoxError.BAD_FIELD, 'missing key'); // -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN. const fqdn = dns.fqdn(subdomain, domain); let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert }); if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message); if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`); // check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert }); if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from certificate: ${safe.error.message}`); const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key }); if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`); if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.'); // check expiration result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert }); if (!result) return new BoxError(BoxError.BAD_FIELD, 'Certificate has expired.'); return null; } async function reload() { if (constants.TEST) return; const [error] = await safe(shell.promises.sudo('reload', [ RESTART_SERVICE_CMD, 'nginx' ], {})); if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`); } // this is used in migration - 20211006200150-domains-ensure-fallbackCertificate.js async function generateFallbackCertificate(domain) { assert.strictEqual(typeof domain, 'string'); const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`); const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`); const opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8'); // SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present! let opensslConfWithSan; const cn = domain; debug(`generateFallbackCertificate: domain=${domain} cn=${cn}`); opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`; const configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf'); safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8'); // the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176) const certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`); if (!safe.child_process.execSync(certCommand)) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error.message); safe.fs.unlinkSync(configFile); const cert = safe.fs.readFileSync(certFilePath, 'utf8'); if (!cert) throw new BoxError(BoxError.FS_ERROR, safe.error.message); safe.fs.unlinkSync(certFilePath); const key = safe.fs.readFileSync(keyFilePath, 'utf8'); if (!key) throw new BoxError(BoxError.FS_ERROR, safe.error.message); safe.fs.unlinkSync(keyFilePath); return { cert, key }; } async function setFallbackCertificate(domain, certificate) { assert.strictEqual(typeof domain, 'string'); assert(certificate && typeof certificate === 'object'); debug(`setFallbackCertificate: setting certs for domain ${domain}`); 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); await reload(); } async function restoreFallbackCertificates() { const result = await domains.list(); for (const domain of result) { if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.key`), domain.fallbackCertificate.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); } } function getAppLocationsSync(app) { assert.strictEqual(typeof app, 'object'); 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) { assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains) assert.strictEqual(typeof domainObject, 'object'); if (fqdn !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN return dns.makeWildcard(fqdn).replace('*.', '_.'); } else { return fqdn; } } function needsRenewalSync(cert) { assert(Buffer.isBuffer(cert)); 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 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`); if (location.certificate) return location.certificate; if (domainObject.tlsConfig.provider === 'fallback') return domainObject.fallbackCertificate; const certName = getAcmeCertificateNameSync(fqdn, domainObject); const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`); const key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`); if (!key || !cert) return domainObject.fallbackCertificate; return { key, cert }; } 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); let cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`); let key = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`); 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`); writeFileSync(certFilePath, cert); writeFileSync(keyFilePath, key); return { certFilePath, keyFilePath }; } async function ensureCertificate(location, auditSource) { assert.strictEqual(typeof location, 'object'); assert.strictEqual(typeof auditSource, 'object'); 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; } if (domainObject.tlsConfig.provider === 'fallback') { debug(`ensureCertificate: ${fqdn} will use fallback certs`); return; } 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) { 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`); } debug(`ensureCertificate: ${fqdn} needs acme cert`); const [error] = await safe(acme2.getCertificate(fqdn, domainObject)); debug(`ensureCertificate: error: ${error ? error.message : 'null'}`); await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: error?.message || '' })); } async function writeDashboardNginxConfig(vhost, certificatePath) { assert.strictEqual(typeof vhost, 'string'); assert.strictEqual(typeof certificatePath, 'object'); const data = { sourceDir: path.resolve(__dirname, '..'), vhost, hasIPv6: sysinfo.hasIPv6(), endpoint: 'dashboard', certFilePath: certificatePath.certFilePath, keyFilePath: certificatePath.keyFilePath, robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'), proxyAuth: { enabled: false, id: null, location: nginxLocation('/') }, ocsp: await isOcspEnabled(certificatePath.certFilePath) }; const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `dashboard/${vhost}.conf`); writeFileSync(nginxConfigFilename, nginxConf); } // also syncs the certs to disk async function writeDashboardConfig(domain) { assert.strictEqual(typeof domain, 'string'); 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 writeAppLocationNginxConfig(app, location, certificatePath) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof location, 'object'); assert.strictEqual(typeof certificatePath, 'object'); const type = location.type, vhost = location.fqdn; const data = { sourceDir: path.resolve(__dirname, '..'), vhost, hasIPv6: sysinfo.hasIPv6(), ip: null, port: null, endpoint: null, redirectTo: null, certFilePath: certificatePath.certFilePath, keyFilePath: certificatePath.keyFilePath, robotsTxtQuoted: null, cspQuoted: null, hideHeaders: [], proxyAuth: { enabled: false }, upstreamUri: '', // only for endpoint === external ocsp: await isOcspEnabled(certificatePath.certFilePath) }; if (type === apps.LOCATION_TYPE_PRIMARY || type === apps.LOCATION_TYPE_ALIAS || type === apps.LOCATION_TYPE_SECONDARY) { data.endpoint = 'app'; if (app.manifest.id === constants.PROXY_APP_APPSTORE_ID) { data.endpoint = 'external'; data.upstreamUri = app.upstreamUri; } // maybe these should become per domain at some point const reverseProxyConfig = app.reverseProxyConfig || {}; // some of our code uses fake app objects if (reverseProxyConfig.robotsTxt) data.robotsTxtQuoted = JSON.stringify(app.reverseProxyConfig.robotsTxt); if (reverseProxyConfig.csp) { data.cspQuoted = `"${app.reverseProxyConfig.csp}"`; data.hideHeaders = [ 'Content-Security-Policy' ]; if (reverseProxyConfig.csp.includes('frame-ancestors ')) data.hideHeaders.push('X-Frame-Options'); } if (type === apps.LOCATION_TYPE_PRIMARY || type == apps.LOCATION_TYPE_ALIAS) { data.proxyAuth = { enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth, id: app.id, location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/') }; data.ip = app.containerIp; data.port = app.manifest.httpPort; } else if (type === apps.LOCATION_TYPE_SECONDARY) { data.ip = app.containerIp; const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === vhost); data.port = app.manifest.httpPorts[secondaryDomain.environmentVariable].containerPort; } } else if (type === apps.LOCATION_TYPE_REDIRECT) { data.proxyAuth = { enabled: false, id: app.id, location: nginxLocation('/') }; data.endpoint = 'redirect'; data.redirectTo = app.fqdn; } const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); const filename = path.join(paths.NGINX_APPCONFIG_DIR, app.id, `${vhost.replace('*', '_')}.conf`); 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 = 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 writeCertificate(location); await writeAppLocationNginxConfig(app, location, certificatePath); } await reload(); } async function setUserCertificate(app, location) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof location, 'object'); 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 = getAppLocationsSync(app); for (const location of locations) { await ensureCertificate(location, auditSource); } await writeAppConfigs(app); if (app.manifest.addons?.tls) await setupTlsAddon(app); } async function unconfigureApp(app) { assert.strictEqual(typeof app, 'object'); if (!safe.fs.rmSync(path.join(paths.NGINX_APPCONFIG_DIR, app.id), { recursive: true, force: true })) throw new BoxError(BoxError.FS_ERROR, `Could not remove nginx config directory: ${safe.error.message}`); await reload(); } async function ensureCertificates(locations, auditSource, progressCallback) { assert(Array.isArray(locations)); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof progressCallback, 'function'); let percent = 1; for (const location of locations) { percent += Math.round(100/locations.length); progressCallback({ percent, message: `Ensuring certs of ${location.fqdn}` }); await ensureCertificate(location, auditSource); } } 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' }); const domainObjectMap = await domains.getDomainObjectMap(); const certNamesInUse = new Set(); for (const location of locations) { certNamesInUse.add(await getAcmeCertificateNameSync(location.fqdn, domainObjectMap[location.domain])); } 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; 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 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(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}-${certName}.key`); await blobs.del(`${blobs.CERT_PREFIX}-${certName}.cert`); await blobs.del(`${blobs.CERT_PREFIX}-${certName}.csr`); removedCertNames.push(certName); } } if (removedCertNames.length) await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_CLEANUP, auditSource, { domains: removedCertNames })); debug('cleanupCerts: done'); } async function checkCerts(auditSource, progressCallback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof progressCallback, 'function'); 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 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() { debug('removeAppConfigs: removing app nginx configs'); // remove all configs which are not the default or current dashboard for (const entry of fs.readdirSync(paths.NGINX_APPCONFIG_DIR, { withFileTypes: true })) { if (entry.isDirectory() && entry.name === 'dashboard') continue; if (entry.isFile() && entry.name === constants.NGINX_DEFAULT_CONFIG_FILE_NAME) continue; const fullPath = path.join(paths.NGINX_APPCONFIG_DIR, entry.name); if (entry.isDirectory()) { fs.rmSync(fullPath, { recursive: true, force: true }); } else if (entry.isFile()) { fs.unlinkSync(fullPath); } } } async function writeDefaultConfig(options) { assert.strictEqual(typeof options, 'object'); const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert'); const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key'); if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { debug('writeDefaultConfig: create new cert'); const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy // the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176) if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`)) { debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`); throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); } } const data = { sourceDir: path.resolve(__dirname, '..'), vhost: '', hasIPv6: sysinfo.hasIPv6(), endpoint: options.activated ? 'ip' : 'setup', certFilePath, keyFilePath, robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'), proxyAuth: { enabled: false, id: null, location: nginxLocation('/') }, ocsp: false // self-signed cert }; const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME); debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`); if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error); await reload(); }