mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-24 15:30:35 +00:00
404 hosts add update complete, fix certbot renewals
and remove the need for email and agreement on cert requests
This commit is contained in:
@@ -13,6 +13,7 @@ import utils from "../lib/utils.js";
|
||||
import { ssl as logger } from "../logger.js";
|
||||
import certificateModel from "../models/certificate.js";
|
||||
import tokenModel from "../models/token.js";
|
||||
import userModel from "../models/user.js";
|
||||
import internalAuditLog from "./audit-log.js";
|
||||
import internalHost from "./host.js";
|
||||
import internalNginx from "./nginx.js";
|
||||
@@ -81,7 +82,7 @@ const internalCertificate = {
|
||||
Promise.resolve({
|
||||
permission_visibility: "all",
|
||||
}),
|
||||
token: new tokenModel(),
|
||||
token: tokenModel(),
|
||||
},
|
||||
{ id: certificate.id },
|
||||
)
|
||||
@@ -118,10 +119,7 @@ const internalCertificate = {
|
||||
data.nice_name = data.domain_names.join(", ");
|
||||
}
|
||||
|
||||
const certificate = await certificateModel
|
||||
.query()
|
||||
.insertAndFetch(data)
|
||||
.then(utils.omitRow(omissions()));
|
||||
const certificate = await certificateModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
|
||||
|
||||
if (certificate.provider === "letsencrypt") {
|
||||
// Request a new Cert from LE. Let the fun begin.
|
||||
@@ -139,12 +137,19 @@ const internalCertificate = {
|
||||
// 2. Disable them in nginx temporarily
|
||||
await internalCertificate.disableInUseHosts(inUseResult);
|
||||
|
||||
const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first();
|
||||
if (!user || !user.email) {
|
||||
throw new error.ValidationError(
|
||||
"A valid email address must be set on your user account to use Let's Encrypt",
|
||||
);
|
||||
}
|
||||
|
||||
// With DNS challenge no config is needed, so skip 3 and 5.
|
||||
if (certificate.meta?.dns_challenge) {
|
||||
try {
|
||||
await internalNginx.reload();
|
||||
// 4. Request cert
|
||||
await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate);
|
||||
await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email);
|
||||
await internalNginx.reload();
|
||||
// 6. Re-instate previously disabled hosts
|
||||
await internalCertificate.enableInUseHosts(inUseResult);
|
||||
@@ -159,9 +164,9 @@ const internalCertificate = {
|
||||
try {
|
||||
await internalNginx.generateLetsEncryptRequestConfig(certificate);
|
||||
await internalNginx.reload();
|
||||
setTimeout(() => {}, 5000)
|
||||
setTimeout(() => {}, 5000);
|
||||
// 4. Request cert
|
||||
await internalCertificate.requestLetsEncryptSsl(certificate);
|
||||
await internalCertificate.requestLetsEncryptSsl(certificate, user.email);
|
||||
// 5. Remove LE config
|
||||
await internalNginx.deleteLetsEncryptRequestConfig(certificate);
|
||||
await internalNginx.reload();
|
||||
@@ -204,13 +209,12 @@ const internalCertificate = {
|
||||
data.meta = _.assign({}, data.meta || {}, certificate.meta);
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog
|
||||
.add(access, {
|
||||
action: "created",
|
||||
object_type: "certificate",
|
||||
object_id: certificate.id,
|
||||
meta: data,
|
||||
});
|
||||
await internalAuditLog.add(access, {
|
||||
action: "created",
|
||||
object_type: "certificate",
|
||||
object_id: certificate.id,
|
||||
meta: data,
|
||||
});
|
||||
|
||||
return certificate;
|
||||
},
|
||||
@@ -248,13 +252,12 @@ const internalCertificate = {
|
||||
}
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog
|
||||
.add(access, {
|
||||
action: "updated",
|
||||
object_type: "certificate",
|
||||
object_id: row.id,
|
||||
meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw
|
||||
});
|
||||
await internalAuditLog.add(access, {
|
||||
action: "updated",
|
||||
object_type: "certificate",
|
||||
object_id: row.id,
|
||||
meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw
|
||||
});
|
||||
|
||||
return savedRow;
|
||||
},
|
||||
@@ -268,7 +271,7 @@ const internalCertificate = {
|
||||
* @return {Promise}
|
||||
*/
|
||||
get: async (access, data) => {
|
||||
const accessData = await access.can("certificates:get", data.id)
|
||||
const accessData = await access.can("certificates:get", data.id);
|
||||
const query = certificateModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
@@ -367,12 +370,9 @@ const internalCertificate = {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
}
|
||||
|
||||
await certificateModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
await certificateModel.query().where("id", row.id).patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
|
||||
// Add to audit log
|
||||
row.meta = internalCertificate.cleanMeta(row.meta);
|
||||
@@ -435,10 +435,7 @@ const internalCertificate = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCount: async (userId, visibility) => {
|
||||
const query = certificateModel
|
||||
.query()
|
||||
.count("id as count")
|
||||
.where("is_deleted", 0);
|
||||
const query = certificateModel.query().count("id as count").where("is_deleted", 0);
|
||||
|
||||
if (visibility !== "all") {
|
||||
query.andWhere("owner_user_id", userId);
|
||||
@@ -501,12 +498,10 @@ const internalCertificate = {
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Array} data.domain_names
|
||||
* @param {String} data.meta.letsencrypt_email
|
||||
* @param {Boolean} data.meta.letsencrypt_agree
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createQuickCertificate: async (access, data) => {
|
||||
return internalCertificate.create(access, {
|
||||
return await internalCertificate.create(access, {
|
||||
provider: "letsencrypt",
|
||||
domain_names: data.domain_names,
|
||||
meta: data.meta,
|
||||
@@ -652,7 +647,7 @@ const internalCertificate = {
|
||||
const certData = {};
|
||||
|
||||
try {
|
||||
const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"])
|
||||
const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]);
|
||||
// Examples:
|
||||
// subject=CN = *.jc21.com
|
||||
// subject=CN = something.example.com
|
||||
@@ -739,9 +734,10 @@ const internalCertificate = {
|
||||
/**
|
||||
* Request a certificate using the http challenge
|
||||
* @param {Object} certificate the certificate row
|
||||
* @param {String} email the email address to use for registration
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestLetsEncryptSsl: async (certificate) => {
|
||||
requestLetsEncryptSsl: async (certificate, email) => {
|
||||
logger.info(
|
||||
`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
|
||||
);
|
||||
@@ -760,7 +756,7 @@ const internalCertificate = {
|
||||
"--authenticator",
|
||||
"webroot",
|
||||
"--email",
|
||||
certificate.meta.letsencrypt_email,
|
||||
email,
|
||||
"--preferred-challenges",
|
||||
"dns,http",
|
||||
"--domains",
|
||||
@@ -779,9 +775,10 @@ const internalCertificate = {
|
||||
|
||||
/**
|
||||
* @param {Object} certificate the certificate row
|
||||
* @param {String} email the email address to use for registration
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestLetsEncryptSslWithDnsChallenge: async (certificate) => {
|
||||
requestLetsEncryptSslWithDnsChallenge: async (certificate, email) => {
|
||||
await installPlugin(certificate.meta.dns_provider);
|
||||
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
|
||||
logger.info(
|
||||
@@ -807,7 +804,7 @@ const internalCertificate = {
|
||||
`npm-${certificate.id}`,
|
||||
"--agree-tos",
|
||||
"--email",
|
||||
certificate.meta.letsencrypt_email,
|
||||
email,
|
||||
"--domains",
|
||||
certificate.domain_names.join(","),
|
||||
"--authenticator",
|
||||
@@ -847,7 +844,7 @@ const internalCertificate = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
renew: async (access, data) => {
|
||||
await access.can("certificates:update", data)
|
||||
await access.can("certificates:update", data);
|
||||
const certificate = await internalCertificate.get(access, data);
|
||||
|
||||
if (certificate.provider === "letsencrypt") {
|
||||
@@ -860,11 +857,9 @@ const internalCertificate = {
|
||||
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
|
||||
);
|
||||
|
||||
const updatedCertificate = await certificateModel
|
||||
.query()
|
||||
.patchAndFetchById(certificate.id, {
|
||||
expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, {
|
||||
expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
@@ -1159,7 +1154,9 @@ const internalCertificate = {
|
||||
return "no-host";
|
||||
}
|
||||
// Other errors
|
||||
logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`);
|
||||
logger.info(
|
||||
`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`,
|
||||
);
|
||||
return `other:${result.responsecode}`;
|
||||
}
|
||||
|
||||
@@ -1201,7 +1198,7 @@ const internalCertificate = {
|
||||
|
||||
getLiveCertPath: (certificateId) => {
|
||||
return `/etc/letsencrypt/live/npm-${certificateId}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default internalCertificate;
|
||||
|
@@ -54,10 +54,21 @@ const internalDeadHost = {
|
||||
thisData.advanced_config = "";
|
||||
}
|
||||
|
||||
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
||||
const row = await deadHostModel.query()
|
||||
.insertAndFetch(thisData)
|
||||
.then(utils.omitRow(omissions()));
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
action: "created",
|
||||
object_type: "dead-host",
|
||||
object_id: row.id,
|
||||
meta: _.assign({}, data.meta || {}, row.meta),
|
||||
});
|
||||
|
||||
if (createCertificate) {
|
||||
const cert = await internalCertificate.createQuickCertificate(access, data);
|
||||
|
||||
// update host with cert id
|
||||
await internalDeadHost.update(access, {
|
||||
id: row.id,
|
||||
@@ -71,17 +82,13 @@ const internalDeadHost = {
|
||||
expand: ["certificate", "owner"],
|
||||
});
|
||||
|
||||
// Sanity check
|
||||
if (createCertificate && !freshRow.certificate_id) {
|
||||
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
|
||||
}
|
||||
|
||||
// Configure nginx
|
||||
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
|
||||
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
action: "created",
|
||||
object_type: "dead-host",
|
||||
object_id: freshRow.id,
|
||||
meta: data,
|
||||
});
|
||||
|
||||
return freshRow;
|
||||
},
|
||||
@@ -94,7 +101,6 @@ const internalDeadHost = {
|
||||
*/
|
||||
update: async (access, data) => {
|
||||
const createCertificate = data.certificate_id === "new";
|
||||
|
||||
if (createCertificate) {
|
||||
delete data.certificate_id;
|
||||
}
|
||||
@@ -147,6 +153,13 @@ const internalDeadHost = {
|
||||
|
||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
||||
|
||||
|
||||
// do the row update
|
||||
await deadHostModel
|
||||
.query()
|
||||
.where({id: data.id})
|
||||
.patch(data);
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
action: "updated",
|
||||
|
@@ -6,46 +6,6 @@ import utils from "./utils.js";
|
||||
|
||||
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
|
||||
|
||||
/**
|
||||
* @param {array} pluginKeys
|
||||
*/
|
||||
const installPlugins = async (pluginKeys) => {
|
||||
let hasErrors = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pluginKeys.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
batchflow(pluginKeys)
|
||||
.sequential()
|
||||
.each((_i, pluginKey, next) => {
|
||||
certbot
|
||||
.installPlugin(pluginKey)
|
||||
.then(() => {
|
||||
next();
|
||||
})
|
||||
.catch((err) => {
|
||||
hasErrors = true;
|
||||
next(err);
|
||||
});
|
||||
})
|
||||
.error((err) => {
|
||||
logger.error(err.message);
|
||||
})
|
||||
.end(() => {
|
||||
if (hasErrors) {
|
||||
reject(
|
||||
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs a cerbot plugin given the key for the object from
|
||||
* ../global/certbot-dns-plugins.json
|
||||
@@ -84,4 +44,43 @@ const installPlugin = async (pluginKey) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {array} pluginKeys
|
||||
*/
|
||||
const installPlugins = async (pluginKeys) => {
|
||||
let hasErrors = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pluginKeys.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
batchflow(pluginKeys)
|
||||
.sequential()
|
||||
.each((_i, pluginKey, next) => {
|
||||
installPlugin(pluginKey)
|
||||
.then(() => {
|
||||
next();
|
||||
})
|
||||
.catch((err) => {
|
||||
hasErrors = true;
|
||||
next(err);
|
||||
});
|
||||
})
|
||||
.error((err) => {
|
||||
logger.error(err.message);
|
||||
})
|
||||
.end(() => {
|
||||
if (hasErrors) {
|
||||
reject(
|
||||
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { installPlugins, installPlugin };
|
||||
|
@@ -98,6 +98,8 @@ router
|
||||
name: dnsPlugins[key].name,
|
||||
credentials: dnsPlugins[key].credentials,
|
||||
}));
|
||||
|
||||
clean.sort((a, b) => a.name.localeCompare(b.name));
|
||||
res.status(200).send(clean);
|
||||
} catch (err) {
|
||||
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
|
@@ -62,15 +62,9 @@
|
||||
"dns_provider_credentials": {
|
||||
"type": "string"
|
||||
},
|
||||
"letsencrypt_agree": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"letsencrypt_certificate": {
|
||||
"type": "object"
|
||||
},
|
||||
"letsencrypt_email": {
|
||||
"$ref": "../common.json#/properties/email"
|
||||
},
|
||||
"propagation_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
|
@@ -36,8 +36,6 @@
|
||||
"domain_names": ["test.example.com"],
|
||||
"expires_on": "2025-01-07T04:34:18.000Z",
|
||||
"meta": {
|
||||
"letsencrypt_email": "jc@jc21.com",
|
||||
"letsencrypt_agree": true,
|
||||
"dns_challenge": false
|
||||
}
|
||||
}
|
||||
|
@@ -37,8 +37,6 @@
|
||||
"nice_name": "My Test Cert",
|
||||
"domain_names": ["test.jc21.supernerd.pro"],
|
||||
"meta": {
|
||||
"letsencrypt_email": "jc@jc21.com",
|
||||
"letsencrypt_agree": true,
|
||||
"dns_challenge": false
|
||||
}
|
||||
}
|
||||
|
@@ -36,8 +36,6 @@
|
||||
"domain_names": ["test.example.com"],
|
||||
"expires_on": "2025-01-07T04:34:18.000Z",
|
||||
"meta": {
|
||||
"letsencrypt_email": "jc@jc21.com",
|
||||
"letsencrypt_agree": true,
|
||||
"dns_challenge": false
|
||||
}
|
||||
}
|
||||
|
@@ -52,8 +52,6 @@
|
||||
"nice_name": "test.example.com",
|
||||
"domain_names": ["test.example.com"],
|
||||
"meta": {
|
||||
"letsencrypt_email": "jc@jc21.com",
|
||||
"letsencrypt_agree": true,
|
||||
"dns_challenge": false,
|
||||
"letsencrypt_certificate": {
|
||||
"cn": "test.example.com",
|
||||
|
@@ -121,11 +121,13 @@ const setupCertbotPlugins = async () => {
|
||||
// Make sure credentials file exists
|
||||
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
|
||||
// Escape single quotes and backslashes
|
||||
const escapedCredentials = certificate.meta.dns_provider_credentials
|
||||
.replaceAll("'", "\\'")
|
||||
.replaceAll("\\", "\\\\");
|
||||
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
|
||||
promises.push(utils.exec(credentials_cmd));
|
||||
if (typeof certificate.meta.dns_provider_credentials === "string") {
|
||||
const escapedCredentials = certificate.meta.dns_provider_credentials
|
||||
.replaceAll("'", "\\'")
|
||||
.replaceAll("\\", "\\\\");
|
||||
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
|
||||
promises.push(utils.exec(credentials_cmd));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
Reference in New Issue
Block a user