Merge pull request #6004 from mikeyhenein/certbot-errors

Show certbot's failure reason in the UI instead of "Internal Error"
This commit is contained in:
jc21 authored and GitHub committed 2026-11-04 14:33:39 +10:00
commit e3631e88c6
1 file changed
+103 -5
+103 -5
View File
@@ -24,6 +24,104 @@ const certbotCommand = "certbot";
const certbotLogsDir = "/data/logs";
const certbotWorkDir = "/tmp/letsencrypt-lib";
// Certbot output lines that say nothing about why a request failed
const certbotNoise = [
/^Saving debug log/,
/^Ask for help/,
/^See the logfile/,
/^(- )+-?$/,
/^Processing \//,
/^Renewing an existing/,
/^Requesting a certificate/,
/^Plugins selected/,
/^Account registered/,
/^Waiting \d+ seconds/,
/^All renewals failed/,
/^\S+\/fullchain\.pem \(failure\)$/,
/^\d+ renew failure\(s\)/,
];
// Plain-language hints for failures that are common and fixable outside NPM
const certbotHints = [
[
/expected \S+ to be a symlink/i,
() =>
"This certificate's files in /etc/letsencrypt were copied without their symlinks (live/ must link to archive/). Restore /etc/letsencrypt from a copy made with tar or rsync -a, or delete this certificate and create it again.",
],
[
/Renewal configuration file \S+ is broken/i,
() =>
"This certificate's files in /etc/letsencrypt are damaged or missing. Restore /etc/letsencrypt from a backup, or delete this certificate and create it again.",
],
[
/\b9109\b.*?location: ([0-9a-f.:]+)/i,
(m) => {
const ip = m[1].replace(/\.+$/, "");
return `Cloudflare refused the API token from this server's public IP (${ip}). The token has Client IP Address Filtering: add ${ip} to the token in Cloudflare (My Profile > API Tokens), or remove the filter, then renew again.`;
},
],
[
/\b(6003|6111|9103|9109|10000)\b|Invalid request headers|Invalid access token|Authentication error/i,
() =>
"Cloudflare rejected the API token. Check the token value and that it has Zone > DNS > Edit for this domain's zone.",
],
[
/too many (certificates|failed authorizations|new orders)|rateLimited/i,
() => "Let's Encrypt rate limit reached. Wait before retrying (see https://letsencrypt.org/docs/rate-limits/).",
],
[
/NXDOMAIN|DNS problem/i,
() => "Let's Encrypt could not look up the domain. Check the DNS records of every domain on the certificate.",
],
[
/Timeout during connect|Connection refused|Invalid response from/i,
() =>
"Let's Encrypt could not reach this server over HTTP (port 80) for the challenge. Check that the domain points here and port 80 is open, or use a DNS challenge.",
],
];
/**
* Turns certbot's stderr into a message the UI can show: a hint for common causes plus certbot's own reason.
* Certbot never prints DNS provider credentials.
*
* @param {String} stderr
* @returns {String}
*/
const certbotFailureMessage = (stderr) => {
const reason = `${stderr || ""}`
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !certbotNoise.some((re) => re.test(line)))
.join(" ")
.slice(0, 800);
for (const [pattern, hint] of certbotHints) {
const match = reason.match(pattern);
if (match) {
return `${hint(match)} (Certbot: ${reason})`;
}
}
return reason ? `Certbot failed: ${reason}` : "Certbot failed. See /data/logs/letsencrypt.log for details.";
};
/**
* Runs certbot; a failure becomes a public error with the real reason instead of "Internal Error".
*
* @param {Array} args
* @param {Object} [opts]
* @returns {Promise}
*/
const runCertbot = async (args, opts) => {
try {
return await utils.execFile(certbotCommand, args, opts);
} catch (err) {
if (err instanceof error.CommandError) {
throw new error.ValidationError(certbotFailureMessage(err.message), err);
}
throw err;
}
};
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials", "meta.certificate_key"];
};
@@ -843,7 +941,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.success(result);
return result;
},
@@ -910,7 +1008,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.info(result);
return result;
} finally {
@@ -1004,7 +1102,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.info(result);
return result;
},
@@ -1088,7 +1186,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.info(result);
return result;
} finally {
@@ -1131,7 +1229,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`);
logger.info(result);
return result;