diff --git a/CHANGES b/CHANGES index a8d44603f..2a4541ea3 100644 --- a/CHANGES +++ b/CHANGES @@ -2261,4 +2261,5 @@ * namecheap: fix bug where records were not removed * add UI to disable 2FA of a user * mail: add active flag to mailboxes and lists +* Implement OCSP stapling diff --git a/src/cert/acme2.js b/src/cert/acme2.js index 47b368953..86a599083 100644 --- a/src/cert/acme2.js +++ b/src/cert/acme2.js @@ -323,25 +323,30 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof callback, 'function'); - var outdir = paths.APP_CERTS_DIR; + const outdir = paths.APP_CERTS_DIR; const certName = hostname.replace('*.', '_.'); - var csrFile = path.join(outdir, `${certName}.csr`); - var privateKeyFile = path.join(outdir, `${certName}.key`); + const csrFile = path.join(outdir, `${certName}.csr`); + const privateKeyFile = path.join(outdir, `${certName}.key`); 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); } else { - var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves + let key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error)); if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); debug('createKeyAndCsr: key file saved at %s', privateKeyFile); } - var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`); + const extensionArgs = `-addext "subjectAltName = DNS:${hostname}"` + + ' -addext "basicConstraints = CA:FALSE"' // this is not for a CA cert. cannot sign other certs with this + + ' -addext "keyUsage = nonRepudiation, digitalSignature, keyEncipherment"' + + ' -addext "tlsfeature = status_request"'; // this adds OCSP must-staple + + const csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname} ${extensionArgs}`); if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error)); - if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping + if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(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); diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index 1b8c1aac3..5e431f81e 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -84,6 +84,12 @@ server { ssl_dhparam /home/yellowtent/boxdata/dhparams.pem; add_header Strict-Transport-Security "max-age=63072000"; + <% if ( ocsp ) { -%> + # OCSP. LE certs are generated with must-staple flag so clients can enforce OCSP + ssl_stapling on; + ssl_stapling_verify on; + <% } %> + # https://github.com/twitter/secureheaders # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix # https://wiki.mozilla.org/Security/Guidelines/Web_Security diff --git a/src/reverseproxy.js b/src/reverseproxy.js index bd05fd471..42ae581ee 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -102,6 +102,12 @@ function isExpiringSync(certFilePath, hours) { return result.status === 1; // 1 - expired 0 - not expired } +function hasOCSPStapleSync(certFilePath) { + if (safe.child_process.execSync(`openssl x509 -text -noout -in ${certFilePath} | grep -q status_request`)) return true; + + return false; +} + // checks if the certificate matches the options provided by user (like wildcard, le-staging etc) function providerMatchesSync(domainObject, certFilePath, apiOptions) { assert.strictEqual(typeof domainObject, 'object'); @@ -382,7 +388,7 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) { assert.strictEqual(typeof vhost, 'string'); assert.strictEqual(typeof callback, 'function'); - var data = { + const data = { sourceDir: path.resolve(__dirname, '..'), adminOrigin: settings.adminOrigin(), vhost: vhost, @@ -391,10 +397,11 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) { certFilePath: bundle.certFilePath, keyFilePath: bundle.keyFilePath, robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'), - proxyAuth: { enabled: false, id: null, location: nginxLocation('/') } + proxyAuth: { enabled: false, id: null, location: nginxLocation('/') }, + ocsp: hasOCSPStapleSync(bundle.certFilePath) }; - var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); - var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName); + const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); + const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName); if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); @@ -456,7 +463,7 @@ function writeAppNginxConfig(app, fqdn, bundle, callback) { if (reverseProxyConfig.csp.includes('frame-ancestors ')) hideHeaders.push('X-Frame-Options'); } - var data = { + const data = { sourceDir: sourceDir, adminOrigin: settings.adminOrigin(), vhost: fqdn, @@ -474,9 +481,10 @@ function writeAppNginxConfig(app, fqdn, bundle, callback) { id: app.id, location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/') }, - httpPaths: app.manifest.httpPaths || {} + httpPaths: app.manifest.httpPaths || {}, + ocsp: hasOCSPStapleSync(bundle.certFilePath) }; - var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); + const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); const aliasSuffix = app.fqdn === fqdn ? '' : `-alias-${fqdn.replace('*', '_')}`; var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}${aliasSuffix}.conf`); @@ -496,7 +504,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) { assert.strictEqual(typeof bundle, 'object'); assert.strictEqual(typeof callback, 'function'); - var data = { + const data = { sourceDir: path.resolve(__dirname, '..'), vhost: fqdn, redirectTo: app.fqdn, @@ -507,12 +515,13 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) { robotsTxtQuoted: null, cspQuoted: null, hideHeaders: [], - proxyAuth: { enabled: false, id: app.id, location: nginxLocation('/') } + proxyAuth: { enabled: false, id: app.id, location: nginxLocation('/') }, + ocsp: hasOCSPStapleSync(bundle.certFilePath) }; - var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); + const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); // if we change the filename, also change it in unconfigureApp() - var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${fqdn}.conf`); + const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${fqdn}.conf`); debug('writing config for "%s" redirecting to "%s" to %s with options %j', app.fqdn, fqdn, nginxConfigFilename, data); if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) { @@ -725,7 +734,8 @@ function writeDefaultConfig(options, callback) { certFilePath, keyFilePath, robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'), - proxyAuth: { enabled: false, id: null, location: nginxLocation('/') } + 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);