Aller au contenu principal

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