$ -weight: 500;">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml
-weight: 500;">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml
-weight: 500;">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: name: letsencrypt-prod
spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod-account solvers: - http01: ingress: class: nginx
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: name: letsencrypt-prod
spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod-account solvers: - http01: ingress: class: nginx
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: name: letsencrypt-prod
spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod-account solvers: - http01: ingress: class: nginx
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: name: my-app annotations: cert-manager.io/cluster-issuer: letsencrypt-prod
spec: tls: - hosts: ["app.example.com"] secretName: app-example-com-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: -weight: 500;">service: name: my-app port: number: 80
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: name: my-app annotations: cert-manager.io/cluster-issuer: letsencrypt-prod
spec: tls: - hosts: ["app.example.com"] secretName: app-example-com-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: -weight: 500;">service: name: my-app port: number: 80
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: name: my-app annotations: cert-manager.io/cluster-issuer: letsencrypt-prod
spec: tls: - hosts: ["app.example.com"] secretName: app-example-com-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: -weight: 500;">service: name: my-app port: number: 80
- name: Issue and upload wildcard TLS certificate hosts: localhost vars: sld: "example" tld: "com" region: "eu-center" wildcard_domain: "*.{{ region }}.{{ sld }}.{{ tld }}" local_tmp: "/tmp/wildcard-{{ region }}" k8s_namespace: "ingress-nginx" k8s_secret_name: "wildcard-{{ region }}-tls" tasks: - name: Create certbot auth hook (creates the TXT record) copy: dest: "/tmp/certbot-auth-{{ region }}.sh" mode: "0755" content: | #!/bin/bash set -e namecheap-cli setone \ --sld {{ sld }} --tld {{ tld }} \ --type TXT --name "_acme-challenge.{{ region }}" \ --address "${CERTBOT_VALIDATION}" --ttl 60 # Wait for DNS to propagate for i in {1..30}; do val=$(dig TXT _acme-challenge.{{ region }}.{{ sld }}.{{ tld }} @1.1.1.1 +short | tr -d '"') [[ "$val" == "${CERTBOT_VALIDATION}" ]] && break sleep 10 done sleep 30 # belt and suspenders - name: Issue wildcard certificate command: > certbot certonly --manual --preferred-challenges dns --manual-auth-hook /tmp/certbot-auth-{{ region }}.sh --manual-cleanup-hook /tmp/certbot-cleanup-{{ region }}.sh --agree-tos -m [email protected] --server https://acme-v02.api.letsencrypt.org/directory -d "{{ wildcard_domain }}" --work-dir {{ local_tmp }} --config-dir {{ local_tmp }} --logs-dir {{ local_tmp }} --non-interactive - name: Create or -weight: 500;">update TLS Secret kubernetes.core.k8s: state: present namespace: "{{ k8s_namespace }}" definition: apiVersion: v1 kind: Secret metadata: name: "{{ k8s_secret_name }}" type: kubernetes.io/tls data: tls.crt: "{{ lookup('file', local_tmp + '/live/.../fullchain.pem') | b64encode }}" tls.key: "{{ lookup('file', local_tmp + '/live/.../privkey.pem') | b64encode }}"
- name: Issue and upload wildcard TLS certificate hosts: localhost vars: sld: "example" tld: "com" region: "eu-center" wildcard_domain: "*.{{ region }}.{{ sld }}.{{ tld }}" local_tmp: "/tmp/wildcard-{{ region }}" k8s_namespace: "ingress-nginx" k8s_secret_name: "wildcard-{{ region }}-tls" tasks: - name: Create certbot auth hook (creates the TXT record) copy: dest: "/tmp/certbot-auth-{{ region }}.sh" mode: "0755" content: | #!/bin/bash set -e namecheap-cli setone \ --sld {{ sld }} --tld {{ tld }} \ --type TXT --name "_acme-challenge.{{ region }}" \ --address "${CERTBOT_VALIDATION}" --ttl 60 # Wait for DNS to propagate for i in {1..30}; do val=$(dig TXT _acme-challenge.{{ region }}.{{ sld }}.{{ tld }} @1.1.1.1 +short | tr -d '"') [[ "$val" == "${CERTBOT_VALIDATION}" ]] && break sleep 10 done sleep 30 # belt and suspenders - name: Issue wildcard certificate command: > certbot certonly --manual --preferred-challenges dns --manual-auth-hook /tmp/certbot-auth-{{ region }}.sh --manual-cleanup-hook /tmp/certbot-cleanup-{{ region }}.sh --agree-tos -m [email protected] --server https://acme-v02.api.letsencrypt.org/directory -d "{{ wildcard_domain }}" --work-dir {{ local_tmp }} --config-dir {{ local_tmp }} --logs-dir {{ local_tmp }} --non-interactive - name: Create or -weight: 500;">update TLS Secret kubernetes.core.k8s: state: present namespace: "{{ k8s_namespace }}" definition: apiVersion: v1 kind: Secret metadata: name: "{{ k8s_secret_name }}" type: kubernetes.io/tls data: tls.crt: "{{ lookup('file', local_tmp + '/live/.../fullchain.pem') | b64encode }}" tls.key: "{{ lookup('file', local_tmp + '/live/.../privkey.pem') | b64encode }}"
- name: Issue and upload wildcard TLS certificate hosts: localhost vars: sld: "example" tld: "com" region: "eu-center" wildcard_domain: "*.{{ region }}.{{ sld }}.{{ tld }}" local_tmp: "/tmp/wildcard-{{ region }}" k8s_namespace: "ingress-nginx" k8s_secret_name: "wildcard-{{ region }}-tls" tasks: - name: Create certbot auth hook (creates the TXT record) copy: dest: "/tmp/certbot-auth-{{ region }}.sh" mode: "0755" content: | #!/bin/bash set -e namecheap-cli setone \ --sld {{ sld }} --tld {{ tld }} \ --type TXT --name "_acme-challenge.{{ region }}" \ --address "${CERTBOT_VALIDATION}" --ttl 60 # Wait for DNS to propagate for i in {1..30}; do val=$(dig TXT _acme-challenge.{{ region }}.{{ sld }}.{{ tld }} @1.1.1.1 +short | tr -d '"') [[ "$val" == "${CERTBOT_VALIDATION}" ]] && break sleep 10 done sleep 30 # belt and suspenders - name: Issue wildcard certificate command: > certbot certonly --manual --preferred-challenges dns --manual-auth-hook /tmp/certbot-auth-{{ region }}.sh --manual-cleanup-hook /tmp/certbot-cleanup-{{ region }}.sh --agree-tos -m [email protected] --server https://acme-v02.api.letsencrypt.org/directory -d "{{ wildcard_domain }}" --work-dir {{ local_tmp }} --config-dir {{ local_tmp }} --logs-dir {{ local_tmp }} --non-interactive - name: Create or -weight: 500;">update TLS Secret kubernetes.core.k8s: state: present namespace: "{{ k8s_namespace }}" definition: apiVersion: v1 kind: Secret metadata: name: "{{ k8s_secret_name }}" type: kubernetes.io/tls data: tls.crt: "{{ lookup('file', local_tmp + '/live/.../fullchain.pem') | b64encode }}" tls.key: "{{ lookup('file', local_tmp + '/live/.../privkey.pem') | b64encode }}"
spec: tls: - hosts: ["*.region.example.com"] secretName: wildcard-region-tls
spec: tls: - hosts: ["*.region.example.com"] secretName: wildcard-region-tls
spec: tls: - hosts: ["*.region.example.com"] secretName: wildcard-region-tls
ImportError: cannot import name 'appengine' from 'urllib3.contrib'
ImportError: cannot import name 'appengine' from 'urllib3.contrib'
ImportError: cannot import name 'appengine' from 'urllib3.contrib'
- name: Issue wildcard certificate environment: PYTHONNOUSERSITE: "1" command: > certbot certonly --manual ...
- name: Issue wildcard certificate environment: PYTHONNOUSERSITE: "1" command: > certbot certonly --manual ...
- name: Issue wildcard certificate environment: PYTHONNOUSERSITE: "1" command: > certbot certonly --manual ... - Our DNS provider (Namecheap) does not have a stable cert-manager webhook. There are community webhooks, but they break on upgrades. Maintaining one for a single cert is more work than running certbot once a quarter.
- The wildcard cert covers our shared ingress, not user apps. It rotates rarely, lives in one namespace, and is read by every ingress as a TLS secret. cert-manager is built for the opposite case: many short-lived certs per Ingress.
- A failed cert-manager renewal at 3 a.m. is hard to debug. A failed Ansible run on our laptop is a stack trace we can read. - Ansible writes two scripts: an auth hook (creates the TXT record) and a cleanup hook (deletes it).
- certbot --manual --preferred-challenges dns runs the auth hook, waits for DNS to propagate, lets ACME verify, then runs the cleanup hook.
- The resulting fullchain.pem and privkey.pem get loaded into a Kubernetes Secret of type kubernetes.io/tls.
- Every ingress in the shared namespace references that secret. - Your DNS provider has no first-party or stable cert-manager support
- You have one wildcard, not many
- You would rather audit a 30-line shell script than a webhook deployment - Per-app domains → cert-manager + HTTP-01 + ClusterIssuer. One annotation per Ingress, automatic renewals.
- Wildcards → DNS-01 is mandatory. Use cert-manager with your DNS provider's webhook if it exists. Otherwise, a 60-day Ansible run with certbot --manual and a TLS Secret.
- Two tools is fine. Don't force one model onto two different problems.