Files
home-network-k3s-impl/conformance/tests/l0-static/tls-lifecycle-automated/check.sh
T
jakman85andClaude Opus 5 6a8953e288 feat(prod): observability for the two failure modes that went unseen, plus OIDC self-healing
Minimal prod delivery, authored directly on main (= prod's live revision d1a6628)
rather than by merging work/prod-standup-campaign. That branch carries the DNS apex
inversion, which would have set prod's BASE_DOMAIN to home.keithjackson.dev; prod
runs external-dns with policy: sync and txtOwnerId: prod, so it would have begun
rewriting apex records dev owns while the non-k3s prod still serves 443 there.
Operator sequencing is "dev migrates first". Nothing under clusters/ is touched
here at all, which is what makes this safe to land now.

Also deliberately EXCLUDED, because prod's live cluster-vars has no CNPG_BACKUP_PATH
key and StrictPostBuildSubstitutions would fail the whole tier: the R2 rework of
platform/velero/ and the CNPG destinationPath changes. Those land as
feat/r2-backup-prod together with the endpoint and bucket knobs.

WHAT THIS FIXES

1. Nothing was watching CNPG. All three prod databases have run since bootstrap with
   ContinuousArchiving=False and firstRecoverabilityPoint=null, and no alert existed
   anywhere. Because archive_mode=on and the archive command fails, Postgres refuses
   to recycle WAL: keycloak-db holds every segment ever written (257 MiB on a 5 GiB
   volume, ~6.8 MiB/h) — roughly 29 days to a disk-full PANIC on the cluster's SSO
   database. Found by hand, 38h late. CNPGWALArchiveBacklog would have fired within
   the hour, and evaluates to 3 firing series on prod today.
   Rules key on cnpg_collector_* rather than cnpg_pg_stat_archiver_*, because
   nextcloud-db's exporter cannot log in to its own database (permission denied for
   database "nextcloud", both clusters) so the pg_stat_archiver family does not exist
   for it — the obvious metric would have been blind to one database in three.

2. No PVC outside namespace=monitoring had any fill alert. New rules are partitioned
   by namespace so they cannot double-fire with MonitoringVolumeFillingUp, pinned to
   job="kubelet" (the apiserver copy of the same series reports namespace=default),
   and guarded by inodes > 0 — block-mode PVCs such as unifi-rootdisk report
   available=0 forever and would otherwise have fired critical permanently.

3. VPNTunnelDown could never clear on a cluster with no gateway node; it has been
   firing critical on prod since 2026-09-05. Now guarded so prod self-arms the moment
   a node carries the egress-gateway label, with dev's behaviour unchanged today.

4. The Keycloak realm import fails OPEN: an unresolved placeholder is stored
   LITERALLY and the import still reports success, then sits at Done=True and never
   re-runs. On 2026-09-06 all 12 prod OIDC clients held the literal
   $(env:..._CLIENT_SECRET) string and every SSO login failed, while every signal was
   green. The CronJob is the level-triggered correction; two alerts make its silence
   observable. Repaired live already — this stops it recurring.

Also: the R-NET-04 conformance scan descended into nested agent worktrees under
.claude/, counting 4 egress policies where 1 is authored.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 00:59:22 -05:00

