Add cluster-managed certificate renewal
This commit is contained in:
+255
-4
@@ -78,17 +78,220 @@ CLUSTER_RENEW_THROW_ANCHOR = (
|
||||
"\"Only Let'sEncrypt certificates can be renewed\");"
|
||||
)
|
||||
|
||||
CLUSTER_RENEW_THROW_FRAGMENT = '''if (
|
||||
CLUSTER_RENEW_THROW_FRAGMENT = """if (
|
||||
certificate.provider === "other" &&
|
||||
certificate.meta?.cluster_managed === true &&
|
||||
certificate.meta?.acme_renew === true
|
||||
) {
|
||||
throw new error.ValidationError(
|
||||
"Cluster-managed ACME renewal path is not yet enabled",
|
||||
return await internalCertificate.renewClusterManagedSsl(
|
||||
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:
|
||||
@@ -141,6 +344,17 @@ def apply_certificate_js(source: str) -> str:
|
||||
)
|
||||
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:
|
||||
log("cluster_cert_renew_dispatch=already-present")
|
||||
else:
|
||||
@@ -185,14 +399,51 @@ def verify_certificate_js(source: str) -> None:
|
||||
description="cluster certificate scheduler foreach verification",
|
||||
)
|
||||
|
||||
require_fragment(
|
||||
source,
|
||||
CLUSTER_RENEW_HELPER_FRAGMENT,
|
||||
description="cluster certificate renewal helper verification",
|
||||
)
|
||||
|
||||
require_fragment(
|
||||
source,
|
||||
CLUSTER_RENEW_THROW_FRAGMENT,
|
||||
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_scheduler_verification=passed")
|
||||
log("cluster_cert_renew_helper_verification=passed")
|
||||
log("cluster_cert_renew_dispatch_verification=passed")
|
||||
|
||||
def apply_schema(source: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user