One wildcard certificate for a whole k3s cluster
Purpose
The cluster serves its routes over HTTPS with one wildcard certificate. A single Traefik TLSStore named default holds it, and every IngressRoute says tls: {} to use it.
The change started with a stuck renewal. The renewal had been stuck since 10-12. cert-manager releases older than 1.17.1 sent the DNS cleanup request with an empty Cloudflare zone ID (/zones//dns_records/…), because Cloudflare stopped returning the zone ID on DNS records (cert-manager issue 7540, fixed in 1.17.1). The upgrade to 1.17.4 fixed it.
The duplicate Certificates only showed up during that fix. The default, traefik and argocd namespaces each had their own Certificate for the same wildcard name. Two orders for the same names started together, raced on the shared apex authorization, and one stalled. One Certificate removes that race and cuts the Let’s Encrypt orders to a third. Let’s Encrypt allows 5 duplicate certificates per week for the same names.
Deployment
Traefik runs as traefik:v3.7.13. cert-manager runs v1.18.6 with 3 controller replicas, deployed by an ArgoCD app (it was a hand-installed Helm release until 2026-10-24). See the Traefik post and the cert-manager post for the base setup.
flowchart LR
A["Certificate<br/>(default namespace)"] --> B["Secret<br/>local-example-com-tls"]
B --> C["TLSStore default"]
C --> D["IngressRoute with tls: {}<br/>(32 routes)"]
E["ClusterIssuer<br/>letsencrypt-production"] --> A
Before the change, the default, traefik and argocd namespaces each had a Certificate and Secret for the same wildcard, all asking Let’s Encrypt through the same issuer.
Configuration
The TLSStore points at the one secret:
1
2
3
4
5
6
7
8
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: default
spec:
defaultCertificate:
secretName: local-example-com-tls
Routes opt in with an empty tls block:
1
2
3
4
spec:
entryPoints:
- websecure
tls: {}
The letsencrypt-production ClusterIssuer uses Let’s Encrypt production with a DNS-01 challenge through Cloudflare, limited to the homelab zone. The only Certificate left is in the default namespace and uses that issuer.
The 32 IngressRoutes are spread over the default (28), argocd, docsite, media-indexer and traefik namespaces (one each).
Storage
The certificate lives in one Kubernetes Secret, local-example-com-tls, written by cert-manager.
Access
Clients reach every route through the websecure entrypoint and get the same wildcard certificate, whatever namespace the route is in.
Notes
ArgoCD’s server-side apply sends an empty tls: {} as null, and the IngressRoute CRD rejects that. Only the ArgoCD app traefik syncs with server-side apply. Its app-wide ServerSideApply=true is there because Traefik’s CRDs are too big for client-side apply. So only the Traefik dashboard route needs a per-resource annotation that makes ArgoCD use client-side apply for that manifest:
1
2
3
metadata:
annotations:
argocd.argoproj.io/sync-options: ServerSideApply=false
Apps that use the default client-side apply don’t hit the problem and need no annotation.
The stuck renewal and the duplicate Certificates are separate problems. The cert-manager upgrade fixed the renewal, and the single Certificate keeps the same-name orders from racing again.