479 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# L0 static check (offline, no cluster) — tls-lifecycle-automated
# Serves: R-TLS-02 ("Certificates MUST be automatically **issued, renewed, and revoked**
# without manual steps." — spec docs/00-vision-and-scope.md; mechanism in
# docs/08-tls-and-certificates.md, ADR-0006.)
#
# WHICH CLAUSE THIS LAYER CAN PROVE — AND WHICH IT CANNOT
# ------------------------------------------------------
# R-TLS-02 has one adverbial clause ("without manual steps") and three verbs
# ("issued", "renewed", "revoked"). In a GitOps platform the *without manual steps*
# clause IS a repository property, and it is the half that can be proven with no
# cluster: if a human had to mint, install, or approve a serving certificate, the
# evidence of that human step would be IN THE REPO — a committed `tls.crt`, a Gateway
# pointing at a Secret nothing declares, an issuer that no automated solver can
# satisfy, a per-cert credential handed over out of band, or an admission path that
# waits for a manual CertificateRequest approval. This check asserts that none of
# those exist and that the automated path is completely wired instead.
#
# The three VERBS are runtime events. They are asserted by the companion L2 test
# (conformance/tests/l2-behavioral/cert-lifecycle-automated), which observes an
# issuance, an unattended renewal, and the withdrawal of a removed certificate. This
# check does NOT claim them; see "WHAT THIS DOES *NOT* PROVE" below.
#
# GIVEN the repository as git tracks it,
# WHEN every serving-TLS reference, Certificate, Issuer and the cert-manager release
# are inspected offline,
# THEN (1) at least one cert-manager `Certificate` is authored (vacuity floor: a repo
# with no Certificate cannot pass this check by having nothing to check);
# (2) every Gateway HTTPS listener that TERMINATES TLS references ONLY Secrets
# that a tracked Certificate declares as its `secretName` — so the serving
# credential is produced by the controller, never dropped in by hand;
# (3) NO certificate material is tracked anywhere: no Secret manifest of type
# kubernetes.io/tls, no tls.crt/tls.key/ca.crt key in any committed Secret
# (plain OR SOPS-encrypted), and no PEM certificate body in any tracked
# YAML — i.e. issuance is not, and cannot silently become, a human step;
# (4) every Certificate's `issuerRef` resolves to a TRACKED issuer IN EVERY
# ENVIRONMENT — the `${ACME_CLUSTER_ISSUER}` token is resolved against the
# base cluster-vars default AND every overlay's value, so an environment
# whose issuer does not exist is caught here rather than at prod bring-up;
# (5) every issuer so referenced is an ACME issuer with a NON-EMPTY solver list
# whose every solver is an automated challenge type (dns01 / http01) — the
# same code path serves issuance AND renewal, so a solver a human must
# complete would defeat both;
# (6) each dns01 solver's provider credential is referenced BY NAME and that
# Secret is delivered SOPS-encrypted from git via a kustomization — the
# operator's one-time token hand-off (docs/08 OQ-4), never a per-cert step;
# (7) cert-manager's automatic CertificateRequest approval is NOT disabled
# (`disableAutoApproval`), and any `approveSignerNames` narrowing still
# covers the ClusterIssuers actually used — a request that waits for a
# human approver is the definition of a manual step;
# (8) the whole chain is reconciled from git — cert-manager's HelmRelease, the
# ClusterIssuers and the Certificate are each included in a kustomization
# that a Flux Kustomization renders — so a deleted Certificate is
# re-created and re-issued with no human action;
# (9) no Certificate defeats renewal arithmetically (`renewBefore` >= `duration`
# would mean the renewal instant never arrives).
#
# WHAT THIS DOES *NOT* PROVE (honest gaps — do not read this test as claiming them):
# - ISSUED: that an issuance actually happens. Asserted at L2 (step 2 of
# l2-behavioral/cert-lifecycle-automated) and, for the real ACME/DNS-01 path,
# evidenced by l2-behavioral/trusted-tls serving a publicly-trusted cert.
# - RENEWED: that a renewal actually happens unattended. docs/08's acceptance gate
# is explicit that this must be "observed across a renewal" — L2 step 3 does that
# by fast-forwarding a certificate's renewal window; nothing static can.
# - REVOKED: NOT PROVEN AT ANY LAYER TODAY, and not asserted here. Two distinct
# readings, neither established: (a) ACME revocation — cert-manager does not
# revoke certificates at all, so no repo state could show it; (b) the spec's own
# reading in docs/08 ("removing/rotating a certificate is a declarative change")
# — withdrawal of a REMOVED certificate's serving material requires cert-manager's
# `enableCertificateOwnerRef`, which the tracked HelmRelease does NOT set (chart
# default false). L2 step 4 asserts the withdrawal behaviour and is expected to
# FAIL until that knob is set; escalated to the PM rather than softened (R-TEST-06).
# - That the tracked Certificate is the one the live Gateway is actually serving
# (cluster state), or that the ACME account/DNS credential is still valid.
# - Certificate EXPIRY ALERTING, which docs/08 delegates to R-OBS-04 (see
# l3-fault-injection/alerting).
# =============================================================================
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "${DIR}/../../../.." && pwd)" # repo root
PYTHON="${PYTHON:-python3}"
cd "${ROOT}"
"${PYTHON}" - "${ROOT}" <<'PY'
import re
import subprocess
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
print("check FAIL: PyYAML is required (same dependency as catalog/render.py)",
file=sys.stderr)
raise SystemExit(1)
ROOT = Path(sys.argv[1])
FAILURES: list[str] = []
CHECKS = 0
def check(cond, msg):
global CHECKS
CHECKS += 1
if not cond:
FAILURES.append(msg)
def tracked() -> list[str]:
out = subprocess.run(["git", "-C", str(ROOT), "ls-files", "-z"],
capture_output=True, text=True, check=True).stdout
return [p for p in out.split("\0") if p]
FILES = tracked()
check(len(FILES) > 50, f"git ls-files returned only {len(FILES)} paths — not a real checkout")
YAML_FILES = [f for f in FILES if re.search(r"\.(ya?ml)$", f)]
def docs_of(rel: str):
"""Parsed YAML documents of a tracked file, or [] when it is not parseable YAML."""
try:
return [d for d in yaml.safe_load_all((ROOT / rel).read_text(errors="replace"))
if isinstance(d, dict)]
except Exception:
return []
DOCS: list[tuple[str, dict]] = [(rel, d) for rel in YAML_FILES for d in docs_of(rel)]
def of_kind(kind: str, group_prefix: str = "") -> list[tuple[str, dict]]:
out = []
for rel, d in DOCS:
if d.get("kind") != kind:
continue
if group_prefix and not str(d.get("apiVersion", "")).startswith(group_prefix):
continue
out.append((rel, d))
return out
def ns_of(d: dict, default: str = "default") -> str:
return (d.get("metadata") or {}).get("namespace") or default
def name_of(d: dict) -> str:
return (d.get("metadata") or {}).get("name") or "<unnamed>"
# ------------------------------------------------------------ cluster-vars resolution
# Every environment's value for a ${TOKEN}: the base default plus each overlay's JSON6902
# `- op: replace / path: /data/KEY / value: ...` patch on the cluster-vars ConfigMap. This
# is the same mechanism flux-postbuild-substitution audits; here we need the VALUES, so an
# issuer name can be resolved per environment instead of being asserted for dev only.
def env_values(key: str) -> dict[str, str]:
vals: dict[str, str] = {}
base = ROOT / "clusters" / "base" / "cluster-vars.yaml"
if base.is_file():
for d in yaml.safe_load_all(base.read_text()):
if isinstance(d, dict) and d.get("kind") == "ConfigMap" \
and name_of(d) == "cluster-vars":
v = (d.get("data") or {}).get(key)
if v is not None:
vals["base"] = str(v)
for overlay in sorted((ROOT / "clusters").glob("*/kustomization.yaml")):
env = overlay.parent.name
if env == "base":
continue
try:
k = yaml.safe_load(overlay.read_text()) or {}
except Exception:
continue
for p in (k.get("patches") or []):
tgt = p.get("target") or {}
if tgt.get("kind") != "ConfigMap" or tgt.get("name") != "cluster-vars":
continue
try:
ops = yaml.safe_load(p.get("patch") or "") or []
except Exception:
continue
for op in ops if isinstance(ops, list) else []:
if isinstance(op, dict) and str(op.get("path", "")) == f"/data/{key}":
vals[env] = str(op.get("value"))
return vals
TOKEN_RE = re.compile(r"^\$\{([A-Z0-9_]+)\}$")
def resolve_per_env(value: str) -> dict[str, str]:
"""A literal resolves to itself in every environment; a ${TOKEN} to each env's value."""
m = TOKEN_RE.match(value.strip())
if not m:
return {"(literal)": value.strip()}
return env_values(m.group(1))
# --------------------------------------------- what Flux actually renders (reachability)
# A manifest is DELIVERED only if it is reachable from a Flux Kustomization's `path` through
# the kustomize resource graph: <path>/kustomization.yaml -> resources -> (file | directory
# -> that directory's kustomization.yaml -> ...). Membership is deliberately computed by
# walking that graph rather than by asking "is this file's DIRECTORY mentioned somewhere":
# the loose form silently passes when an entry is deleted from the very kustomization.yaml
# that the directory reference renders (verified — it made two assertions here no-ops).
KUST_RESOURCES: dict[str, list[str]] = {}
for rel in [f for f in YAML_FILES if f.endswith("kustomization.yaml")]:
refs: list[str] = []
for d in docs_of(rel):
for r in (d.get("resources") or []) + (d.get("components") or []):
if not isinstance(r, str) or "://" in r: # skip remote bases
continue
try:
p = (ROOT / rel).parent.joinpath(r).resolve()
refs.append(str(p.relative_to(ROOT)))
except Exception:
continue
KUST_RESOURCES[rel] = refs
FLUX_PATHS: set[str] = set()
for _rel, _d in DOCS:
if _d.get("kind") == "Kustomization" and \
str(_d.get("apiVersion", "")).startswith("kustomize.toolkit.fluxcd.io/"):
p = str((_d.get("spec") or {}).get("path", "")).lstrip("./").rstrip("/")
if p:
FLUX_PATHS.add(p)
def _walk(entry: str, seen: set[str], out: set[str]) -> None:
if entry in seen:
return
seen.add(entry)
kust = entry if entry.endswith("kustomization.yaml") else f"{entry}/kustomization.yaml"
if kust in KUST_RESOURCES: # a directory (or a kustomization)
for r in KUST_RESOURCES[kust]:
_walk(r, seen, out)
return
out.add(entry) # a leaf manifest
RENDERED: set[str] = set()
for _p in FLUX_PATHS:
_walk(_p, set(), RENDERED)
def delivered(rel: str) -> bool:
"""True iff Flux renders this file from one of its Kustomization paths."""
return rel in RENDERED
# --------------------------------------------------- (1) there is something to check
CERTS = of_kind("Certificate", "cert-manager.io/")
check(len(CERTS) >= 1,
"no cert-manager.io Certificate is authored anywhere in the tracked tree — every "
"assertion below would be vacuous, so this is a FAILURE, not a pass (R-TLS-02)")
ISSUERS = of_kind("ClusterIssuer", "cert-manager.io/") + of_kind("Issuer", "cert-manager.io/")
check(len(ISSUERS) >= 1,
"no cert-manager ClusterIssuer/Issuer is authored — nothing could issue automatically")
# ---------------------------- (2) every terminating listener serves a controller cert
CERT_SECRETS: dict[tuple[str, str], str] = { # (namespace, secretName) -> file
(ns_of(d, "default"), str((d.get("spec") or {}).get("secretName", ""))): rel
for rel, d in CERTS
}
listeners_checked = 0
for rel, gw in of_kind("Gateway", "gateway.networking.k8s.io/"):
gw_ns = ns_of(gw)
for lis in ((gw.get("spec") or {}).get("listeners") or []):
tls = lis.get("tls") or {}
if str(tls.get("mode", "")) != "Terminate":
continue
listeners_checked += 1
refs = tls.get("certificateRefs") or []
check(len(refs) > 0,
f"{rel}: Gateway {gw_ns}/{name_of(gw)} listener {lis.get('name')!r} terminates "
"TLS with NO certificateRefs — it can only serve a certificate someone puts "
"there by hand")
for r in refs:
kind = str(r.get("kind", "Secret"))
key = (str(r.get("namespace") or gw_ns), str(r.get("name")))
check(kind == "Secret",
f"{rel}: listener {lis.get('name')!r} certificateRef kind is {kind!r}; only "
"Secret refs can be traced to a cert-manager Certificate")
check(key in CERT_SECRETS,
f"{rel}: listener {lis.get('name')!r} serves Secret {key[0]}/{key[1]}, which "
"NO tracked cert-manager Certificate declares as its secretName — that "
"credential would have to be created by hand (R-TLS-02 'without manual steps')")
check(listeners_checked > 0,
"no Gateway listener with tls.mode=Terminate found in the tracked tree — there is no "
"serving-TLS reference to trace, so (2) proved nothing")
# --------------------------------- (3) no certificate material is tracked, anywhere
TLS_KEYS = {"tls.crt", "tls.key", "ca.crt", "tls-combined.pem"}
for rel, d in DOCS:
if d.get("kind") != "Secret":
continue
stype = str(d.get("type", ""))
check(stype != "kubernetes.io/tls",
f"{rel}: a Secret of type kubernetes.io/tls is COMMITTED (metadata.name="
f"{name_of(d)}) — a serving certificate authored by a human, not issued")
for field in ("data", "stringData"):
for k in (d.get(field) or {}):
check(str(k) not in TLS_KEYS,
f"{rel}: committed Secret {name_of(d)} carries {field}.{k} — certificate "
"material in git means a manual issuance step exists (R-TLS-02)")
PEM_RE = re.compile(r"-----BEGIN (?:[A-Z ]+ )?CERTIFICATE-----")
for rel in YAML_FILES:
body = (ROOT / rel).read_text(errors="replace")
# Scoped to YAML manifests deliberately: prose/docs may legitimately SHOW a PEM header
# in an example, and a .md file is not applied to any cluster. A manifest cannot.
check(not PEM_RE.search(body),
f"{rel}: contains a PEM CERTIFICATE body — a hand-minted certificate in a manifest")
# --------------------------- (4)+(5)+(6) the issuer chain is automated in EVERY env
# The cert-manager release's namespace IS the controller's `--cluster-resource-namespace`
# (chart default), i.e. where a ClusterIssuer's solver credential must live. Derived from the
# HelmRelease, never written down here, so this holds if the release namespace ever changes.
cm_releases = [(rel, d) for rel, d in of_kind("HelmRelease") if name_of(d) == "cert-manager"]
CM_NS = ns_of(cm_releases[0][1], "cert-manager") if cm_releases else "cert-manager"
ISSUER_INDEX = {(d.get("kind"), name_of(d)): (rel, d) for rel, d in ISSUERS}
solver_creds: list[tuple[str, str, str, str]] = [] # (issuer, ns, secretName, ref field)
issuers_verified: set[str] = set()
for rel, cert in CERTS:
spec = cert.get("spec") or {}
ref = spec.get("issuerRef") or {}
kind = str(ref.get("kind", "Issuer"))
per_env = resolve_per_env(str(ref.get("name", "")))
check(len(per_env) > 0,
f"{rel}: Certificate {name_of(cert)} issuerRef.name is a token with NO value in "
"clusters/base/cluster-vars.yaml or any overlay — it would render empty and never issue")
for env, iname in sorted(per_env.items()):
hit = ISSUER_INDEX.get((kind, iname))
check(hit is not None,
f"{rel}: Certificate {name_of(cert)} resolves issuerRef to {kind}/{iname} in "
f"environment {env!r}, but no such issuer is tracked — that environment cannot "
"issue anything without a hand-created issuer (R-TLS-02, R-ENV-02)")
if hit is None:
continue
irel, issuer = hit
issuers_verified.add(iname)
ispec = issuer.get("spec") or {}
acme = ispec.get("acme")
check(isinstance(acme, dict),
f"{irel}: {kind}/{iname} (used by {name_of(cert)} in env {env!r}) is not an ACME "
f"issuer — its issuance/renewal path is {sorted(ispec)}; docs/08/ADR-0006 mandate "
"ACME so both are unattended")
if not isinstance(acme, dict):
continue
check(str(acme.get("server", "")).startswith("https://"),
f"{irel}: {kind}/{iname} has no https ACME directory endpoint")
check(bool((acme.get("privateKeySecretRef") or {}).get("name")),
f"{irel}: {kind}/{iname} declares no privateKeySecretRef — the ACME account key "
"would have to be provisioned by hand")
solvers = acme.get("solvers") or []
check(len(solvers) > 0,
f"{irel}: {kind}/{iname} declares NO solvers — every order would sit unsolved "
"until a human completed the challenge")
for s in solvers:
types = [t for t in ("dns01", "http01") if t in s]
check(len(types) == 1,
f"{irel}: {kind}/{iname} has a solver with challenge types {sorted(s)} — only "
"the automated dns01/http01 solvers satisfy 'without manual steps'")
for provider, cfg in (s.get("dns01") or {}).items():
if not isinstance(cfg, dict):
continue
for field, val in cfg.items():
if isinstance(val, dict) and str(field).endswith(("SecretRef", "secretRef")):
n = str(val.get("name", ""))
check(bool(n),
f"{irel}: {kind}/{iname} dns01 {provider}.{field} has no secret name")
if n:
# A ClusterIssuer resolves secret refs in the controller's
# cluster-resource-namespace; a namespaced Issuer in its own.
want_ns = CM_NS if kind == "ClusterIssuer" else ns_of(issuer, CM_NS)
solver_creds.append((iname, want_ns, n, f"{provider}.{field}"))
check(len(issuers_verified) > 0, "no issuer was reachable from any Certificate — (5) proved nothing")
# (6) the solver credential is delivered, SOPS-encrypted, from git — never per-cert by hand.
check(len(solver_creds) > 0,
"no dns01 solver credential reference found — docs/08 (OQ-4) requires the scoped "
"Cloudflare token to drive DNS-01, so this check would otherwise be vacuous")
# Keyed by (namespace, name): a same-named Secret in ANOTHER namespace is NOT this credential.
# (Verified: keying by name alone let the external-dns copy of `cloudflare-api-token` satisfy
# the cert-manager reference, so deleting cert-manager's own copy passed — a silent no-op.)
SECRET_DOCS = {(ns_of(d, CM_NS), name_of(d)): (rel, d)
for rel, d in DOCS if d.get("kind") == "Secret"}
for iname, sns, sname, field in solver_creds:
hit = SECRET_DOCS.get((sns, sname))
check(hit is not None,
f"{iname}: dns01 {field} references Secret {sname!r}, which no tracked manifest "
f"delivers into namespace {sns!r} (the issuer's secret-resolution namespace) — the "
"credential would be created by hand in-cluster (R-TLS-02)")
if hit is None:
continue
srel, sdoc = hit
check("sops" in sdoc,
f"{srel}: Secret {sname} is the DNS-01 credential but is not SOPS-encrypted (R-SEC-01)")
check(delivered(srel),
f"{srel}: Secret {sname} is not rendered by any Flux Kustomization path — Flux would "
"never deliver it, so issuance would wait on a manual kubectl apply")
# ------------------------------- (7) automatic CertificateRequest approval is intact
check(len(cm_releases) == 1,
f"expected exactly one cert-manager HelmRelease, found {[r for r, _ in cm_releases]} — "
"the controller that issues and renews must itself be GitOps-managed")
for rel, hr in cm_releases:
values = ((hr.get("spec") or {}).get("values") or {})
check(values.get("disableAutoApproval") in (None, False),
f"{rel}: cert-manager values set disableAutoApproval={values.get('disableAutoApproval')!r} "
"— every CertificateRequest would then wait for an approver, which is a manual step")
signers = values.get("approveSignerNames")
if signers is not None:
pats = [str(s) for s in (signers if isinstance(signers, list) else [signers])]
for iname in sorted(issuers_verified):
ok = any(p in ("*", "*/*") or "clusterissuers.cert-manager.io" in p and
(p.endswith("/*") or p.endswith(f"/{iname}")) for p in pats)
check(ok,
f"{rel}: approveSignerNames={pats} does not cover ClusterIssuer {iname} — "
"requests for it would never be auto-approved")
# The chart's own CRD install must be on, else the CRs below cannot even be applied.
check((values.get("crds") or {}).get("enabled") is True,
f"{rel}: cert-manager values do not enable CRD installation — the Certificate CRD "
"would have to be installed by hand")
# ------------------------------- (8) the whole chain is continuously reconciled from git
check(len(FLUX_PATHS) > 0 and len(RENDERED) > 20,
f"the kustomize resource graph resolved {len(RENDERED)} manifest(s) from {len(FLUX_PATHS)} "
"Flux Kustomization path(s) — too few to be a real graph, so (8) would prove nothing")
for rel, _ in cm_releases + CERTS + ISSUERS:
check(delivered(rel),
f"{rel} is not rendered by any Flux Kustomization path — nothing reconciles it, so a "
"deleted Certificate/issuer/controller would stay deleted until a human re-applied it")
# --------------------------------------------- (9) renewal is not defeated arithmetically
def to_seconds(v) -> int | None:
m = re.fullmatch(r"\s*(\d+)\s*([smh])\s*", str(v))
if not m:
return None
return int(m.group(1)) * {"s": 1, "m": 60, "h": 3600}[m.group(2)]
renew_pairs = 0
for rel, cert in CERTS:
spec = cert.get("spec") or {}
dur, rb = to_seconds(spec.get("duration")), to_seconds(spec.get("renewBefore"))
if dur is None or rb is None:
continue
renew_pairs += 1
check(rb < dur,
f"{rel}: Certificate {name_of(cert)} sets renewBefore={spec.get('renewBefore')} >= "
f"duration={spec.get('duration')} — the renewal instant never arrives")
# --------------------------------------------------------------------------- verdict
# NO-OP GUARD: this repo has been bitten by checks that degrade to a successful no-op.
# The floor below is a count of assertions that MUST have run for the verdict to mean
# anything; it is not a style rule.
check(CHECKS >= 20,
f"only {CHECKS} assertions ran — the check degenerated to a near-no-op and its pass "
"would be meaningless")
if FAILURES:
print(f"CHECK FAIL (tls-lifecycle-automated): {len(FAILURES)} of {CHECKS} assertions failed",
file=sys.stderr)
for f in FAILURES:
print(f" - {f}", file=sys.stderr)
raise SystemExit(1)
print(f"check OK: tls-lifecycle-automated — {CHECKS} assertions; {len(CERTS)} Certificate(s), "
f"{len(ISSUERS)} issuer(s), {len(issuers_verified)} issuer(s) reached from a Certificate "
f"across every environment, {listeners_checked} terminating listener(s), "
f"{len(solver_creds)} dns01 credential ref(s), {renew_pairs} duration/renewBefore pair(s). "
"R-TLS-02 'without manual steps' clause only — issued/renewed/revoked are RUNTIME verbs "
"(see l2-behavioral/cert-lifecycle-automated; 'revoked' is NOT proven at any layer).")
PY