Configuring DU-Specific Certificates Manually
This page applies to releases where airctl provision-certs is not available (check with airctl provision-certs --help). On releases that have it, use Using Custom Certificates instead — it automates every step described on this page.
On this release, DU-specific certificates issued by cert-manager are not provisioned or managed by airctl. To use cert-manager-issued certificates (for example, via Let's Encrypt) instead of the shared self-signed wildcard certificate, you must create the ClusterIssuer and per-namespace Certificate resources yourself, and manage the internal replication label by hand so it doesn't overwrite the certificate you just issued.
Why manual steps are required
Every DU namespace normally receives the same self-signed wildcard certificate through a kubernetes-replicator annotation on a source secret:
metadata:
annotations:
replicator.v1.mittwald.de/replicate-to-matching: cert-manager-tls=http-wildcard-certAny namespace labeled cert-manager-tls=http-wildcard-cert receives a replicated copy of that secret. Without airctl provision-certs to manage this, you must remove the label from each namespace yourself before issuing a DU-specific certificate — otherwise the replicator overwrites it with the shared wildcard certificate again.
Naming constraint — read this before creating anything
The kplane du-upgrade Helm chart, run on every airctl upgrade, contains a template gated by use_du_specific_le_http_cert that creates a Certificate object with two hardcoded values:
{% raw %}
{{- if and (ne .Values.pmk_environment "airgap") (.Values.use_du_specific_le_http_cert) }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: letsencrypt-http-certificate
namespace: {{ .Values.namespace }}
spec:
commonName: {{ .Values.namespace }}.{{ .Values.domain }}
dnsNames:
- {{ .Values.namespace }}.{{ .Values.domain }}
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
secretName: {{ .Values.http_cert_secret }}
{{ end }}
{% endraw %}issuerRef.name— yourClusterIssuermust be named exactlyletsencrypt-prod, regardless of which ACME server or solver it actually uses. Any other name means the nextairctl upgradecreates aCertificatepointing at aClusterIssuerthat doesn't exist.metadata.name— use exactlyletsencrypt-http-certificatefor theCertificateobject in each namespace, so this chart recognizes and preserves it across upgrades.secretNamestayshttp-wildcard-cert— every ingress/deployment already points at this secret name.
Prerequisites
cert-manager installed
kubectl get pods -n cert-manager
AWS IAM credentials with Route53 permissions on the hosted zone (if using the Route53 DNS-01 solver)
route53:GetChange, route53:ChangeResourceRecordSets, and route53:ListHostedZonesByName (if not pinning a zone ID)
Which namespace is the actual replication source
kubectl get secret http-wildcard-cert -A -o yaml | grep -B5 replicate-to-matching
The real namespace names for your cluster
do not assume a generic naming pattern — confirm with kubectl get ns
Create the ClusterIssuer (staging first)
Validate against Let's Encrypt's staging environment before spending a production rate-limit slot.
kubectl patch --type=merge on the solvers list replaces the entire list entry, not just the fields you pass — patching in only region/hostedZoneID can silently wipe accessKeyID and secretAccessKeySecretRef. Always kubectl apply -f the full ClusterIssuer spec when changing anything inside solvers.
Validate with a throwaway certificate
Before touching any real namespace's secret, confirm the DNS-01 → Route53 → issuance pipeline works with a disposable certificate that doesn't share a secret name with anything real:
Issuance typically takes a couple of minutes end to end — the ACME challenge usually reports valid well before the Certificate itself flips to Ready; give it time past that point before assuming it's stuck. Clean up once confirmed:
Back up every real http-wildcard-cert secret before touching anything
Deleting a cert-manager Certificate object does not restore the secret's prior content — it just stops managing it. Back up every namespace you plan to switch, including the replication source namespace, before doing anything else:
Remove the replicator label from every target namespace (except the source namespace)
The replication source namespace never carries this label — only namespaces receiving the replicated copy need it removed. Do this immediately before the next step — if the label lingers while cert-manager writes the new certificate, the replicator can overwrite it within seconds.
Apply the per-namespace Certificate
Repeat for every namespace in scope, including the replication source namespace itself (it needs its own DU-specific certificate too):
Known issue — CannotRegenerateKey on the replication source namespace. If the source namespace's secret holds an RSA key larger than cert-manager's default (2048-bit), cert-manager refuses to replace it under the default rotationPolicy: Never and reports CannotRegenerateKey instead of regenerating silently. Namespaces holding only a replicated copy of that key are usually unaffected — only the direct owner of the original secret tends to hit this. Fix:
Then wait — it proceeds to a fresh DNS-01 challenge and issues normally.
Verify what's actually being served
Ready: True only means cert-manager wrote the secret. Confirm the ingress is actually serving it, and confirm real trust (not curl -k, which skips verification and proves nothing):
Staging certificates only verify successfully on a host that explicitly trusts the Let's Encrypt staging root — that's expected and not a sign anything is broken.
Switch staging to production
Once staging issuance is proven, re-apply the ClusterIssuer with acme.server pointed at the production endpoint (https://acme-v02.api.letsencrypt.org/directory) — always a full apply, never a partial patch (see the solvers warning above). cert-manager does not proactively reissue an already-Ready certificate just because the issuer's server field changed, so force reissuance by deleting the secret (the Certificate object still exists, so cert-manager recreates the secret automatically):
Confirm real (non-staging) issuance:
Restart deployments that read the certificate at startup
Pods mounting the secret as a volume pick up the renewed certificate within about 60 seconds via kubelet sync — no restart needed. Deployments that read it once at process startup need a rollout restart. Discover the real mounts per namespace rather than assuming names:
Then for each deployment found:
Reverting to the original wildcard certificate
Do these steps in order — each assumes the previous one finished. Substitute your own namespace list and replication source namespace name.
Restore the original secret content from your backups
kubectl apply fails here — the backup YAML has no kubectl.kubernetes.io/last-applied-configuration annotation and carries a stale resourceVersion, so the API server rejects it with a conflict. Delete the current secret first, then create fresh from the backup file instead of applying over it:
Verify the restore actually put the original certificate back — check subject/issuer/SANs against what you expect (self-signed, issuer == subject):
Find and restart the deployments that mount the secret
Don't assume names — discover them fresh, and filter out one-shot Jobs (they already ran and don't need restarting):
Only restart pods owned by a ReplicaSet (i.e. a Deployment), one at a time so a slow rollout doesn't get bundled into an SSH timeout on the others:
Last updated
Was this helpful?
