Skip to main content

Phase 60 β€” Cert Observability

Implemented 2026-06-27. Adds Prometheus alerting and Grafana visibility for all cert-manager-managed TLS certificates on the platform.


The Problem This Solves​

Without cert observability, certificates expire silently. The platform has 17 certificates across 16 namespaces β€” the 2026-08-07 batch (10 certs) all share the same expiry, meaning a single missed renewal causes a cascade of HTTPS failures across argocd, grafana, harbor, backstage, homer, and more.

Additionally, 3 certificates had no explicit duration or renewBefore set, relying on cert-manager's implicit defaults with unpredictable renewal windows.


What cert-manager manages (and what it doesn't)​

This is the most common source of confusion when looking at TLS certificates on the platform.

Three types of TLS material in the cluster​

TypeCountVisible in Grafana dashboardManaged by
Certificate CRs β€” cert-manager resources that declaratively request a TLS cert17Yescert-manager
kubernetes.io/tls Secrets without a Certificate CR3Nok3s / Longhorn (self-managed)
Opaque Secrets (e.g. argocd-oidc, grafana-oidc)40+NoESO / manually created

The Grafana dashboard (ID 20842) and the PrometheusRule alerts only cover Certificate CRs. They do not see TLS Secrets that were created outside cert-manager.

The 17 cert-manager Certificates​

All 17 have a cert-manager.io/issuer-name annotation on their Secret and a corresponding Certificate CR. They are all signed by the internal minicloud-ca ClusterIssuer (except the root CA itself, which uses selfsigned-bootstrap).

kubectl get certificate -A
# Returns exactly 17 β€” one Certificate CR per row
CertificateNamespaceIssuerExpiry batch
argocd-tlsargocdminicloud-ca2026-08-07
backstage-tlsbackstageminicloud-ca2026-08-07
chat-tlsaiminicloud-ca2026-08-07
platform-demo-tlsgitops-demominicloud-ca2026-08-07
whoami-tlsgitops-demominicloud-ca2026-08-07
harbor-tls-certharborminicloud-ca2026-08-07
homer-tlshomerminicloud-ca2026-08-07
nats-tlsmessagingminicloud-ca2026-08-07
grafana-tlsmonitoringminicloud-ca2026-08-07
podinfo-tlspodinfominicloud-ca2026-08-07
authentik-tlsauthentikminicloud-ca2026-09-15
ktayl-web-tlsktayl-webminicloud-ca2026-09-18
alertmanager-tlsmonitoringminicloud-ca2026-09-20
prometheus-tlsmonitoringminicloud-ca2026-09-20
nextcloud-tlsnextcloudminicloud-ca2026-09-19
vault-tlsvaultminicloud-ca2026-09-19
minicloud-root-cacert-managerselfsigned-bootstrap2036-05-06

The 3 TLS Secrets NOT managed by cert-manager​

These exist as kubernetes.io/tls Secrets but have no Certificate CR and no cert-manager.io/* annotation. They will never appear in the Grafana dashboard or trigger the PrometheusRule alerts.

SecretNamespaceManaged byRotation
k3s-servingkube-systemk3s control plane (listener.cattle.io)Rotated automatically by k3s on each node restart or cert expiry. Covers the API server endpoint (kubernetes.default.svc.cluster.local, set-hog, 10.0.0.2).
longhorn-webhook-calonghorn-systemLonghorn operatorInternal CA for Longhorn's admission webhooks. Longhorn regenerates this on startup if expired.
longhorn-webhook-tlslonghorn-systemLonghorn operatorWebhook TLS cert signed by longhorn-webhook-ca. Same lifecycle β€” Longhorn owns it.

Why you don't need to worry about these three: k3s and Longhorn each handle their own certificate lifecycle internally. They are not exposed via Ingress and are never sent to a browser. If either rotates a cert, the component that needs it (kubelet, admission controller) picks it up automatically with no human action.

How to tell if a TLS Secret is cert-manager-managed​

# cert-manager-managed: has this annotation
kubectl get secret <name> -n <ns> -o jsonpath='{.metadata.annotations.cert-manager\.io/issuer-name}'
# β†’ "minicloud-ca" (or any non-empty value)

# NOT cert-manager-managed: annotation is empty or absent
kubectl get secret k3s-serving -n kube-system -o jsonpath='{.metadata.annotations.cert-manager\.io/issuer-name}'
# β†’ ""

Alternatively, check for a Certificate CR with a matching spec.secretName:

kubectl get certificate -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.spec.secretName}{"\n"}{end}' | sort

If your TLS Secret name does not appear in that list, cert-manager does not control it.


What Was Implemented​

ServiceMonitor​

cert-manager's Helm chart ships a ServiceMonitor in the cert-manager namespace targeting port tcp-prometheus-servicemonitor (9402). The existing ServiceMonitor was adopted by ArgoCD via the cert-observability Application.

Prometheus scrapes cert-manager every 60s and exposes the key metric:

certmanager_certificate_expiration_timestamp_seconds{namespace, name, issuer_name, ...}

PrometheusRule (3 alerts)​

AlertConditionSeverityFor
CertificateExpiringSoonExpires in < 14 dayswarning1h
CertificateExpiringCriticalExpires in < 3 dayscritical15m
CertificateNotReadyReady != True for 10mwarning10m
# manifests/cert-observability/01-prometheusrule.yaml
(certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 14

Grafana Dashboard​

Import dashboard ID 20842 from grafana.com:

  1. Grafana β†’ Dashboards β†’ New β†’ Import
  2. Enter ID 20842
  3. Select datasource: Prometheus
  4. Click Import

Shows per-cert expiry timeline, renewal history, and ready status.


Certificate Duration Standardisation​

All platform certificates now have explicit duration: 2160h (90 days) and renewBefore: 720h (30 days). Three were missing these fields:

CertificateNamespaceFix method
ktayl-web-tlsktayl-webAdded to Certificate manifest in minicloud-gitops
prometheus-tlsmonitoringIngress annotations + cert delete/recreate
alertmanager-tlsmonitoringIngress annotations + cert delete/recreate

Why ingress-shim certs require a different approach​

prometheus-tls and alertmanager-tls are managed by cert-manager's ingress-shim β€” they're owned by their Ingress object, not standalone Certificate manifests. Duration is controlled by annotations on the Ingress:

# Added to kps-prometheus and kps-alertmanager Ingresses:
cert-manager.io/duration: "2160h"
cert-manager.io/renew-before: "720h"

cert-manager only reads these annotations at Certificate creation time. Updating the annotations on an existing cert has no effect β€” the Certificate must be deleted and recreated:

kubectl annotate ingress kps-prometheus -n monitoring \
cert-manager.io/duration=2160h cert-manager.io/renew-before=720h --overwrite
kubectl annotate ingress kps-alertmanager -n monitoring \
cert-manager.io/duration=2160h cert-manager.io/renew-before=720h --overwrite

kubectl delete certificate prometheus-tls alertmanager-tls -n monitoring
# cert-manager recreates them immediately from the Ingress annotations
kubectl get certificate -n monitoring # both Ready=True within seconds

ArgoCD Application​

# apps/cert-observability.yaml
path: manifests/cert-observability
destination.namespace: cert-manager
syncPolicy: automated, prune, selfHeal, ServerSideApply

Verification​

# Confirm Prometheus is scraping cert-manager
kubectl exec -n monitoring prometheus-kps-prometheus-0 -c prometheus -- \
wget -qO- 'http://localhost:9090/api/v1/query?query=certmanager_certificate_expiration_timestamp_seconds' \
| python3 -c "
import json, sys, time
d = json.load(sys.stdin)
results = d.get('data', {}).get('result', [])
now = time.time()
for r in sorted(results, key=lambda x: float(x['value'][1])):
m = r.get('metric', {})
ns = m.get('namespace', '?')
name = m.get('name', '?')
days = (float(r['value'][1]) - now) / 86400
print(f'{ns}/{name}: {days:.0f} days')
"

# Confirm PrometheusRule is loaded
kubectl exec -n monitoring prometheus-kps-prometheus-0 -c prometheus -- \
wget -qO- 'http://localhost:9090/api/v1/rules' 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
for g in d['data']['groups']:
if 'cert' in g.get('name','').lower():
print(g['name'], ':', [r.get('name') for r in g['rules']])
"
# cert-manager : ['CertificateExpiringSoon', 'CertificateExpiringCritical', 'CertificateNotReady']

# Check all certs have explicit duration
kubectl get certificates -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: duration={.spec.duration}{"\n"}{end}'

Cert Expiry Schedule​

BatchExpiryAuto-renew (30d before)Namespaces
Main batch2026-08-07~2026-07-08argocd, backstage, ai, gitops-demo Γ—2, harbor, homer, messaging, monitoring/grafana, podinfo
Recent2026-09-15–20~2026-08-15authentik, ktayl-web, prometheus, alertmanager, nextcloud, vault
Root CA2036-05-062034-05-06cert-manager