Configuring DU-Specific Certificates Manually
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
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.
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:
Upgrading to a Release with provision-certs
Why this matters: the upgrade-safety logic that ships with provision-certs decides whether to skip re-adding the cert-manager-tls replication label based on a certMode field in /opt/pf9/airctl/conf/airctl-config.yaml. That field only ever gets set by running provision-certs itself. A DU configured through this manual page has no certMode set — as far as the new upgrade logic can tell, it looks identical to a DU that was never customized. On upgrade, it takes the default path: it re-adds the cert-manager-tls label to every namespace. Once that label is back, kubernetes-replicator resumes watching the secret and will overwrite your manually-issued certificate with the shared self-signed wildcard, usually within seconds of the label going on.
The safe migration path is:
Revert to self-signed before upgrading
Follow Reverting to the original wildcard certificate above in full, and confirm every namespace is back on the self-signed certificate (issuer == subject) before proceeding. This puts the DU into the state the upgrade path actually expects, so nothing gets caught mid-migration.
Re-provision your certificate the supported way
Once the upgrade completes and airctl provision-certs --help shows the command is available, re-issue your DU-specific certificate through it instead of by hand:
See Using Custom Certificates for the full walkthrough, including the BYOC wildcard and BYOC per-namespace modes if you'd rather supply your own certificate/key directly instead of using cert-manager. This run persists certMode to the config file, so every future upgrade is protected automatically — you will not need to repeat this revert-upgrade-reprovision sequence again.
Last updated
Was this helpful?
