Post

One wildcard certificate for a whole k3s cluster

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.

This post is licensed under CC BY 4.0 by the author.