If you’re running Kubernetes and want automatic HTTPS for your services, cert-manager is one of the best tools available. It integrates directly with Let’s Encrypt to handle certificate requests, renewals, and management.
In my setup, I wanted to:
- Use Cloudflare DNS for ACME DNS-01 challenges.
- Automatically issue wildcard certificates (
*.example.com). - Use the EmberStack Reflector plugin to share those certificates across namespaces.
To make the process easier to repeat, I split the configuration into four Ansible playbooks:
cert-manager-helm-values.yml– Helm configuration for cert-managercert-manager.yml– Installs cert-manager and sets up the Let’s Encrypt issuercert-manager-reflector-plugin.yml– Installs the reflector plugincert-manager-wildcard-cert.yml– Creates a wildcard certificate
cert-manager Helm Values (cert-manager-helm-values.yml)
# Custom cert-manager Helm values file for Cloudflare DNS01 challenge
installCRDs: true
extraArgs:
- --dns01-recursive-nameservers=1.1.1.1:53,9.9.9.9:53
- --dns01-recursive-nameservers-only=true
podDnsPolicy: None
podDnsConfig:
nameservers:
- "1.1.1.1"
- "9.9.9.9"
This configuration ensures cert-manager installs its CRDs and uses Cloudflare’s and Quad9’s public resolvers for DNS lookups.
It’s especially helpful if you run a custom internal DNS setup or need predictable resolution for ACME challenges.
Installing cert-manager and Creating a ClusterIssuer (cert-manager.yml)
- name: Install cert-manager
hosts: master
gather_facts: false
vars:
desired_state: "present"
tasks:
- name: Add jetstack Helm repo
kubernetes.core.helm_repository:
kubeconfig: /home/{{ user }}/.kube/config
context: {{ kube_context }} # I have multiple clusters, so this line is not needed if you only manage one cluster with your kubeconfig.
repo_name: jetstack
repo_url: "https://charts.jetstack.io"
state: "{{ desired_state }}"
delegate_to: localhost
- name: Deploy cert-manager
kubernetes.core.helm:
kubeconfig: /home/{{ user }}/.kube/config
context: {{ kube_context }} # see note above
state: "{{ desired_state }}"
name: cert-manager
chart_ref: jetstack/cert-manager
release_namespace: cert-manager
create_namespace: true
release_state: present
purge: true
force: true
wait: true
values_files:
- /path/to/cert-manager-helm-values.yml
delegate_to: localhost
- name: Create Cloudflare API token secret
kubernetes.core.k8s:
kubeconfig: /home/{{ user }}/.kube/config
context: {{ kube_context }} # see note above
state: "{{ desired_state }}"
definition:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
type: Opaque
stringData:
api-token: "{{ cloudflare_api_token }}"
delegate_to: localhost
- name: Create ClusterIssuer for Let’s Encrypt
kubernetes.core.k8s:
kubeconfig: /home/{{ user }}/.kube/config
context: {{ kube_context }} # see note above
state: "{{ desired_state }}"
definition:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging # change to letsencrypt-prod once tested
namespace: cert-manager
spec:
acme:
email: "{{ contact_email }}"
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- dns01:
cloudflare:
email: "{{ contact_email }}"
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "{{ domain1 }}"
- "{{ domain2 }}"
This playbook installs cert-manager, creates the Cloudflare API token Secret, and defines a ClusterIssuer using Let’s Encrypt’s staging endpoint.
The staging environment issues untrusted certificates but avoids production rate limits — ideal for testing.
About the {{ }} variables
Anything inside double braces (like {{ kube_context }} or {{ cloudflare_api_token }}) belongs in your Ansible vars file, not the playbook itself.
Keeping credentials and domains separate makes your playbooks reusable and secure.
Example vars.yml:
# vars.yml
user: douglas
kube_context: cluster1
contact_email: do**@*****le.com
cloudflare_api_token: YOUR_API_TOKEN
domain1: example.com
domain2: anotherdomain.com
Because this file contains secrets, it should be encrypted with Ansible Vault:
ansible-vault encrypt vars.yml
ansible-playbook cert-manager.yml --ask-vault-pass -e @vars.yml
Installing the Reflector Plugin (cert-manager-reflector-plugin.yml)
- name: Install cert-manager reflector plugin
hosts: master
gather_facts: false
vars:
desired_state: "present"
tasks:
- name: Add emberstack Helm repo
kubernetes.core.helm_repository:
kubeconfig: /etc/rancher/k3s/k3s.yaml
repo_name: emberstack
repo_url: "https://emberstack.github.io/helm-charts"
state: "{{ desired_state }}"
- name: Update Helm repo cache
kubernetes.core.helm:
kubeconfig: /etc/rancher/k3s/k3s.yaml
state: absent
release_name: dummy
release_namespace: kube-system
update_repo_cache: true
- name: Deploy reflector
kubernetes.core.helm:
kubeconfig: /etc/rancher/k3s/k3s.yaml
state: "{{ desired_state }}"
name: reflector
chart_ref: emberstack/reflector
release_namespace: reflector
create_namespace: true
release_state: present
purge: true
force: true
wait: true
The reflector plugin replicates Secrets across namespaces, allowing a single wildcard certificate to be shared throughout the cluster.
Creating the Wildcard Certificate (cert-manager-wildcard-cert.yml)
- name: Create wildcard certificate via cert-manager
hosts: master
become: true
gather_facts: false
vars:
desired_state: "present"
tasks:
- name: Create wildcard Certificate resource
kubernetes.core.k8s:
kubeconfig: /etc/rancher/k3s/k3s.yaml
state: "{{ desired_state }}"
definition:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert-prod-example
namespace: cert-manager
spec:
secretName: wildcard-cert-prod-example-tls
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
issuerRef:
name: letsencrypt-staging # switch to prod once validated
kind: ClusterIssuer
commonName: "*.example.com"
dnsNames:
- "*.example.com"
This defines a wildcard certificate using the Let’s Encrypt ClusterIssuer. Once complete, cert-manager will store it as a Kubernetes Secret, and the reflector plugin will propagate it to other namespaces.
Verifying cert-manager Installation
Before diving into issuers and certificates, confirm cert-manager itself is running properly.
# Check namespace and pods
kubectl get ns
kubectl get pods -n cert-manager
# Check deployments and replica status
kubectl get deployments -n cert-manager
kubectl describe deployment cert-manager -n cert-manager
# Check services (webhook and CA injector)
kubectl get svc -n cert-manager
# Confirm CRDs were installed
kubectl get crds | grep cert-manager.io
# Check logs from the main controller
kubectl logs -n cert-manager deploy/cert-manager
# Verify all components are ready
kubectl get pods -n cert-manager -o wide
If everything is healthy, you should see three main deployments:cert-manager, cert-manager-cainjector, and cert-manager-webhook.
All should be in a Running state with 1/1 READY.
Verifying cert-manager Functionality
After confirming installation, use these to check certificate-related resources.
# Check the ClusterIssuer
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-staging
# Check certificates
kubectl get certificate -n cert-manager
kubectl describe certificate wildcard-cert-prod-example -n cert-manager
# Check expiry and readiness
kubectl -n cert-manager get certificate \
-o custom-columns=NAME:metadata.name,NOT_AFTER:status.notAfter,READY:status.conditions[?(@.type=="Ready")].status
# Troubleshoot ACME stages
kubectl get order,challenge,certificaterequest -n cert-manager
kubectl describe order <order-name> -n cert-manager
kubectl describe challenge <challenge-name> -n cert-manager
kubectl describe certificaterequest <certrequest-name> -n cert-manager
# Inspect the final TLS secret
kubectl get secret wildcard-cert-prod-example-tls -n cert-manager
kubectl describe secret wildcard-cert-prod-example-tls -n cert-manager
Use Let’s Encrypt Staging First
Always start with the staging environment:
server: https://acme-staging-v02.api.letsencrypt.org/directory
Let’s Encrypt production enforces rate limits (about 50 certificates per domain per week). Staging has no such restriction and is ideal for verifying DNS automation before switching to production.
Summary
Breaking the cert-manager setup into separate playbooks makes it easier to test and maintain:
- Helm configuration
- cert-manager install and issuer setup
- Reflector plugin deployment
- Wildcard certificate definition
Keep your secrets encrypted with Ansible Vault.
Verify cert-manager installation first, confirm ClusterIssuer readiness, and only then test certificate issuance with staging.
Once everything checks out, point the issuer to the production API and you’ll have fully automated TLS across your Kubernetes cluster.
Appendix: Quick Reference – cert-manager Verification Commands
When troubleshooting or validating your deployment, this quick reference covers the main checks from installation to active certificates.
Verify Installation
kubectl get ns | grep cert-manager
kubectl get pods -n cert-manager
kubectl get deployments -n cert-manager
kubectl describe deployment cert-manager -n cert-manager
kubectl get svc -n cert-manager
kubectl get crds | grep cert-manager.io
kubectl logs -n cert-manager deploy/cert-manager
Verify ClusterIssuer or Issuer
kubectl get issuer,clusterissuer
kubectl describe clusterissuer letsencrypt-staging
Verify Certificates
kubectl get certificate -n cert-manager
kubectl describe certificate wildcard-cert-prod-example -n cert-manager
Check Expiry and Readiness
kubectl -n cert-manager get certificate \
-o custom-columns=NAME:metadata.name,NOT_AFTER:status.notAfter,READY:status.conditions[?(@.type=="Ready")].status
Inspect ACME Orders, Challenges, and Requests
kubectl get order,challenge,certificaterequest -n cert-manager
kubectl describe order <order-name> -n cert-manager
kubectl describe challenge <challenge-name> -n cert-manager
kubectl describe certificaterequest <certrequest-name> -n cert-manager
Confirm the TLS Secret
kubectl get secret wildcard-cert-prod-example-tls -n cert-manager
kubectl describe secret wildcard-cert-prod-example-tls -n cert-manager