{"id":442,"date":"2025-01-23T05:45:23","date_gmt":"2025-01-23T05:45:23","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=442"},"modified":"2025-01-23T05:45:23","modified_gmt":"2025-01-23T05:45:23","slug":"nginx-ingress-with-cert-manager","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=442","title":{"rendered":"How to set up Nginx Ingress with Cert Manager in EKS"},"content":{"rendered":"<p>In this blog, we will learn how to integrate TLS certificates into a Kubernetes cluster using <strong>Cert-Manager<\/strong> and the <strong>Let&#8217;s Encrypt<\/strong> Certificate Authority.<\/p>\n<p>By the end of this blog, you will have learned.<\/p>\n<ol>\n<li>Installation of CertManager<\/li>\n<li>Configuration of Let&#8217;s encrypt Certificate Authority to Certmanager<\/li>\n<\/ol>\n<h2 id=\"what-is-cert-manager\">What is Cert Manager?<\/h2>\n<p><a href=\"https:\/\/devopscube.com\/configure-ingress-tls-kubernetes\/\" rel=\"noreferrer noopener\">SSL\/TLS certificates<\/a> are essential for Kubernetes <strong>Ingress<\/strong> objects to secure communication between the users and the application.<\/p>\n<p>Cert Manager is a tool that creates and manages TLS certificates for Ingress objects, also automatically renewing their validity at the right time.<\/p>\n<p>The Cert Manager works with <strong>Certificate Authorities (CA)<\/strong> such as Let&#8217;s Encrypt, Hashicorp Vault, etc.<\/p>\n<p>Before setting up the Cert Manager, let us understand its workflow.<\/p>\n<h2 id=\"cert-manager-workflow\">Cert Manager Workflow<\/h2>\n<p>The diagram below explains how the Cert manager works with the kubernetes cluster to provision and manage the TLS Certificates to safeguard the Ingress.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-38.png\" class=\"kg-image\" alt=\"the workflow diagram of the cert manager in the kubernets cluster with the lets encrypt certificate authority\" loading=\"lazy\" width=\"2000\" height=\"2279\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/11\/image-38.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/11\/image-38.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/11\/image-38.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-38.png 2216w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<ol>\n<li>When we create an Ingress object, we add a reference to the Cert Manager Issuer in it.<\/li>\n<li>The Ingress Controller retrieves information from the Ingress object and requests a certificate from the Certificate Manager.<\/li>\n<li>The Cert Manager will request the Certificate Authority, for example, Let&#8217;s Encrypt.<\/li>\n<li>After the verification, the CA will generate and provide the certificate to the Cert Manager.<\/li>\n<li>The generated certificate will be stored in Kubernetes as a TLS Secret.<\/li>\n<li>The Ingress Controller will perform the TLS termination using the stored certificate.<\/li>\n<li>When a user tries to access the application, the external traffic is routed from the external Load Balancer to the Ingress Controller.<\/li>\n<li>The TLS termination will happen in the Ingress Controller with the TLS certificate and securely route the traffic to the application Pods.<\/li>\n<\/ol>\n<h2 id=\"how-to-set-up-cert-manager-on-kubernetes\">How to Set Up Cert Manager on Kubernetes<\/h2>\n<p>We will install the Cert Manager using the Helm chart. Before adding the repository, we need to ensure the following prerequisites are met.<\/p>\n<h3 id=\"prerequisites\">Prerequisites<\/h3>\n<ol>\n<li><a href=\"https:\/\/devopscube.com\/upgrade-kubernetes-cluster-kubeadm\/\" rel=\"noreferrer noopener\">Kubernetes Cluster<\/a> version 1.30+<\/li>\n<li>Nginx Ingress Controller [Kubernetes cluster]<\/li>\n<li><a href=\"https:\/\/devopscube.com\/create-helm-chart\/\" rel=\"noreferrer noopener\">Helm<\/a> [Local Workstation]<\/li>\n<li>Kubectl [Local Workstation]<\/li>\n<li>DNS Provider (Route53, Cloudflare, etc)<\/li>\n<\/ol>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">For this tutorial, I am using an Amazon EKS cluster and Route53 as the DNS provider.<br \/>A DNS provider is required for our certificate issuer (Let&#8217;s Encrypt). It verifies the validity of domain names to provide certificates.<\/div>\n<\/div>\n<h3 id=\"step-1-install-cert-manager-on-kubernetes\">Step 1: Install Cert Manager on Kubernetes<\/h3>\n<p>First, we need to add the Cert Manager Helm Repository<\/p>\n<pre><code>helm repo add jetstack https:\/\/charts.jetstack.io --force-update<\/code><\/pre>\n<p>Update the repository before installation<\/p>\n<pre><code>helm repo update<\/code><\/pre>\n<p>Now, we are ready to deploy the Cert Manager, but if you want to download the Helm chart to store your own repo, use the following command.<\/p>\n<pre><code>helm pull jetstack\/cert-manager --untar<\/code><\/pre>\n<p>The following is the tree structure of the Helm chart<\/p>\n<pre><code>cert-manager\n\u251c\u2500\u2500 Chart.yaml\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 templates\n\u2502   \u251c\u2500\u2500 NOTES.txt\n\u2502   \u251c\u2500\u2500 _helpers.tpl\n\u2502   \u251c\u2500\u2500 cainjector-config.yaml\n\u2502   \u251c\u2500\u2500 cainjector-deployment.yaml\n\u2502   \u251c\u2500\u2500 cainjector-poddisruptionbudget.yaml\n\u2502   \u251c\u2500\u2500 cainjector-psp-clusterrole.yaml\n\u2502   \u251c\u2500\u2500 cainjector-psp-clusterrolebinding.yaml\n\u2502   \u251c\u2500\u2500 cainjector-psp.yaml\n\u2502   \u251c\u2500\u2500 cainjector-rbac.yaml\n\u2502   \u251c\u2500\u2500 cainjector-service.yaml\n\u2502   \u251c\u2500\u2500 cainjector-serviceaccount.yaml\n\u2502   \u251c\u2500\u2500 controller-config.yaml\n\u2502   \u251c\u2500\u2500 crd-acme.cert-manager.io_challenges.yaml\n\u2502   \u251c\u2500\u2500 crd-acme.cert-manager.io_orders.yaml\n\u2502   \u251c\u2500\u2500 crd-cert-manager.io_certificaterequests.yaml\n\u2502   \u251c\u2500\u2500 crd-cert-manager.io_certificates.yaml\n\u2502   \u251c\u2500\u2500 crd-cert-manager.io_clusterissuers.yaml\n\u2502   \u251c\u2500\u2500 crd-cert-manager.io_issuers.yaml\n\u2502   \u251c\u2500\u2500 deployment.yaml\n\u2502   \u251c\u2500\u2500 extras-objects.yaml\n\u2502   \u251c\u2500\u2500 networkpolicy-egress.yaml\n\u2502   \u251c\u2500\u2500 networkpolicy-webhooks.yaml\n\u2502   \u251c\u2500\u2500 poddisruptionbudget.yaml\n\u2502   \u251c\u2500\u2500 podmonitor.yaml\n\u2502   \u251c\u2500\u2500 psp-clusterrole.yaml\n\u2502   \u251c\u2500\u2500 psp-clusterrolebinding.yaml\n\u2502   \u251c\u2500\u2500 psp.yaml\n\u2502   \u251c\u2500\u2500 rbac.yaml\n\u2502   \u251c\u2500\u2500 service.yaml\n\u2502   \u251c\u2500\u2500 serviceaccount.yaml\n\u2502   \u251c\u2500\u2500 servicemonitor.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-job.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-psp-clusterrole.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-psp-clusterrolebinding.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-psp.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-rbac.yaml\n\u2502   \u251c\u2500\u2500 startupapicheck-serviceaccount.yaml\n\u2502   \u251c\u2500\u2500 webhook-config.yaml\n\u2502   \u251c\u2500\u2500 webhook-deployment.yaml\n\u2502   \u251c\u2500\u2500 webhook-mutating-webhook.yaml\n\u2502   \u251c\u2500\u2500 webhook-poddisruptionbudget.yaml\n\u2502   \u251c\u2500\u2500 webhook-psp-clusterrole.yaml\n\u2502   \u251c\u2500\u2500 webhook-psp-clusterrolebinding.yaml\n\u2502   \u251c\u2500\u2500 webhook-psp.yaml\n\u2502   \u251c\u2500\u2500 webhook-rbac.yaml\n\u2502   \u251c\u2500\u2500 webhook-service.yaml\n\u2502   \u251c\u2500\u2500 webhook-serviceaccount.yaml\n\u2502   \u2514\u2500\u2500 webhook-validating-webhook.yaml\n\u251c\u2500\u2500 values.schema.json\n\u2514\u2500\u2500 values.yaml\n\n2 directories, 52 files<\/code><\/pre>\n<p>The following are the container images used in this Helm chart<\/p>\n<ul>\n<li><code>quay.io\/jetstack\/cert-manager-controller<\/code><\/li>\n<li><code>quay.io\/jetstack\/cert-manager-webhook<\/code><\/li>\n<li><code>quay.io\/jetstack\/cert-manager-cainjector<\/code><\/li>\n<li><code>quay.io\/jetstack\/cert-manager-acmesolver<\/code><\/li>\n<li><code>quay.io\/jetstack\/cert-manager-startupapicheck<\/code><\/li>\n<\/ul>\n<p>In the Helm chart, you can see a values file (<code>values.yaml<\/code>) which has all modifiable values that we can change if required.<\/p>\n<p>Instead of editing this main file, we create a new file and add our own settings.<\/p>\n<p>Create a file named <code>custom-values.yaml<\/code> and add the following contents to enable the Cert Manager Custom Resource Definitions<\/p>\n<pre><code class=\"language-bash\">crds:\n  enabled: true\n  keep: false<\/code><\/pre>\n<p>The modification we have done is<\/p>\n<ul>\n<li><code>crds.enabled: true<\/code> &#8211; To install the Cert Manager&#8217;s required CRDs <\/li>\n<li><code>crds.keep: false<\/code> &#8211; To remove the CRDs when we uninstall the Cert Manager.<\/li>\n<\/ul>\n<p>After making the changes, we can install the Cert Manager using the following command.<\/p>\n<pre><code class=\"language-bash\">helm install \\\n  cert-manager jetstack\/cert-manager \\\n  --namespace cert-manager \\\n  --create-namespace \\\n  --values custom-values.yaml<\/code><\/pre>\n<h3 id=\"step-2-validating-the-cert-manager-installation\">Step 2: Validating the Cert Manager Installation<\/h3>\n<p>Once the installation is complete, verify the Cert Manager resources and ensure everything is running as expected. <\/p>\n<pre><code class=\"language-bash\">kubectl get all -n cert-manager<\/code><\/pre>\n<p>You will get the following output<\/p>\n<pre><code class=\"language-bash\">cert-manager\nNAME                                           READY   STATUS    RESTARTS   AGE\npod\/cert-manager-55c5fdb5f5-hfp7t              1\/1     Running   0          87s\npod\/cert-manager-cainjector-5dc4bf4cdf-fgwtz   1\/1     Running   0          87s\npod\/cert-manager-webhook-6ff7dcb868-4fkbp      1\/1     Running   0          87s\n\nNAME                              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)            AGE\nservice\/cert-manager              ClusterIP   10.100.223.113   &lt;none&gt;        9402\/TCP           87s\nservice\/cert-manager-cainjector   ClusterIP   10.100.192.136   &lt;none&gt;        9402\/TCP           87s\nservice\/cert-manager-webhook      ClusterIP   10.100.161.169   &lt;none&gt;        443\/TCP,9402\/TCP   87s\n\nNAME                                      READY   UP-TO-DATE   AVAILABLE   AGE\ndeployment.apps\/cert-manager              1\/1     1            1           88s\ndeployment.apps\/cert-manager-cainjector   1\/1     1            1           88s\ndeployment.apps\/cert-manager-webhook      1\/1     1            1           88s\n\nNAME                                                 DESIRED   CURRENT   READY   AGE\nreplicaset.apps\/cert-manager-55c5fdb5f5              1         1         1       88s\nreplicaset.apps\/cert-manager-cainjector-5dc4bf4cdf   1         1         1       88s\nreplicaset.apps\/cert-manager-webhook-6ff7dcb868      1         1         1       88s<\/code><\/pre>\n<p>We also need to check whether the Custom Resource Definitions are deployed or not.<\/p>\n<pre><code class=\"language-bash\">kubectl get crds<\/code><\/pre>\n<p>You will get the following output<\/p>\n<pre><code class=\"language-bash\">$ kubectl get crds | grep -iE \"cert-manager\"\n\ncertificaterequests.cert-manager.io          2025-11-04T10:42:30Z\ncertificates.cert-manager.io                 2025-11-04T10:42:30Z\nchallenges.acme.cert-manager.io              2025-11-04T10:42:31Z\nclusterissuers.cert-manager.io               2025-11-04T10:42:32Z\nissuers.cert-manager.io                      2025-11-04T10:42:32Z\norders.acme.cert-manager.io                  2025-11-04T10:42:30Z<\/code><\/pre>\n<p>The following is an explanation of the Custom Resources.<\/p>\n<ul>\n<li><code>certificaterequests<\/code> &#8211; To track the certificate request progress and status.<\/li>\n<li><code>certificates<\/code> &#8211; Store the certificates created by the Certificate Authority (CA)<\/li>\n<li><code>challenges<\/code> &#8211; To verify the ownership of the requester<\/li>\n<li><code>clusterissuers<\/code> &#8211; Certificate issuers for the entire cluster<\/li>\n<li><code>issuers<\/code> &#8211; Certificate issuers for specific namespaces<\/li>\n<li><code>orders<\/code> &#8211; To track all the requests made to the Certificate Authority <\/li>\n<\/ul>\n<p>Now, our Cert Manager is ready, so in the next step, we map the Load Balancer to Route53<\/p>\n<h2 id=\"map-the-aws-elb-to-route-53\">Map the AWS ELB to Route 53<\/h2>\n<p>Assuming you already installed Ingress controller on EKS, then it should have created a Load Balancer.<\/p>\n<p>If you are not setup Ingress, refer this &#8211;&gt; <a href=\"https:\/\/devopscube.com\/setup-ingress-kubernetes-nginx-controller\/#deploy-jobs-to-update-webhook-certificates\" rel=\"noreferrer\"><strong>Setup Nginx Ingress Controller On Kubernetes<\/strong><\/a><\/p>\n<p>Now, we are going to map that Load Balancer DNS to Route53 for public DNS resolution.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udea8<\/div>\n<div class=\"kg-callout-text\">You should have a hosted zone in the Route53 or any other service provider to continue this configuration.<\/div>\n<\/div>\n<p>Open the Route53 service in AWS and click &#8220;Create record&#8221;.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-29-10.png\" class=\"kg-image\" alt=\"creating a record on the route53 for the load balancer created by the nginx ingress controller.\" loading=\"lazy\" width=\"1063\" height=\"473\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-29-10.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/image-29-10.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-29-10.png 1063w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Fill the record with the following details of <\/p>\n<ul>\n<li>Prefix for the domain name &#8211; Here, I am giving as <code>nginx<\/code> so the domain name will be <code>nginx.devopsproject.dev<\/code><\/li>\n<li>Record type &#8211; Should be <code>A<\/code> to map with Load Balancer DNS<\/li>\n<li>Enable &#8220;Alias&#8221; to select the Load balancer type, region and DNS address.<\/li>\n<\/ul>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-29.png\" class=\"kg-image\" alt=\"the recored configuration for the load balancer such as the record name, type and the routing policy.\" loading=\"lazy\" width=\"2000\" height=\"1129\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/11\/image-29.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/11\/image-29.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/11\/image-29.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w2400\/2025\/11\/image-29.png 2400w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>To ensure the mapping is properly done, make a DNS query to the domain name.<\/p>\n<pre><code>$ dig nginx.devopsproject.dev\n\n; &lt;&lt;&gt;&gt; DiG 9.10.6 &lt;&lt;&gt;&gt; nginx.devopsproject.dev\n;; global options: +cmd\n;; Got answer:\n;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 55696\n;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 4096\n;; QUESTION SECTION:\n;nginx.devopsproject.dev.\tIN\tA\n\n;; ANSWER SECTION:\nnginx.devopsproject.dev. 60\tIN\tA\t44.252.195.62\nnginx.devopsproject.dev. 60\tIN\tA\t34.218.125.185\n\n;; AUTHORITY SECTION:\ndevopsproject.dev.\t172800\tIN\tNS\tns-1127.awsdns-12.org.\ndevopsproject.dev.\t172800\tIN\tNS\tns-1958.awsdns-52.co.uk.\ndevopsproject.dev.\t172800\tIN\tNS\tns-418.awsdns-52.com.\ndevopsproject.dev.\t172800\tIN\tNS\tns-966.awsdns-56.net.\n\n;; Query time: 22 msec\n;; SERVER: fe80::1%6#53(fe80::1%6)\n;; WHEN: Wed Nov 05 17:55:16 IST 2025\n;; MSG SIZE  rcvd: 224\n<\/code><\/pre>\n<p>We can see the query is routed to the IPs of the Load Balancer, which ensures that the resolution is properly done.<\/p>\n<p>Now, we need a demo application for the testing.<\/p>\n<h2 id=\"install-a-demo-application\">Install a Demo Application <\/h2>\n<p>I am deploying an Nginx web server for testing.<\/p>\n<pre><code class=\"language-bash\">kubectl create deployment web-server --image nginx --port 80<\/code><\/pre>\n<p>We need to create a service for this deployment.<\/p>\n<pre><code class=\"language-bash\">kubectl expose deployment web-server --name web-svc --port 80 --target-port 80              <\/code><\/pre>\n<p>Once the deployment is completed, ensure the web server is running.<\/p>\n<pre><code class=\"language-bash\">$ kubectl get po,svc\n\nNAME                             READY   STATUS    RESTARTS   AGE\npod\/web-server-6d7585d7f-z869v   1\/1     Running   0          4m59s\n\nNAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE\nservice\/kubernetes   ClusterIP   10.100.0.1      &lt;none&gt;        443\/TCP   57m\nservice\/web-svc      ClusterIP   10.100.174.18   &lt;none&gt;        80\/TCP    37s<\/code><\/pre>\n<p>Our test deployment is ready. So, we need to configure for the certificate creation.<\/p>\n<h2 id=\"create-a-cluster-issuer\">Create a Cluster Issuer <\/h2>\n<p>Here, we need to choose who should provide our certificates<\/p>\n<p>For this, we will use,<\/p>\n<p>Cluster Issuer CRD to request certificate and <strong>Let&#8217;s Encrypt<\/strong> as <strong>Certificate Authority<\/strong> to provide the certificate.<\/p>\n<p>Copy and paste the following contents on your terminal to create a manifest for the <code>ClusterIssuer<\/code> object.<\/p>\n<pre><code class=\"language-bash\">cat &lt;&lt; EOF &gt; cluster-issuer.yaml\napiVersion: cert-manager.io\/v1\nkind: ClusterIssuer\nmetadata:\n  name: letsencrypt-dev\nspec:\n  acme:\n    server: https:\/\/acme-v02.api.letsencrypt.org\/directory\n    email: devopscube@gmail.com\n    privateKeySecretRef:\n      name: letsencrypt-prod\n    solvers:\n      - http01:\n          ingress:\n            class: nginx\nEOF<\/code><\/pre>\n<p>This issuer works for the entire cluster, so you can utilize for any namespaces.<\/p>\n<p>Replace the email with yours to get notifications before the certificate expires.<\/p>\n<p>You can give any name on <code>privateKeySecretRef.name<\/code>, so that it creates a secret with that name to store a private key for the Let&#8217;s encrypt account (ACME account).<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\"><code spellcheck=\"false\" style=\"white-space: pre-wrap;\">spec.acme.solvers.http01.ingress.class: nginx<\/code> is the default <b><strong style=\"white-space: pre-wrap;\">Ingress Class<\/strong><\/b> of the <b><strong style=\"white-space: pre-wrap;\">Nginx Ingress Controller<\/strong><\/b>.<\/div>\n<\/div>\n<pre><code class=\"language-bash\">kubectl apply -f cluster-issuer.yaml<\/code><\/pre>\n<p>We have to ensure that the object is successfully created.<\/p>\n<pre><code class=\"language-bash\">$ kubectl get clusterissuers\n\nNAME              READY   AGE\nletsencrypt-dev   True    28s<\/code><\/pre>\n<h2 id=\"create-an-ingress-object-with-the-certificate-issuer\">Create an Ingress Object with the Certificate Issuer<\/h2>\n<p>Now, we need to create Ingress object with the issuer to create TLS certificates for the Ingress during the creation.<\/p>\n<pre><code class=\"language-bash\">cat &lt;&lt; EOF &gt; web-ingress.yaml\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: web-server-ingress\n  namespace: default\n  annotations:\n    nginx.ingress.kubernetes.io\/force-ssl-redirect: \"true\"\n    nginx.ingress.kubernetes.io\/backend-protocol: \"HTTP\"\n    nginx.ingress.kubernetes.io\/ssl-passthrough: \"false\"\n    cert-manager.io\/cluster-issuer: \"letsencrypt-dev\"\nspec:\n  ingressClassName: nginx\n  rules:\n    - host: nginx.devopsproject.dev\n      http:\n        paths:\n          - pathType: Prefix\n            backend:\n              service:\n                name: web-svc\n                port:\n                  number: 80\n            path: \/\n\n  tls:\n    - hosts:\n      - nginx.devopsproject.dev\n      secretName: nginx-devopsproject-tls\nEOF<\/code><\/pre>\n<p>To integrate the Cert Manager with the Ingress, we need to pass the following annotation.<\/p>\n<ul>\n<li><code>metadata.annotations.cert-manager.io\/cluster-issuer: letsencrypt-dev<\/code><\/li>\n<li>In the TLS section, add hostname and name for a secret.<\/li>\n<\/ul>\n<p>Cert Manager use this secret name to create TLS secret once it gets the certificate from the Let&#8217;s Encrypt Issuer.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f web-ingress.yaml<\/code><\/pre>\n<p>The TLS certificate will be generated only after the Ingress object <a href=\"https:\/\/devopscube.com\/kubernetes-deployment-tutorial\/\" rel=\"noreferrer noopener\">deployment<\/a>.<\/p>\n<p>Ensure that the Ingress Object is created and verify the status.<\/p>\n<pre><code>$ kubectl get ingress\n\nNAME                 CLASS   HOSTS                     ADDRESS                                                                         PORTS     AGE\nweb-server-ingress   nginx   nginx.devopsproject.dev   a090d3d63364645a08b739a596fd6095-b607c436b02c89d4.elb.us-west-2.amazonaws.com   80, 443   16h<\/code><\/pre>\n<p>Each time you create an Ingress object for an application, the TLS certificate will automatically be generated and attached to the resource.<\/p>\n<p>Now, we can check the Secret because the TLS certificate will be stored as a secret in the cluster.<\/p>\n<pre><code>$ kubectl get secret\n\nNAME                      TYPE                DATA   AGE\nnginx-devopsproject-tls   kubernetes.io\/tls   2      16h<\/code><\/pre>\n<p>If you want to know more details about the generated certificate, we can describe the <code>certificates<\/code> Custom Resource Definition.<\/p>\n<pre><code>$ kubectl describe certificate nginx-devopsproject-tls \n\nName:         nginx-devopsproject-tls\nNamespace:    default\nLabels:       &lt;none&gt;\nAnnotations:  &lt;none&gt;\nAPI Version:  cert-manager.io\/v1\nKind:         Certificate\nMetadata:\n  Creation Timestamp:  2025-11-05T12:08:52Z\n  Generation:          1\n  Owner References:\n    API Version:           networking.k8s.io\/v1\n    Block Owner Deletion:  true\n    Controller:            true\n    Kind:                  Ingress\n    Name:                  web-server-ingress\n    UID:                   8c7bb1a7-fca9-4593-9738-ea5786ab8669\n  Resource Version:        87098\n  UID:                     5e27ca7a-8cd6-4a7c-a123-fdeadfc9bb4b\nSpec:\n  Dns Names:\n    nginx.devopsproject.dev\n  Issuer Ref:\n    Group:      cert-manager.io\n    Kind:       ClusterIssuer\n    Name:       letsencrypt-dev\n  Secret Name:  nginx-devopsproject-tls\n  Usages:\n    digital signature\n    key encipherment\nStatus:\n  Conditions:\n    Last Transition Time:  2025-11-05T12:08:54Z\n    Message:               Certificate is up to date and has not expired\n    Observed Generation:   1\n    Reason:                Ready\n    Status:                True\n    Type:                  Ready\n  Not After:               2026-02-03T11:10:22Z\n  Not Before:              2025-11-05T11:10:23Z\n  Renewal Time:            2026-01-04T11:10:22Z\n  Revision:                1\nEvents:                    &lt;none&gt;<\/code><\/pre>\n<p>We can now check our application with the hostname and ensure the TLS Certificate is attached.<\/p>\n<h2 id=\"verify-the-tls-attachment\">Verify the TLS Attachment<\/h2>\n<p>To verify this, open any browser and paste the hostname as URL (e.g., <code>nginx.devopsproject.dev<\/code>)<\/p>\n<p>The TLS termination will happen in the Ingress Controller when the external traffic is reached.<\/p>\n<figure class=\"kg-card kg-video-card kg-width-regular\" data-kg-thumbnail=\"https:\/\/devopscube.com\/content\/media\/2025\/11\/testing-web-page_thumb.jpg\" data-kg-custom-thumbnail=\"\">\n<div class=\"kg-video-container\">\n                <video src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/media\/2025\/11\/testing-web-page.mp4\" poster=\"https:\/\/img.spacergif.org\/v1\/1920x1080\/0a\/spacer.png\" width=\"1920\" height=\"1080\" playsinline=\"\" preload=\"metadata\" style=\"background: transparent url('https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/media\/2025\/11\/testing-web-page_thumb.jpg') 50% 50% \/ cover no-repeat;\"><\/video><\/p>\n<div class=\"kg-video-overlay\">\n                    <button class=\"kg-video-large-play-icon\" aria-label=\"Play video\"><br \/>\n                        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\">\n                            <path d=\"M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z\"><\/path>\n                        <\/svg><br \/>\n                    <\/button>\n                <\/div>\n<div class=\"kg-video-player-container\">\n<div class=\"kg-video-player\">\n                        <button class=\"kg-video-play-icon\" aria-label=\"Play video\"><br \/>\n                            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\">\n                                <path d=\"M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z\"><\/path>\n                            <\/svg><br \/>\n                        <\/button><br \/>\n                        <button class=\"kg-video-pause-icon kg-video-hide\" aria-label=\"Pause video\"><br \/>\n                            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\">\n                                <rect x=\"3\" y=\"1\" width=\"7\" height=\"22\" rx=\"1.5\" ry=\"1.5\"><\/rect>\n                                <rect x=\"14\" y=\"1\" width=\"7\" height=\"22\" rx=\"1.5\" ry=\"1.5\"><\/rect>\n                            <\/svg><br \/>\n                        <\/button><br \/>\n                        <span class=\"kg-video-current-time\">0:00<\/span><\/p>\n<div class=\"kg-video-time\">\n                            \/<span class=\"kg-video-duration\">0:12<\/span>\n                        <\/div>\n<p>                        <input type=\"range\" class=\"kg-video-seek-slider\" max=\"100\" value=\"0\"><br \/>\n                        <button class=\"kg-video-playback-rate\" aria-label=\"Adjust playback speed\">1\u00d7<\/button><br \/>\n                        <button class=\"kg-video-unmute-icon\" aria-label=\"Unmute\"><br \/>\n                            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\">\n                                <path d=\"M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z\"><\/path>\n                            <\/svg><br \/>\n                        <\/button><br \/>\n                        <button class=\"kg-video-mute-icon kg-video-hide\" aria-label=\"Mute\"><br \/>\n                            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\">\n                                <path d=\"M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z\"><\/path>\n                            <\/svg><br \/>\n                        <\/button><br \/>\n                        <input type=\"range\" class=\"kg-video-volume-slider\" max=\"100\" value=\"100\">\n                    <\/div>\n<\/p><\/div>\n<\/p><\/div>\n<\/figure>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Cert Manager will track the certificates that have been created and renew them before their expiration.<\/p>\n<p>By default, the certification validity is 90 days and if you want change the renewal, you can modify the settings.<\/p>\n<p>Cert Manager also can create self signed certificates by itself without a help of an Isser. To know more, please refer to this <a href=\"https:\/\/cert-manager.io\/?ref=devopscube.com\" rel=\"noreferrer\">official documentation<\/a>.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/nginx-ingress-with-cert-manager\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to set up Nginx Ingress with Cert Manager in EKS \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/nginx-ingress-with-cert-manager\/<\/p>\n","protected":false},"author":1,"featured_media":443,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-442","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/442","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=442"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/442\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/443"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}