TLS · Ingress · cert-manager · the edge
The cluster says the certificate is fine.The browser disagrees.
certwatch audits certificates across the whole path a request takes — Routes, Ingresses, cert-manager Certificates, TLS Secrets, and the certificate actually served at the public edge — and tells you where they disagree, before a customer's browser does.
The hazard
A certificate is about to expire. cert-manager renewed the Secret in the cluster weeks ago, so every check you have is green — the object is healthy, the condition is Ready, nothing is on fire.
But the certificate a browser actually receives isn't that Secret. Where a load balancer terminates public TLS before traffic reaches the router, the cert lives on that device, and it is the one about to lapse. Nobody is watching it, because nothing in Kubernetes knows it exists.
The green dashboard is measuring the wrong certificate. certwatch measures the one your users get.
Diagnosis
| Severity | Days | Object | Issue |
|---|---|---|---|
| CRITICAL | 3 | edge/api.example.com | Edge cert expires in 3 days — renew on the terminator, not the cluster |
| CRITICAL | — | route/apps/portal | Certificate does not cover the route host |
| CRITICAL | −2 | secret/legacy/old-tls | Certificate expired 2 days ago |
| WARNING | — | route/apps/portal | Edge serves a different cert than the cluster holds |
| WARNING | 9 | route/legacy/embedded | Route carries an inline certificate nobody renews |
Run certwatch explain for the full detail — what, where, and the safe fix for each finding.
The one it's built to catch
This is the certificate a browser receives for api.example.com. It is not the Secret in the cluster — that one renewed on schedule. Where TLS is terminated externally, renewing the cluster Secret does nothing to the cert on the wire.
--probe-edge dials each host's :443 and inspects the certificate actually served. It flags edge expiry, edge-versus-cluster drift, and the self-signed fallback that appears when the intended cert fails to load — none of which any in-cluster check can see.
The Lament of the Green Dashboard Behold, the console was Ready, and the condition was True, and the operator rested; yet the certificate upon the gate had turned to dust three days since, and the stranger at the door was turned away, and no alert was sounded, for the watchman watched the wrong wall.
Eight checks
| Rule | Condition |
|---|---|
| edge-cert-expiry | The certificate served at the public edge is expiring |
| edge-cluster-mismatch | The edge serves a different cert than the cluster holds for that host |
| secret-cert-expiry | A TLS Secret's certificate is expiring or already expired |
| host-not-covered | A Route or Ingress host is not in its certificate's SANs |
| embedded-route-cert | A Route carries an inline cert — an unmanaged expiry no renewal touches |
| certmanager-not-ready | A cert-manager Certificate is not in a Ready state |
| self-signed-at-edge | The public edge is serving a self-signed fallback certificate |
| orphaned-tls-secret | A TLS Secret nothing references — informational |
How it behaves
Read-only by construction. No operator, nothing to deploy, no --fix. It reads through your existing oc or kubectl and parses certificates locally with the Go standard library — no client-go version skew against whatever your cluster runs. --probe-edge only opens outbound TLS to hosts already declared in your own Routes and Ingresses.
--from reads a directory of JSON dumps instead of a live cluster. The same checks run with no credentials — useful for audits, for handing a bundle to a reviewer, and for triage from the wrong side of a change freeze.
mkdir dump for r in routes ingresses secrets certificates; do oc get $r --all-namespaces -o json > dump/$r.json done certwatch scan --from dump certwatch scan --fail-on warning # for CI