certificate *.example.com not before 2026-01-14 not after in 3 days served by the load balancer, actually

TLS · Ingress · cert-manager · the edge

certwatch

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.

$go install github.com/vikozs/certwatch/cmd/certwatch@latest
Source on GitHub Download a binary

The hazard

Every dashboard is green. The edge is three days from expiry.

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

$ certwatch scan --probe-edge5 findings
SeverityDaysObjectIssue
CRITICAL3edge/api.example.comEdge cert expires in 3 days — renew on the terminator, not the cluster
CRITICALroute/apps/portalCertificate does not cover the route host
CRITICAL−2secret/legacy/old-tlsCertificate expired 2 days ago
WARNINGroute/apps/portalEdge serves a different cert than the cluster holds
WARNING9route/legacy/embeddedRoute 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

EXPIRED
subjectCN=api.example.com
issuerInternal CA
served atapi.example.com : 443  (the public edge)
now
notBefore 2026-01-14notAfter 2026-08-10

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

What it recognises

RuleCondition
edge-cert-expiryThe certificate served at the public edge is expiring
edge-cluster-mismatchThe edge serves a different cert than the cluster holds for that host
secret-cert-expiryA TLS Secret's certificate is expiring or already expired
host-not-coveredA Route or Ingress host is not in its certificate's SANs
embedded-route-certA Route carries an inline cert — an unmanaged expiry no renewal touches
certmanager-not-readyA cert-manager Certificate is not in a Ready state
self-signed-at-edgeThe public edge is serving a self-signed fallback certificate
orphaned-tls-secretA TLS Secret nothing references — informational

How it behaves

It never writes to your cluster

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.

  • Dependenciesnone — Go standard library only
  • Installone static binary
  • PlatformsLinux, macOS, Windows · amd64, arm64
  • LicenceApache-2.0

Audit a cluster you can't reach

--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