aly badawy/homelab
all systems operational
// cluster · tls

cert-manager

Automated certificate issuance and renewal. A single wildcard certificate covers all *.in.alybadawy.com hostnames. cert-manager handles the full lifecycle — no manual cert management ever.

Valid namespace: networking wildcard cert DNS-01 / Cloudflare

cert-manager runs the full ACME DNS-01 challenge against Cloudflare to issue a wildcard cert for *.in.alybadawy.com. The resulting wildcard-tls Secret is loaded by ingress-nginx as its default certificate — every HTTPS hostname in the cluster gets the wildcard automatically, with zero per-Ingress configuration.

01 Three-component architecture

TLS works because three resources are set up correctly and in the right order:

  1. ClusterIssuer letsencrypt-prod — tells cert-manager how to talk to Let's Encrypt and how to prove domain ownership (DNS-01 via Cloudflare API).
  2. Certificate wildcard-in-alybadawy-com — requests the actual cert. cert-manager creates a DNS TXT record in Cloudflare, Let's Encrypt verifies it, cert-manager stores the result in the wildcard-tls Secret.
  3. ingress-nginx default-ssl-certificate — tells nginx to load wildcard-tls for all incoming HTTPS connections. Per-Ingress tls[] blocks are not needed.
Why DNS-01? HTTP-01 challenges cannot issue wildcard certificates — Let's Encrypt requires DNS-01 for wildcards. DNS-01 also works for private/LAN-only services that aren't publicly reachable over HTTP, which is exactly this cluster's use case.

02 ClusterIssuer

Two issuers exist: letsencrypt-prod (used by default) and letsencrypt-staging (for testing without hitting rate limits). Both use DNS-01 with the Cloudflare solver. cert-manager is configured to query 8.8.8.8 and 1.1.1.1 directly for propagation checks, bypassing any local DNS that might not see the TXT record yet.

k8s/components/cert-manager/cluster-issuer.yaml (excerpt) yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: alybadawy@icloud.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token  # seeded by provision/bootstrap-argocd in step 7

03 The wildcard Certificate

One Certificate resource covers every current and future hostname under in.alybadawy.com. Adding a new service never requires touching cert-manager.

k8s/components/cert-manager/wildcard-certificate.yaml yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-in-alybadawy-com
  namespace: networking
  annotations:
    argocd.argoproj.io/sync-wave: "2"  # after ClusterIssuer at wave 1
spec:
  secretName: wildcard-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: "*.in.alybadawy.com"
  dnsNames:
    - "*.in.alybadawy.com"
    - "in.alybadawy.com"

cert-manager automatically renews the cert before expiry. Let's Encrypt certs are valid for 90 days; renewal starts at 60 days. When cert-manager writes an updated wildcard-tls Secret, ingress-nginx hot-reloads it — no downtime, no manual steps.

04 Adding a new service

Because TLS is handled globally, adding a new HTTPS service requires zero cert-manager configuration:

  1. Create an Ingress resource with ingressClassName: nginx and your hostname.
  2. Add nginx.ingress.kubernetes.io/force-ssl-redirect: "true" for HTTP→HTTPS redirect.
  3. Do not add cert-manager.io/cluster-issuer. Do not add a tls[] block.

The wildcard cert already covers the new hostname. nginx serves it automatically.

05 Troubleshooting

cert status checks bash
# certificate status and expiry
$ kubectl get certificate wildcard-in-alybadawy-com -n networking
NAME                        READY   SECRET         AGE
wildcard-in-alybadawy-com   True    wildcard-tls   5d

# verify SANs and expiry date
$ kubectl get secret wildcard-tls -n networking \
    -o jsonpath='{.data.tls\.crt}' | base64 -d \
    | openssl x509 -noout -text | grep -A2 "Subject Alternative\|Not After"

# watch cert-manager reconcile (useful during first issuance)
$ kubectl logs -n networking -l app.kubernetes.io/name=cert-manager --tail=50
DNS prerequisite. The _acme-challenge.in.alybadawy.com TXT record requires Cloudflare to have an explicit A or CNAME record for in.alybadawy.com — without it, Cloudflare's DNS will store the TXT but not serve it. Both in.alybadawy.com and *.in.alybadawy.com should have CNAME records pointing to alybadawy.com (unproxied).