diff --git a/migrations/20210505223829-blobs-migrate-certs.js b/migrations/20210505223829-blobs-migrate-certs.js new file mode 100644 index 000000000..d44d6375a --- /dev/null +++ b/migrations/20210505223829-blobs-migrate-certs.js @@ -0,0 +1,43 @@ +'use strict'; + +const async = require('async'), + child_process = require('child_process'), + fs = require('fs'), + path = require('path'); + +const OLD_CERTS_DIR = '/home/yellowtent/boxdata/certs'; +const NEW_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert'; + +exports.up = function(db, callback) { + fs.readdir(OLD_CERTS_DIR, function (error, filenames) { + if (error && error.code === 'ENOENT') return callback(); + if (error) return callback(error); + + filenames = filenames.filter(f => f.endsWith('.key') && !f.endsWith('.host.key') && !f.endsWith('.user.key')); // ignore fallback and user keys + + async.eachSeries(filenames, function (filename, iteratorCallback) { + const privateKeyFile = filename; + const privateKey = fs.readFileSync(path.join(OLD_CERTS_DIR, filename)); + const certificateFile = filename.replace(/\.key$/, '.cert'); + const certificate = fs.readFileSync(path.join(OLD_CERTS_DIR, certificateFile)); + const csrFile = filename.replace(/\.key$/, '.csr'); + const csr = fs.readFileSync(path.join(OLD_CERTS_DIR, csrFile)); + + async.series([ + db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${privateKeyFile}`, privateKey), + db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${certificateFile}`, certificate), + db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${csrFile}`, csr), + ], iteratorCallback); + }, function (error) { + if (error) return callback(error); + + child_process.execSync(`cp ${OLD_CERTS_DIR}/* ${NEW_CERTS_DIR}`); // this way we copy the non-migrated ones like .host, .user etc as well + fs.rmdir(OLD_CERTS_DIR, { recursive: true }, callback); + }); + }); +}; + +exports.down = function(db, callback) { + callback(); +}; + diff --git a/setup/start.sh b/setup/start.sh index a602796ca..7bcb32511 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -63,9 +63,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \ mkdir -p "${PLATFORM_DATA_DIR}/update" mkdir -p "${PLATFORM_DATA_DIR}/sftp/ssh" # sftp keys mkdir -p "${PLATFORM_DATA_DIR}/firewall" -mkdir -p "${PLATFORM_DATA_DIR}/certs" -mkdir -p "${BOX_DATA_DIR}/certs" mkdir -p "${BOX_DATA_DIR}/mail/dkim" # ensure backups folder exists and is writeable diff --git a/src/blobs.js b/src/blobs.js index 76036914c..58c2d3520 100644 --- a/src/blobs.js +++ b/src/blobs.js @@ -14,6 +14,8 @@ exports = module.exports = { SFTP_PUBLIC_KEY: 'sftp_public_key', SFTP_PRIVATE_KEY: 'sftp_private_key', + CERT_PREFIX: 'cert', + _clear: clear }; diff --git a/src/cert/acme2.js b/src/cert/acme2.js index fae665285..9b2cf689c 100644 --- a/src/cert/acme2.js +++ b/src/cert/acme2.js @@ -2,6 +2,7 @@ const assert = require('assert'), async = require('async'), + blobs = require('../blobs.js'), BoxError = require('../boxerror.js'), crypto = require('crypto'), debug = require('debug')('box:cert/acme2'), @@ -274,11 +275,24 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe Acme2.prototype.createKeyAndCsr = async function (hostname) { assert.strictEqual(typeof hostname, 'string'); - const outdir = paths.APP_CERTS_DIR; + const outdir = paths.NGINX_CERT_DIR; const certName = hostname.replace('*.', '_.'); const csrFile = path.join(outdir, `${certName}.csr`); const privateKeyFile = path.join(outdir, `${certName}.key`); + let privateKey = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`); + if (!privateKeyFile) { + debug(`createKeyAndCsr: reuse the key for renewal at ${privateKeyFile}`); + } else { + debug('createKeyAndCsr: create new key'); + privateKey = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves + if (!privateKey) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + await blobs.set(`${blobs.CERT_PREFIX}-${certName}.key`, privateKey); + } + + if (!safe.fs.writeFileSync(privateKeyFile, privateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not write private key: ${safe.error.message}`); + debug(`createKeyAndCsr: key file saved at ${privateKeyFile}`); + if (safe.fs.existsSync(privateKeyFile)) { // in some old releases, csr file was corrupt. so always regenerate it debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile); @@ -298,6 +312,7 @@ Acme2.prototype.createKeyAndCsr = async function (hostname) { const csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname} ${extensionArgs}`); if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csrDer); if (!safe.fs.writeFileSync(csrFile, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile); @@ -309,7 +324,7 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof certUrl, 'string'); - const outdir = paths.APP_CERTS_DIR; + const outdir = paths.NGINX_CERT_DIR; await promiseRetry({ times: 5, interval: 20000 }, async () => { debug('downloadCertificate: downloading certificate'); @@ -322,6 +337,7 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl) { const certName = hostname.replace('*.', '_.'); const certificateFile = path.join(outdir, `${certName}.cert`); + await blobs.set(`${blobs.CERT_PREFIX}-${certName}.cert`, fullChainPem); if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error); debug(`downloadCertificate: cert file for ${hostname} saved at ${certificateFile}`); @@ -519,7 +535,7 @@ Acme2.prototype.getCertificate = async function (vhost, domain) { await this.loadDirectory(); await this.acmeFlow(vhost, domain); - const outdir = paths.APP_CERTS_DIR; + const outdir = paths.NGINX_CERT_DIR; const certName = vhost.replace('*.', '_.'); return { certFilePath: path.join(outdir, `${certName}.cert`), keyFilePath: path.join(outdir, `${certName}.key`) }; diff --git a/src/paths.js b/src/paths.js index 7dbfbb6fc..8ceff2c71 100644 --- a/src/paths.js +++ b/src/paths.js @@ -48,7 +48,6 @@ exports = module.exports = { // this is not part of appdata because an icon may be set before install MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'), - APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'), LOG_DIR: path.join(baseDir(), 'platformdata/logs'), TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'), diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 1a57f24d3..a792700e8 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -234,8 +234,8 @@ function setFallbackCertificate(domain, fallback, callback) { assert.strictEqual(typeof callback, 'function'); debug(`setFallbackCertificate: setting certs for domain ${domain}`); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); // TODO: maybe the cert is being used by the mail container reload(callback); @@ -248,8 +248,8 @@ function restoreFallbackCertificates(callback) { if (error) return callback(error); result.forEach(function (domain) { - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain.domain}.host.key`), domains.fallbackCertificate.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.key`), domains.fallbackCertificate.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); }); callback(null); @@ -259,8 +259,8 @@ function restoreFallbackCertificates(callback) { function getFallbackCertificatePathSync(domain) { assert.strictEqual(typeof domain, 'string'); - const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`); - const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`); + 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 }; } @@ -273,11 +273,11 @@ function setAppCertificate(location, domainObject, certificate, callback) { let fqdn = domains.fqdn(location, domainObject); if (certificate.cert && certificate.key) { - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error; - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error; + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error; + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error; } else { // remove existing cert/key - if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message); - if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message); + if (!safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message); + if (!safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message); } reload(callback); @@ -292,13 +292,13 @@ function getAcmeCertificatePath(vhost, domainObject, callback) { if (vhost !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN let certName = domains.makeWildcard(vhost).replace('*.', '_.'); - certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`); - keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`); + certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`); + keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`); if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath }); } else { - certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`); - keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`); + certFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.cert`); + keyFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.key`); if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath }); } @@ -319,8 +319,8 @@ function getCertificatePath(fqdn, domain, callback) { if (error) return callback(error); // user cert always wins - let certFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`); - let keyFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`); + let certFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`); + let keyFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`); if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath }); @@ -344,8 +344,8 @@ function ensureCertificate(vhost, domain, auditSource, callback) { if (error) return callback(error); // user cert always wins - let certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`); - let keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`); + let certFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.user.cert`); + let keyFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.user.key`); if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { debug(`ensureCertificate: ${vhost} will use custom app certs`); diff --git a/src/routes/test/domains-test.js b/src/routes/test/domains-test.js index 5f850a72a..079f4db24 100644 --- a/src/routes/test/domains-test.js +++ b/src/routes/test/domains-test.js @@ -343,10 +343,10 @@ describe('Domains API', function () { }); it('did set the certificate', function (done) { - var cert = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.cert`), 'utf-8'); + var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, `${DOMAIN_0.domain}.host.cert`), 'utf-8'); expect(cert).to.eql(validCert1); - var key = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.key`), 'utf-8'); + var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, `${DOMAIN_0.domain}.host.key`), 'utf-8'); expect(key).to.eql(validKey1); done();