404 hosts add update complete, fix certbot renewals

and remove the need for email and agreement on cert requests
This commit is contained in:
Jamie Curnow
2025-09-23 18:02:00 +10:00
parent f39efb3e63
commit 4240e00a46
34 changed files with 936 additions and 448 deletions

View File

@@ -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;

View File

@@ -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",