This commit is contained in:
Mateusz Gruszczyński committed 2026-10-03 18:10:20 +02:00
1 parent 1297ea2920
commit dc0fcabf51
49 files changed
+2632 -252

No files matched your search

+60 -36
View File
@@ -28,7 +28,7 @@ ALGORITHMS = {"HMAC-SHA256", "HMAC-SHA384", "HMAC-SHA512"}
MAX_INPUT = 16384
MAX_PEM = 131072
PROVIDERS = {"http01", "cloudflare", "ovh", "rfc2136"}
SECRET_FIELDS = {"key_secret", "api_token", "application_key", "application_secret", "consumer_key"}
SECRET_FIELDS = {"key_secret", "api_token", "application_key", "application_secret", "consumer_key", "credential_ini_b64"}
COMMON_FIELDS = {"provider", "certificate_id", "email", "domains", "environment", "agree_tos", "confirm_production", "propagation_seconds"}
PROVIDER_FIELDS = {
"http01": set(),
@@ -226,29 +226,33 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
raise AcmeError("acme.profile_busy") from None
for folder in ["config", "work", "logs"]:
private_directory(work / folder)
# Keep credentials for this lineage's renewal. Replace atomically; never
# follow an existing credential-file symlink or put secrets in SQLite.
# DNS credentials are supplied by RustProxy's encrypted Secret Store.
# Certbot needs a file path, but the plaintext file exists only while the
# provider command runs and is deleted before this worker returns.
credential_file = work / (value["provider"] + ".ini")
if value["provider"] == "http01":
webroot = certificate_directory / "acme-http01"
for path in (webroot, webroot / ".well-known", webroot / ".well-known" / "acme-challenge"):
private_directory(path)
else:
temporary = work / ("credentials-" + uuid.uuid4().hex)
write_new(temporary, credentials(value))
os.replace(temporary, credential_file)
profile = {k: v for k, v in value.items() if k not in SECRET_FIELDS}
temporary = work / ("profile-" + uuid.uuid4().hex)
write_new(temporary, json.dumps(profile).encode())
os.replace(temporary, work / "profile.json")
args = certbot_command(value, work, credential_file, executable)
credential_file.unlink(missing_ok=True)
write_new(credential_file, credentials(value))
try:
result = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=1200 + value["propagation_seconds"], check=False)
except subprocess.TimeoutExpired:
raise AcmeError("acme.provider_timeout") from None
if result.returncode:
raise AcmeError("acme.certbot_failed_check_private_log")
profile = {k: v for k, v in value.items() if k not in SECRET_FIELDS}
temporary = work / ("profile-" + uuid.uuid4().hex)
write_new(temporary, json.dumps(profile).encode())
os.replace(temporary, work / "profile.json")
args = certbot_command(value, work, credential_file, executable)
try:
result = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=1200 + value["propagation_seconds"], check=False)
except subprocess.TimeoutExpired:
raise AcmeError("acme.provider_timeout") from None
if result.returncode:
raise AcmeError("acme.certbot_failed_check_private_log")
finally:
if value["provider"] != "http01":
credential_file.unlink(missing_ok=True)
cert = read_generated(work, "fullchain.pem")
key = read_generated(work, "privkey.pem")
# Immutable copies allow validation/publication without changing the
@@ -313,7 +317,7 @@ def certificate_dates(pem: bytes, executable: str | None = None) -> dict:
def renewal_request(raw: object) -> dict:
if not isinstance(raw, dict) or set(raw) - {"action", "certificate_id", "provider", "environment", "cert_file", "key_file", "dry_run"}:
if not isinstance(raw, dict) or set(raw) - {"action", "certificate_id", "provider", "environment", "cert_file", "key_file", "dry_run", "credential_ini_b64"}:
raise AcmeError("acme.invalid_renewal_request")
if raw.get("action") != "renew" or raw.get("provider") not in PROVIDERS or raw.get("environment") not in DIRECTORIES:
raise AcmeError("acme.invalid_renewal_request")
@@ -325,7 +329,20 @@ def renewal_request(raw: object) -> dict:
raise AcmeError("acme.invalid_renewal_request")
if type(raw.get("dry_run", False)) is not bool:
raise AcmeError("acme.invalid_renewal_request")
return dict(raw, dry_run=raw.get("dry_run", False))
if raw["provider"] != "http01":
encoded = raw.get("credential_ini_b64")
try:
credential_ini = base64.b64decode(encoded, validate=True) if isinstance(encoded, str) else b""
except (ValueError, TypeError):
credential_ini = b""
if not credential_ini or len(credential_ini) > 65536 or b"\x00" in credential_ini:
raise AcmeError("acme.invalid_api_credential")
else:
credential_ini = b""
value = dict(raw, dry_run=raw.get("dry_run", False))
value["credential_ini"] = credential_ini
value.pop("credential_ini_b64", None)
return value
def renew(raw: dict, certificate_directory: Path, executable: str | None = None) -> dict:
@@ -367,30 +384,37 @@ def renew(raw: dict, certificate_directory: Path, executable: str | None = None)
if not isinstance(names, list) or not 1 <= len(names) <= 16:
raise AcmeError("acme.invalid_domains")
names = [domain(n, wildcard=value["provider"] != "http01") for n in names]
# This exact renewal file must have been created by the original issuance.
# The renewal config persists, but provider credentials do not. Recreate
# the exact path referenced by Certbot only for this invocation.
read_private(work / "config" / "renewal" / "rustproxy.conf", 65536)
credential_file = work / (value["provider"] + ".ini")
if value["provider"] != "http01":
read_private(work / (value["provider"] + ".ini"))
credential_file.unlink(missing_ok=True)
write_new(credential_file, value["credential_ini"])
else:
for path in [certificate_directory / "acme-http01", certificate_directory / "acme-http01" / ".well-known",
certificate_directory / "acme-http01" / ".well-known" / "acme-challenge"]:
private_directory(path)
installed_cert = read_private(certificate_directory / value["cert_file"], MAX_PEM)
installed_key = read_private(certificate_directory / value["key_file"], MAX_PEM)
# Check dependencies/installed PEM before invoking a CA operation.
old_dates = certificate_dates(installed_cert)
args = [executable, "renew", "--cert-name", "rustproxy", "--non-interactive",
"--no-random-sleep-on-renew", "--no-directory-hooks",
"--config-dir", str(work / "config"), "--work-dir", str(work / "work"), "--logs-dir", str(work / "logs")]
if value["dry_run"]:
args.append("--dry-run")
try:
result = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=2100, check=False)
except subprocess.TimeoutExpired:
raise AcmeError("acme.provider_timeout") from None
if result.returncode:
raise AcmeError("acme.certbot_failed_check_private_log")
installed_cert = read_private(certificate_directory / value["cert_file"], MAX_PEM)
installed_key = read_private(certificate_directory / value["key_file"], MAX_PEM)
# Check dependencies/installed PEM before invoking a CA operation.
old_dates = certificate_dates(installed_cert)
args = [executable, "renew", "--cert-name", "rustproxy", "--non-interactive",
"--no-random-sleep-on-renew", "--no-directory-hooks",
"--config-dir", str(work / "config"), "--work-dir", str(work / "work"), "--logs-dir", str(work / "logs")]
if value["dry_run"]:
args.append("--dry-run")
try:
result = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=2100, check=False)
except subprocess.TimeoutExpired:
raise AcmeError("acme.provider_timeout") from None
if result.returncode:
raise AcmeError("acme.certbot_failed_check_private_log")
finally:
if value["provider"] != "http01":
credential_file.unlink(missing_ok=True)
output = {k: value[k] for k in ("provider", "certificate_id", "environment")}
output.update(domains=names, changed=False, staged=False, dry_run=value["dry_run"], **old_dates)
if value["dry_run"]: