How to Install & Automate SSL Certificates on Kubernetes and NGINX Ingress Using ACME
-
Automating SSL/TLS for your Kubernetes cluster using an ACME SSL certificate keeps your applications secure without manual renewals. This guide uses cert-manager with External Account Binding (EAB) and shows how to configure a kubeadm Kubernetes cluster with NGINX Ingress, issue, install, auto-renew, and verify certificates. Example commands use placeholders, replace them with your actual values.
-
-
*
Prerequisites
Mandatory (required for ACME automation)
These requirements apply regardless of how the Kubernetes cluster is built. You must have the following in place to issue and auto-renew SSL/TLS certificates using ACME and cert-manager:
- Working Kubernetes cluster
- kubectl access to the cluster
- An Ingress controller installed and publicly reachable on port 80
- A domain with a DNS A record pointing to the cluster’s external IP
- ACME subscription with External Account Binding (EAB) credentials (Key ID and HMAC key)
- ACME server directory URL
Environment Used in This Guide (Reference Only)
The steps and examples in this guide were validated using the environment below. Users do not need to match this setup exactly.
- Ubuntu 22.04 node
- Single-node Kubernetes cluster initialized with kubeadm
- containerd runtime
- NGINX Ingress Controller
- MetalLB for external IP assignment
Note: Any Kubernetes distribution, Linux OS, container runtime, ingress controller, or load-balancing method may be used as long as the mandatory prerequisites above are met. -
1
Step 1 - Install cert-manager
Install the cert-manager CRDs and components used to request ACME certificates.
- Apply CRDs
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.1/cert-manager.crds.yaml --validate=false - Create namespace
kubectl create namespace cert-manager - Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.1/cert-manager.yaml - Verify cert-manager pods
kubectl get pods -n cert-manager --watch # or once: kubectl get pods -n cert-manager
Replace these placeholders with your own values:
- CERT_MANAGER_VERSION - cert-manager version used (v1.15.1+)
Image Caption: Executing cert-manager installation steps including applying CRDs, creating the namespace, deploying components, and verifying running pods.Tips:- If CRD apply fails, re-run with --validate=false to bypass validation errors.
- If cert-manager pods or webhook fail to start, verify the Kubernetes API server time is correct and CRDs are applied. Check logs with: kubectl logs -n cert-manager deploy/cert-manager
- If pods are stuck in Pending or CrashLoopBackOff, run kubectl describe pod POD_NAME -n cert-manager for detailed errors.
- Apply CRDs
-
2
Step 2 - Create EAB secret
Store your External Account Binding (EAB) credentials for the ClusterIssuer. Replace ALL CAPS placeholders before applying.
- Create YAML file acme-eab-secret.yaml (file content)
apiVersion:v1 kind:Secret metadata: name:acme-eab-secret namespace:cert-manager type:Opaque stringData: kid:"EAB_KID" hmacKey:"EAB_HMAC_KEY" - Apply the secret
kubectl apply -f acme-eab-secret.yaml # OR kubectl create secret generic acme-eab-secret \ --namespace cert-manager \ --from-literal=kid="EAB_KID" \ --from-literal=hmacKey="EAB_HMAC_KEY"
Replace these placeholders with your own values:
- EAB_KID External Account Binding Key ID
- EAB_HMAC_KEY External Account Binding HMAC Key
Image Caption: Creating and applying the acme-eab-secret.yaml file containing the EAB KID and HMAC key for cert-manager.Tips:- If ClusterIssuer reports missing EAB key data, verify the key: field in your ClusterIssuer matches hmacKey in the secret.
- If the secret is not recognized, confirm it exists in the cert-manager namespace with the correct name (acme-eab-secret).
- If apply fails, check YAML formatting and re-run: kubectl apply -f acme-eab-secret.yaml
- Create YAML file acme-eab-secret.yaml (file content)
-
3
Step 3 - Create ClusterIssuer for ACME + EAB (HTTP-01)
Define a cluster-wide ACME issuer that uses your CA's ACME server and EAB secret for account registration. This configuration uses HTTP-01 challenge solving via NGINX Ingress.
- Create YAML file cluster-issuer.yaml
apiVersion:cert-manager.io/v1 kind:ClusterIssuer metadata: name:sectigo-prod spec: acme: email:ACCOUNT_EMAIL server:SERVER privateKeySecretRef: name:sectigo-prod externalAccountBinding: keyID:EAB_KID keySecretRef: name:acme-eab-secret key:hmacKey solvers: - http01: ingress: class:nginx
Replace these placeholders with your own values:- ACCOUNT_EMAIL - Email for ACME account (notifications)
- SERVER - ACME directory URL (e.g., https://acme.sectigo.com/v2/DV)
- EAB_KID - External Account Binding Key ID
- Apply ClusterIssuer
kubectl apply -f cluster-issuer.yaml - Verify ClusterIssuer Registration
kubectl describe clusterissuer sectigo-prod # look for Status: Ready=True and events showing registration
Image Caption: Creating the ACME ClusterIssuer, applying the cluster-issuer.yaml file, and verifying the issuer status as Ready in Kubernetes.Tips:- If ACME registration fails or shows invalid EAB, verify SERVER, EAB_KID, and EAB_HMAC_KEY values.
- If secret not found, confirm acme-eab-secret exists in the cert-manager namespace.
- If you receive HTTP 403/404 errors, verify the ACME server URL is correct.
- Create YAML file cluster-issuer.yaml
-
4
Step 4 - Create Ingress for HTTP-01 + TLS
Create an Ingress with cert-manager annotations to request and mount a TLS secret. Replace these placeholders with your own values:
- INGRESS_NAME - Ingress resource name (hello-ingress)
- NAMESPACE - Namespace where Ingress and Service live (default)
- DOMAIN - testroot.net
- SECRET_NAME - TLS secret name to be created by cert-manager (tls-secret)
- SERVICE_NAME - hello-service
- PATH - URL path for routing (use / for root, or /hello for the sample app)
- Ingress YAML header (save as ingress.yaml, section 1)
apiVersion:networking.k8s.io/v1 kind:Ingress metadata: name:INGRESS_NAME namespace:NAMESPACE annotations: kubernetes.io/ingress.class:"nginx"cert-manager.io/cluster-issuer:"sectigo-prod"nginx.ingress.kubernetes.io/ssl-redirect:"true" - Ingress spec TLS & rule (append to ingress.yaml, section 2)
spec: ingressClassName:nginx tls: - hosts: - DOMAIN secretName:SECRET_NAME rules: - host:DOMAIN http: paths: - path:/PATH pathType:Prefix backend: service: name:SERVICE_NAME port: number:80 - Apply Ingress
kubectl apply -f ingress.yaml - Check Ingress status
kubectl get ingress INGRESS_NAME -n NAMESPACE -o wide
Image Caption: Creating and applying the hello-ingress manifest with TLS and ClusterIssuer annotations, and verifying the ingress host, class, and address using kubectl get ingress.Tips:- If the ADDRESS column is empty or not showing the MetalLB IP → verify the NGINX Ingress service and MetalLB setup:
kubectl get svc -n ingress-nginx - If ACME solver pods or temporary challenge ingresses are failing, inspect them using:
kubectl get pods -A | grep acme
kubectl get ingress -A | grep acme - If TLS is not provisioning and Ingress class mismatch occurs, ensure the following match the controller name:
ingressClassName: nginx
annotation: kubernetes.io/ingress.class: "nginx"
-
5
Step 5 - Monitor Certificate Issuance
Verify cert-manager creates a Certificate and CertificateRequest, and confirm the HTTP-01 challenge succeeds. Replace these placeholders with your own values below:
- CERTREQ_NAME - Name of the CertificateRequest created by cert-manager
- SECRET_NAME - tls-secret (certificate secret)
- List Certificates
kubectl get certificate -A - Describe Certificate (SECRET_NAME)
kubectl describe certificate SECRET_NAME -n NAMESPACE
Image Caption: Verifying the issued TLS certificate by checking the tls-secret status and describing the Certificate resource to confirm it is Ready. - Inspect CertificateRequest
kubectl get certificaterequest -A kubectl describe certificaterequest <CERTREQ_NAME> -n <NAMESPACE>
Image Caption: Output of kubectl get certificaterequest and kubectl describe certificaterequest, confirming that the tls-secret-1 request is Approved and Ready using the sectigo-prod ClusterIssuer.Tips:- If the Certificate shows READY=False:
Run:
kubectl describe certificaterequest <name>
This will usually show the ACME server error such as DNS failure, unreachable HTTP-01 challenge, or account registration issues. - If HTTP-01 challenge fails with 404 (common issue):
This means the Ingress is not routing to the ACME solver pod.
Fix by checking:
- ingressClassName matches the controller (e.g., nginx)
- Ingress Controller Service has a reachable external/MetalLB IP
- No conflicting Ingress rules
- If the Certificate shows READY=False:
-
6
Step 6 - Verify Externally
Confirm TLS is served and the certificate is issued by your CA.
- HTTP to HTTPS redirect check
curl -I http://DOMAIN
Expect: 308 redirect to https://DOMAIN
Note: Replace DOMAIN with your actual domain name (for example, testroot.net). - TLS handshake and cert details
curl -vk https://DOMAIN
Image Caption: Verifying HTTP→HTTPS redirection (308) and testing the secured endpoint using curl -vk to confirm TLS 1.3, CA-issued certificate, and successful HTTPS response. - Verify Kubernetes secret contains TLS data
kubectl get secret SECRET_NAME -n NAMESPACE -o yaml
Image Caption: Output of kubectl get secret tls-secret -n default -o yaml showing the base64-encoded TLS certificate and private key. - (Optional) Verify Certificate in web browser
Access your domain using a web browser, click the padlock icon in the address bar, and inspect the certificate details. Confirm that the certificate is issued by the CA and that the connection is fully secured.
Image Caption: TLS certificate details displayed in the browser, confirming the CA as the issuer and secure connection for the domain.
Tips:- Connection refused: Ingress endpoint unreachable. Verify MetalLB external IP assignment, Ingress-NGINX service EXTERNAL-IP, and firewall/SG rules allowing 80/443.
- Wrong or unexpected certificate (self-signed/CA not issued): A manual TLS secret exists or cert-manager failed issuance. Delete the manual secret and inspect:
kubectl describe certificaterequest and kubectl describe certificate for ACME/CA errors. - If the browser shows a warning or incomplete chain: check that the tls-secret contains the full certificate chain (tls.crt) and that cert-manager is correctly issuing the fullchain.
- HTTP to HTTPS redirect check
-
7
Step 7 - Manual Renewal Test
Test cert-manager's ability to re-issue a certificate by deleting the existing Certificate and Secret.
WARNING: This temporarily deletes the TLS secret and causes brief downtime. Use only in test environments.
- View current certificate expiry
kubectl describe certificate SECRET_NAME -n NAMESPACE
Image Caption: kubectl describe certificate output showing cert-manager successfully issuing a valid TLS certificate (Status: Ready) for the domain testroot.net. - Force re-issue (exact two commands)
kubectl delete secret SECRET_NAME -n NAMESPACE kubectl delete certificate SECRET_NAME -n NAMESPACE
Note: Replace NAMESPACE with your actual Kubernetes namespace (for example, default). - Watch cert-manager recreate and reissue
kubectl get certificaterequest -A kubectl get certificate -A kubectl get secret -n NAMESPACE
Image Caption: Renewal triggered — cert-manager creates new CertificateRequest and Secret.
Tips:- CertificateRequest not Ready: Check cert-manager logs for ACME/issuer errors:
kubectl logs -n cert-manager deploy/cert-manager - Certificate stuck Pending: Verify DNS A record matches the MetalLB external IP and Ingress is reachable over HTTP.
- Secret not created/empty: Ensure Certificate resource references correct secretName and issuer (ClusterIssuer) is Ready.
- View current certificate expiry
-
Optional Checks & Troubleshooting
-
These steps are not required for most installations but are helpful if you encounter issues. You may also contact our support team for more questions.
-
-
*
Quick Cluster Verification (Optional)
Run these checks to confirm your Kubernetes cluster, ingress controller, DNS, and network are correctly configured before starting the ACME installation.
- Verify cluster health
kubectl get nodes - Verify ingress controller is running
kubectl get pods -n ingress-nginx - Verify ingress service has an external IP
kubectl get svc -n ingress-nginx - Verify DNS resolution
nslookup DOMAIN - Verify port 80 is reachable
curl -I http://DOMAIN
This confirms:
- Cluster health
- Ingress controller availability
- EXTERNAL-IP assignment
- DNS resolution
- HTTP (port 80) reachability for ACME HTTP-01 challenge
- Verify cluster health
-
*
Summary
You have successfully:
- Installed and verified cert-manager
- Created a CA EAB secret
- Created ClusterIssuer with HTTP-01 solver
- Deployed sample app and Service
- Created Ingress annotated for cert-manager
- Issued and verified TLS certificate
- Tested manual renewal and verified automatic renewal
SSL renewals now run automatically without manual intervention.
-
*
Final Placeholder Library (Universal + Kubernetes-focused)
Placeholder - Description
- ACCOUNT_EMAIL - Email used for ACME account registration
- CERT_MANAGER_VERSION - cert-manager version (v1.15.1+)
- CERTREQ_NAME - Certificate request resource name (auto-generated by cert-manager, appears in kubectl output)
- DEPLOYMENT_NAME - Name of the Deployment (hello)
- DOMAIN - Fully qualified domain (e.g., testroot.net)
- EAB_HMAC_KEY - External Account Binding HMAC Key
- EAB_KID - External Account Binding Key ID
- INGRESS_NAME - Name of the Ingress resource (hello-ingress)
- NAMESPACE - Kubernetes namespace (default: default)
- POD_NAME - Specific pod name when running kubectl debugging commands (from kubectl get pods output)
- SECRET_NAME - TLS secret created by cert-manager (tls-secret)
- SERVER - ACME server URL (e.g., https://acme.sectigo.com/v2/DV)
- SERVICE_NAME - Name of the Service (hello-service)
-