Add cluster-managed certificate renewal

This commit is contained in:
2026-09-19 13:07:20 -04:00
parent d58e059c8e
commit 59f14484ea
+255 -4
View File
@@ -78,17 +78,220 @@ CLUSTER_RENEW_THROW_ANCHOR = (
"\"Only Let'sEncrypt certificates can be renewed\");" "\"Only Let'sEncrypt certificates can be renewed\");"
) )
CLUSTER_RENEW_THROW_FRAGMENT = '''if ( CLUSTER_RENEW_THROW_FRAGMENT = """if (
certificate.provider === "other" && certificate.provider === "other" &&
certificate.meta?.cluster_managed === true && certificate.meta?.cluster_managed === true &&
certificate.meta?.acme_renew === true certificate.meta?.acme_renew === true
) { ) {
throw new error.ValidationError( return await internalCertificate.renewClusterManagedSsl(
"Cluster-managed ACME renewal path is not yet enabled", access,
certificate,
); );
} }
throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");''' throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");"""
CLUSTER_RENEW_HELPER_ANCHOR = (
"\trenewLetsEncryptSsl: async (certificate) => {"
)
CLUSTER_RENEW_HELPER_FRAGMENT = """\trenewClusterManagedSsl: async (access, certificate) => {
requireAcmeAuthority();
if (
certificate.provider !== "other" ||
certificate.meta?.cluster_managed !== true ||
certificate.meta?.acme_renew !== true
) {
throw new error.ValidationError(
"Certificate is not eligible for cluster-managed ACME renewal",
);
}
const livePath = internalCertificate.getLiveCertPath(certificate.id);
if (internalCertificate.hasLetsEncryptSslCerts(certificate)) {
const renewMethod = certificate.meta?.dns_challenge
? internalCertificate.renewLetsEncryptSslWithDnsChallenge
: internalCertificate.renewLetsEncryptSsl;
await renewMethod(certificate);
} else {
const user = await userModel
.query()
.where("is_deleted", 0)
.andWhere("id", certificate.owner_user_id)
.first();
if (!user?.email) {
throw new error.ValidationError(
"A valid email address must be set on the certificate owner account to use Let's Encrypt",
);
}
const inUseResult = await internalHost.getHostsWithDomains(
certificate.domain_names,
);
await internalCertificate.disableInUseHosts(inUseResult);
if (certificate.meta?.dns_challenge) {
try {
await internalNginx.reload();
await internalCertificate.requestLetsEncryptSslWithDnsChallenge(
certificate,
user.email,
);
await internalNginx.reload();
await internalCertificate.enableInUseHosts(inUseResult);
} catch (err) {
await internalCertificate.enableInUseHosts(inUseResult);
await internalNginx.reload();
throw err;
}
} else {
try {
await internalNginx.generateLetsEncryptRequestConfig(
certificate,
);
await internalNginx.reload();
await internalCertificate.requestLetsEncryptSsl(
certificate,
user.email,
);
await internalNginx.deleteLetsEncryptRequestConfig(
certificate,
);
await internalNginx.reload();
await internalCertificate.enableInUseHosts(inUseResult);
} catch (err) {
await internalNginx.deleteLetsEncryptRequestConfig(
certificate,
);
await internalCertificate.enableInUseHosts(inUseResult);
await internalNginx.reload();
throw err;
}
}
}
const certPath = `${livePath}/cert.pem`;
const chainPath = `${livePath}/chain.pem`;
const keyPath = `${livePath}/privkey.pem`;
const certInfo =
await internalCertificate.getCertificateInfoFromFile(
certPath,
true,
);
const certificatePem = fs.readFileSync(certPath, "utf8");
const intermediateCertificate = fs.readFileSync(
chainPath,
"utf8",
);
const certificateKey = fs.readFileSync(keyPath, "utf8");
const updatedMeta = _.assign({}, certificate.meta, {
certificate: certificatePem,
intermediate_certificate: intermediateCertificate,
certificate_key: certificateKey,
});
const previousMeta = _.cloneDeep(certificate.meta);
const previousExpiresOn = certificate.expires_on;
const stagedCertificate = _.assign({}, certificate, {
meta: updatedMeta,
});
try {
await internalCertificate.writeCustomCert(stagedCertificate);
} catch (err) {
try {
await internalCertificate.writeCustomCert(certificate);
} catch (rollbackErr) {
logger.error(
`Failed restoring cluster certificate files after staging failure: ${rollbackErr.message}`,
);
}
throw err;
}
let updatedCertificate;
try {
updatedCertificate =
await certificateModel.query().patchAndFetchById(
certificate.id,
{
expires_on: moment(
certInfo.dates.to,
"X",
).format("YYYY-MM-DD HH:mm:ss"),
meta: updatedMeta,
},
);
updatedCertificate.meta = updatedMeta;
} catch (err) {
try {
await internalCertificate.writeCustomCert(certificate);
} catch (rollbackErr) {
logger.error(
`Failed restoring cluster certificate files after database failure: ${rollbackErr.message}`,
);
}
throw err;
}
try {
await internalNginx.reload();
} catch (err) {
try {
await certificateModel.query().patchAndFetchById(
certificate.id,
{
expires_on: previousExpiresOn,
meta: previousMeta,
},
);
certificate.meta = previousMeta;
await internalCertificate.writeCustomCert(certificate);
} catch (rollbackErr) {
logger.error(
`Failed rolling back cluster certificate after nginx reload failure: ${rollbackErr.message}`,
);
}
throw err;
}
const auditCertificate = _.cloneDeep(updatedCertificate);
auditCertificate.meta = internalCertificate.cleanMeta(
auditCertificate.meta,
);
await internalAuditLog.add(access, {
action: "renewed",
object_type: "certificate",
object_id: updatedCertificate.id,
meta: auditCertificate,
});
return updatedCertificate;
},
renewLetsEncryptSsl: async (certificate) => {"""
def apply_certificate_js(source: str) -> str: def apply_certificate_js(source: str) -> str:
@@ -141,6 +344,17 @@ def apply_certificate_js(source: str) -> str:
) )
log("cluster_cert_scheduler_foreach=added") log("cluster_cert_scheduler_foreach=added")
if CLUSTER_RENEW_HELPER_FRAGMENT in source:
log("cluster_cert_renew_helper=already-present")
else:
source = replace_exactly_once(
source,
CLUSTER_RENEW_HELPER_ANCHOR,
CLUSTER_RENEW_HELPER_FRAGMENT,
description="cluster certificate renewal helper",
)
log("cluster_cert_renew_helper=added")
if CLUSTER_RENEW_THROW_FRAGMENT in source: if CLUSTER_RENEW_THROW_FRAGMENT in source:
log("cluster_cert_renew_dispatch=already-present") log("cluster_cert_renew_dispatch=already-present")
else: else:
@@ -185,14 +399,51 @@ def verify_certificate_js(source: str) -> None:
description="cluster certificate scheduler foreach verification", description="cluster certificate scheduler foreach verification",
) )
require_fragment(
source,
CLUSTER_RENEW_HELPER_FRAGMENT,
description="cluster certificate renewal helper verification",
)
require_fragment( require_fragment(
source, source,
CLUSTER_RENEW_THROW_FRAGMENT, CLUSTER_RENEW_THROW_FRAGMENT,
description="cluster certificate renewal dispatch verification", description="cluster certificate renewal dispatch verification",
) )
require_fragment(
source,
'fs.readFileSync(certPath, "utf8")',
description="cluster certificate leaf import verification",
)
require_fragment(
source,
'fs.readFileSync(keyPath, "utf8")',
description="cluster certificate private key import verification",
)
require_fragment(
source,
"await internalCertificate.writeCustomCert(stagedCertificate);",
description="cluster certificate staged serving copy verification",
)
require_fragment(
source,
"previousExpiresOn = certificate.expires_on",
description="cluster certificate rollback expiry verification",
)
require_fragment(
source,
"meta: previousMeta",
description="cluster certificate database rollback verification",
)
log("cluster_cert_upload_domains_verification=passed") log("cluster_cert_upload_domains_verification=passed")
log("cluster_cert_scheduler_verification=passed") log("cluster_cert_scheduler_verification=passed")
log("cluster_cert_renew_helper_verification=passed")
log("cluster_cert_renew_dispatch_verification=passed") log("cluster_cert_renew_dispatch_verification=passed")
def apply_schema(source: str) -> str: def apply_schema(source: str) -> str: