diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 2acd8956..1b6689bb 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -13,6 +13,7 @@ const internalNginx = require('./nginx'); const internalHost = require('./host'); const certbot_command = '/usr/bin/certbot'; const le_config = '/etc/letsencrypt.ini'; +const dns_plugins = require('../../utils/certbot-dns-plugins') function omissions() { return ['is_deleted']; @@ -141,11 +142,11 @@ const internalCertificate = { }); }) .then((in_use_result) => { - // Is CloudFlare, no config needed, so skip 3 and 5. - if (data.meta.cloudflare_use) { + // With DNS challenge no config is needed, so skip 3 and 5. + if (certificate.meta.dns_challenge) { return internalNginx.reload().then(() => { // 4. Request cert - return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token); + return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); }) .then(internalNginx.reload) .then(() => { @@ -772,35 +773,58 @@ const internalCertificate = { }, /** - * @param {Object} certificate the certificate row - * @param {String} apiToken the cloudflare api token + * @param {Object} certificate the certificate row + * @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`) + * @param {String | null} credentials the content of this providers credentials file + * @param {String} propagation_seconds the cloudflare api token * @returns {Promise} */ - requestLetsEncryptCloudFlareDnsSsl: (certificate, apiToken) => { - logger.info('Requesting Let\'sEncrypt certificates via Cloudflare DNS for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); + requestLetsEncryptSslWithDnsChallenge: (certificate) => { + const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - let tokenLoc = '~/cloudflare-token'; - let storeKey = 'echo "dns_cloudflare_api_token = ' + apiToken + '" > ' + tokenLoc; + if(!dns_plugin){ + throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`) + } - let cmd = - storeKey + ' && ' + + logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); + + const credentials_loc = `/etc/letsencrypt/credentials-${certificate.id}`; + const credentials_cmd = `echo '${certificate.meta.dns_provider_credentials.replace("'", "\'")}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'`; + const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version; + + const main_cmd = certbot_command + ' certonly --non-interactive ' + '--cert-name "npm-' + certificate.id + '" ' + '--agree-tos ' + '--email "' + certificate.meta.letsencrypt_email + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' + - '--dns-cloudflare --dns-cloudflare-credentials ' + tokenLoc + - (le_staging ? ' --staging' : '') - + ' && rm ' + tokenLoc; + '--authenticator ' + dns_plugin.full_plugin_name + ' ' + + '--' + dns_plugin.full_plugin_name + '-credentials "' + credentials_loc + '"' + + ( + certificate.meta.propagation_seconds !== undefined + ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds + : '' + ) + + (le_staging ? ' --staging' : ''); + + const teardown_cmd = `rm '${credentials_loc}'`; if (debug_mode) { - logger.info('Command:', cmd); + logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd} && ${teardown_cmd}`); } - return utils.exec(cmd).then((result) => { - logger.info(result); - return result; - }); + return utils.exec(credentials_cmd) + .then(() => { + return utils.exec(prepare_cmd) + .then(() => { + return utils.exec(main_cmd) + .then(async (result) => { + await utils.exec(teardown_cmd); + logger.info(result); + return result; + }); + }); + }); }, @@ -817,7 +841,7 @@ const internalCertificate = { }) .then((certificate) => { if (certificate.provider === 'letsencrypt') { - let renewMethod = certificate.meta.cloudflare_use ? internalCertificate.renewLetsEncryptCloudFlareSsl : internalCertificate.renewLetsEncryptSsl; + let renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl; return renewMethod(certificate) .then(() => { @@ -877,22 +901,42 @@ const internalCertificate = { * @param {Object} certificate the certificate row * @returns {Promise} */ - renewLetsEncryptCloudFlareSsl: (certificate) => { - logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); + renewLetsEncryptSslWithDnsChallenge: (certificate) => { + const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - let cmd = certbot_command + ' renew --non-interactive ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--disable-hook-validation ' + - (le_staging ? '--staging' : ''); - - if (debug_mode) { - logger.info('Command:', cmd); + if(!dns_plugin){ + throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`) } - return utils.exec(cmd) - .then((result) => { - logger.info(result); - return result; + logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); + + const credentials_loc = `/etc/letsencrypt/credentials-${certificate.id}`; + const credentials_cmd = `echo '${certificate.meta.dns_provider_credentials.replace("'", "\'")}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'`; + const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version; + + const main_cmd = + certbot_command + ' renew --non-interactive ' + + '--cert-name "npm-' + certificate.id + '" ' + + '--disable-hook-validation' + + (le_staging ? ' --staging' : ''); + + const teardown_cmd = `rm '${credentials_loc}'`; + + if (debug_mode) { + logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd} && ${teardown_cmd}`); + } + + return utils.exec(credentials_cmd) + .then(() => { + return utils.exec(prepare_cmd) + .then(() => { + return utils.exec(main_cmd) + .then(async (result) => { + await utils.exec(teardown_cmd); + logger.info(result); + return result; + }); + }); }); }, diff --git a/backend/schema/endpoints/certificates.json b/backend/schema/endpoints/certificates.json index 27ea2d28..49fd6a7d 100644 --- a/backend/schema/endpoints/certificates.json +++ b/backend/schema/endpoints/certificates.json @@ -42,11 +42,23 @@ "letsencrypt_agree": { "type": "boolean" }, - "cloudflare_use": { + "dns_challenge": { "type": "boolean" }, - "cloudflare_token": { + "dns_provider": { "type": "string" + }, + "dns_provider_credentials": { + "type": "string" + }, + "propagation_seconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + } + ] + } } }