Add cluster-managed certificate metadata support

This commit is contained in:
2026-09-19 10:22:03 -04:00
parent 6cff2261d5
commit 43826bebed
4 changed files with 226 additions and 1 deletions
+23
View File
@@ -50,3 +50,26 @@ Requested mutations must fail closed.
If an expected NPM source anchor cannot be identified or verification fails, If an expected NPM source anchor cannot be identified or verification fails,
the patcher must exit non-zero rather than start an ungated NPM backend. the patcher must exit non-zero rather than start an ungated NPM backend.
## Cluster-managed certificates
Optional cluster-managed custom certificate support is enabled with:
ACME_CLUSTER_CERT=true
This does not grant ACME authority and does not currently perform renewal.
When enabled it:
- adds `meta.cluster_managed` as an explicit boolean certificate property;
- adds `meta.acme_renew` as an explicit boolean certificate property;
- preserves the logical `domain_names` already stored on a custom certificate
when new certificate material is uploaded.
Cluster-managed certificates remain `provider=other`.
The serving certificate therefore remains under:
/data/custom_ssl/npm-<id>
and is independent from any Certbot lineage under `/etc/letsencrypt`.
+169
View File
@@ -0,0 +1,169 @@
"""
Cluster-managed custom certificate mutations.
This module does NOT implement ACME renewal.
It only:
1. Adds cluster lifecycle flags to the certificate meta schema.
2. Preserves logical domain_names when custom certificate material is uploaded.
"""
from __future__ import annotations
import json
from functions.common import log
from functions.common import replace_exactly_once
from functions.common import require_fragment
SCHEMA_PROPERTIES = {
"cluster_managed": {
"type": "boolean",
"default": False,
},
"acme_renew": {
"type": "boolean",
"default": False,
},
}
UPLOAD_DOMAIN_ANCHOR = '''\t\tconst certificate = await internalCertificate.update(access, {
\t\t\tid: data.id,
\t\t\texpires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
\t\t\tdomain_names: validations.certificate.cn ? [validations.certificate.cn] : [],
\t\t\tmeta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
\t\t});
'''
UPLOAD_DOMAIN_FRAGMENT = '''\t\tconst certificate = await internalCertificate.update(access, {
\t\t\tid: data.id,
\t\t\texpires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
\t\t\tdomain_names:
\t\t\t\tArray.isArray(row.domain_names) && row.domain_names.length
\t\t\t\t\t? row.domain_names
\t\t\t\t\t: validations.certificate.cn
\t\t\t\t\t\t? [validations.certificate.cn]
\t\t\t\t\t\t: [],
\t\t\tmeta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
\t\t});
'''
def apply_certificate_js(source: str) -> str:
if UPLOAD_DOMAIN_FRAGMENT in source:
log("cluster_cert_upload_domains=already-present")
return source
source = replace_exactly_once(
source,
UPLOAD_DOMAIN_ANCHOR,
UPLOAD_DOMAIN_FRAGMENT,
description="cluster certificate upload domain preservation",
)
log("cluster_cert_upload_domains=added")
return source
def verify_certificate_js(source: str) -> None:
require_fragment(
source,
UPLOAD_DOMAIN_FRAGMENT,
description="cluster certificate upload domain verification",
)
require_fragment(
source,
"await internalCertificate.writeCustomCert(certificate);",
description="cluster certificate custom write verification",
)
log("cluster_cert_upload_domains_verification=passed")
def apply_schema(source: str) -> str:
try:
document = json.loads(source)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"cluster certificate schema: invalid JSON: {exc}"
) from exc
try:
meta = document["properties"]["meta"]
meta_properties = meta["properties"]
except (KeyError, TypeError) as exc:
raise RuntimeError(
"cluster certificate schema: "
"properties.meta.properties not found"
) from exc
if meta.get("additionalProperties") is not False:
raise RuntimeError(
"cluster certificate schema: "
"meta.additionalProperties is not false"
)
changed = False
for name, definition in SCHEMA_PROPERTIES.items():
existing = meta_properties.get(name)
if existing is None:
meta_properties[name] = definition
changed = True
elif existing != definition:
raise RuntimeError(
"cluster certificate schema: "
f"{name} already exists with unexpected definition"
)
if not changed:
verify_schema(source)
log("cluster_cert_schema=already-present")
return source
result = json.dumps(
document,
indent="\t",
ensure_ascii=False,
) + "\n"
verify_schema(result)
log("cluster_cert_schema=added")
return result
def verify_schema(source: str) -> None:
try:
document = json.loads(source)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"cluster certificate schema verification: invalid JSON: {exc}"
) from exc
try:
meta = document["properties"]["meta"]
meta_properties = meta["properties"]
except (KeyError, TypeError) as exc:
raise RuntimeError(
"cluster certificate schema verification: "
"properties.meta.properties not found"
) from exc
if meta.get("additionalProperties") is not False:
raise RuntimeError(
"cluster certificate schema verification: "
"meta.additionalProperties is not false"
)
for name, definition in SCHEMA_PROPERTIES.items():
if meta_properties.get(name) != definition:
raise RuntimeError(
"cluster certificate schema verification: "
f"{name} definition is missing or unexpected"
)
log("cluster_cert_schema_verification=passed")
+8 -1
View File
@@ -32,10 +32,16 @@ class GateConfig:
request: bool request: bool
renew: bool renew: bool
revoke: bool revoke: bool
cluster_cert: bool
@property @property
def any_enabled(self) -> bool: def any_enabled(self) -> bool:
return self.request or self.renew or self.revoke return (
self.request
or self.renew
or self.revoke
or self.cluster_cert
)
def log(message: str) -> None: def log(message: str) -> None:
@@ -66,6 +72,7 @@ def load_config() -> GateConfig:
request=env_bool("ACME_GATE_REQUEST"), request=env_bool("ACME_GATE_REQUEST"),
renew=env_bool("ACME_GATE_RENEW"), renew=env_bool("ACME_GATE_RENEW"),
revoke=env_bool("ACME_GATE_REVOKE"), revoke=env_bool("ACME_GATE_REVOKE"),
cluster_cert=env_bool("ACME_CLUSTER_CERT"),
) )
+26
View File
@@ -23,9 +23,11 @@ from functions import common
from functions import request from functions import request
from functions import renew from functions import renew
from functions import revoke from functions import revoke
from functions import cluster_cert
DEFAULT_CERTIFICATE_JS = "/app/internal/certificate.js" DEFAULT_CERTIFICATE_JS = "/app/internal/certificate.js"
DEFAULT_CERTIFICATE_SCHEMA = "/app/schema/components/certificate-object.json"
def main() -> None: def main() -> None:
@@ -36,15 +38,28 @@ def main() -> None:
) )
) )
certificate_schema = Path(
os.environ.get(
"NPM_CERTIFICATE_SCHEMA",
DEFAULT_CERTIFICATE_SCHEMA,
)
)
config = common.load_config() config = common.load_config()
common.log(f"certificate.js={certificate_js}") common.log(f"certificate.js={certificate_js}")
common.log(f"certificate_schema={certificate_schema}")
common.log(f"request_gate={config.request}") common.log(f"request_gate={config.request}")
common.log(f"renew_gate={config.renew}") common.log(f"renew_gate={config.renew}")
common.log(f"revoke_gate={config.revoke}") common.log(f"revoke_gate={config.revoke}")
common.log(f"cluster_cert={config.cluster_cert}")
source = common.load_source(certificate_js) source = common.load_source(certificate_js)
schema_source = None
if config.cluster_cert:
schema_source = common.load_source(certificate_schema)
if config.any_enabled: if config.any_enabled:
source = common.apply_authority_helper(source) source = common.apply_authority_helper(source)
@@ -57,6 +72,10 @@ def main() -> None:
if config.revoke: if config.revoke:
source = revoke.apply(source) source = revoke.apply(source)
if config.cluster_cert:
source = cluster_cert.apply_certificate_js(source)
schema_source = cluster_cert.apply_schema(schema_source)
common.verify(source, config) common.verify(source, config)
if config.request: if config.request:
@@ -68,8 +87,15 @@ def main() -> None:
if config.revoke: if config.revoke:
revoke.verify(source) revoke.verify(source)
if config.cluster_cert:
cluster_cert.verify_certificate_js(source)
cluster_cert.verify_schema(schema_source)
common.write_source(certificate_js, source) common.write_source(certificate_js, source)
if config.cluster_cert:
common.write_source(certificate_schema, schema_source)
common.log("All requested ACME authority gates applied successfully") common.log("All requested ACME authority gates applied successfully")