From fe4bd9fed6da28b4a30e8ef28dd1e09f11537bb9 Mon Sep 17 00:00:00 2001 From: Stephen Crosby Date: Thu, 14 Dec 2023 16:21:08 -0800 Subject: [PATCH 1/3] Make auto-renew use built-in renew function --- backend/internal/certificate.js | 86 ++++++++++++++------------------- 1 file changed, 35 insertions(+), 51 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index f68ef30b..512cdfb9 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -26,10 +26,11 @@ function omissions() { const internalCertificate = { - allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], - intervalTimeout: 1000 * 60 * 60, // 1 hour - interval: null, - intervalProcessing: false, + allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], + intervalTimeout: 1000 * 60 * 60, // 1 hour + interval: null, + intervalProcessing: false, + renewBeforeExpirationBy: [7, 'days'], initTimer: () => { logger.info('Let\'s Encrypt Renewal Timer initialized'); @@ -46,58 +47,41 @@ const internalCertificate = { internalCertificate.intervalProcessing = true; logger.info('Renewing SSL certs close to expiry...'); - const cmd = certbotCommand + ' renew --non-interactive --quiet ' + - '--config "' + letsencryptConfig + '" ' + - '--work-dir "/tmp/letsencrypt-lib" ' + - '--logs-dir "/tmp/letsencrypt-log" ' + - '--preferred-challenges "dns,http" ' + - '--disable-hook-validation ' + - (letsencryptStaging ? '--staging' : ''); + const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); - return utils.exec(cmd) - .then((result) => { - if (result) { - logger.info('Renew Result: ' + result); + // Fetch all the letsencrypt certs from the db that will expire within 7 days + certificateModel + .query() + .where('is_deleted', 0) + .andWhere('provider', 'letsencrypt') + .andWhere('expires_on', '<', expirationThreshold) + .then((certificates) => { + if (!certificates || !certificates.length) { + return null; } - return internalNginx.reload() - .then(() => { - logger.info('Renew Complete'); - return result; - }); - }) - .then(() => { - // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times - return certificateModel - .query() - .where('is_deleted', 0) - .andWhere('provider', 'letsencrypt') - .then((certificates) => { - if (certificates && certificates.length) { - let promises = []; + let promises = []; - certificates.map(function (certificate) { - promises.push( - internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .where('id', certificate.id) - .andWhere('provider', 'letsencrypt') - .patch({ - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }); - }) - .catch((err) => { - // Don't want to stop the train here, just log the error - logger.error(err.message); - }) - ); - }); + certificates.forEach(function (certificate) { + const promise = internalCertificate + .renew( + { + can: () => + Promise.resolve({ + permission_visibility: 'all', + }), + }, + { id: certificate.id }, + ) + .catch((err) => { + // Don't want to stop the train here, just log the error + logger.error(err.message); + }); - return Promise.all(promises); - } - }); + promises.push(promise); + }); + + return Promise.all(promises); }) .then(() => { internalCertificate.intervalProcessing = false; From f7d1c490b38449d8148ac01783ca467d90181a00 Mon Sep 17 00:00:00 2001 From: Stephen Crosby Date: Tue, 2 Jan 2024 14:04:16 -0800 Subject: [PATCH 2/3] Run renews sequentially --- backend/internal/certificate.js | 44 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 512cdfb9..28dc8276 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -30,7 +30,7 @@ const internalCertificate = { intervalTimeout: 1000 * 60 * 60, // 1 hour interval: null, intervalProcessing: false, - renewBeforeExpirationBy: [7, 'days'], + renewBeforeExpirationBy: [30, 'days'], initTimer: () => { logger.info('Let\'s Encrypt Renewal Timer initialized'); @@ -49,7 +49,7 @@ const internalCertificate = { const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); - // Fetch all the letsencrypt certs from the db that will expire within 7 days + // Fetch all the letsencrypt certs from the db that will expire within N days certificateModel .query() .where('is_deleted', 0) @@ -60,28 +60,32 @@ const internalCertificate = { return null; } - let promises = []; + /** + * Renews must be run sequentially or we'll get an error 'Another + * instance of Certbot is already running.' + */ + let sequence = Promise.resolve(); certificates.forEach(function (certificate) { - const promise = internalCertificate - .renew( - { - can: () => - Promise.resolve({ - permission_visibility: 'all', - }), - }, - { id: certificate.id }, - ) - .catch((err) => { - // Don't want to stop the train here, just log the error - logger.error(err.message); - }); - - promises.push(promise); + sequence = sequence.then(() => + internalCertificate + .renew( + { + can: () => + Promise.resolve({ + permission_visibility: 'all', + }), + }, + { id: certificate.id }, + ) + .catch((err) => { + // Don't want to stop the train here, just log the error + logger.error(err.message); + }), + ); }); - return Promise.all(promises); + return sequence; }) .then(() => { internalCertificate.intervalProcessing = false; From 9c54d1b7184bad39d364b70a34c91fa9e612f17b Mon Sep 17 00:00:00 2001 From: Stephen Crosby Date: Wed, 10 Jan 2024 20:05:50 -0800 Subject: [PATCH 3/3] Provide the token model for certificate renewal --- backend/internal/certificate.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 28dc8276..42310302 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -8,6 +8,7 @@ const config = require('../lib/config'); const error = require('../lib/error'); const utils = require('../lib/utils'); const certificateModel = require('../models/certificate'); +const tokenModel = require('../models/token'); const dnsPlugins = require('../global/certbot-dns-plugins'); const internalAuditLog = require('./audit-log'); const internalNginx = require('./nginx'); @@ -45,11 +46,11 @@ const internalCertificate = { processExpiringHosts: () => { if (!internalCertificate.intervalProcessing) { internalCertificate.intervalProcessing = true; - logger.info('Renewing SSL certs close to expiry...'); + logger.info('Renewing SSL certs expiring within ' + internalCertificate.renewBeforeExpirationBy[0] + ' ' + internalCertificate.renewBeforeExpirationBy[1] + ' ...'); const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); - // Fetch all the letsencrypt certs from the db that will expire within N days + // Fetch all the letsencrypt certs from the db that will expire within the configured threshold certificateModel .query() .where('is_deleted', 0) @@ -75,6 +76,7 @@ const internalCertificate = { Promise.resolve({ permission_visibility: 'all', }), + token: new tokenModel(), }, { id: certificate.id }, ) @@ -88,6 +90,7 @@ const internalCertificate = { return sequence; }) .then(() => { + logger.info('Completed SSL cert renew process'); internalCertificate.intervalProcessing = false; }) .catch((err) => {