How to Set up Nginx Ingress Controller with SSL on VKE

Updated on July 25, 2024
How to Set up Nginx Ingress Controller with SSL on VKE header image

Introduction

Nginx Ingress Controller is a popular Kubernetes Ingress controller that uses Nginx as a reverse proxy and load balancer to securely route external traffic to services in a cluster. It works as the single access point for underlying services while offering SSL/TLS termination, load balancing, session handling, and path-based routing for services in the Kubernetes cluster.

In this article, you get to set up Nginx Ingress Controller with SSL in a Vultr Kubernetes Engine (VKE) cluster. Using the controller, you will issue Let’s Encrypt certificates with Cert Manager and import commercial SSL certificates for your existing domain name to secure underlying services with HTTPS.

The example services, hello-world, ding-world used in this article represent your Kubernetes services, and the example IP Address 192.0.2.1 represents your cluster's external IP. Replace all occurrences with your actual Kubernetes cluster details.

Prerequisites

Before you begin, make sure you:

Install the Nginx Ingress Controller

  1. Add the Nginx Ingress repository.

     $ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
  2. Update Helm.

     $ helm repo update
  3. Install the Nginx Ingress Controller.

     $ helm install ingress-nginx ingress-nginx/ingress-nginx
  4. After installation, a Vultr Load Balancer is automatically added to your cluster. Verify that the assigned external IP Address matches your load balancer IP in the VKE dashboard.

     $ kubectl get services ingress-nginx-controller

    Your output should look like the one below:

     NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                      AGE
     ingress-nginx-controller   LoadBalancer   10.108.201.180   192.0.2.1   80:31125/TCP,443:31287/TCP   39h

Install CertManager

  1. Install the latest CertManager version.

     $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.1/cert-manager.yaml

    Visit the official CertManager releases page to get the latest version.

  2. Inspect the CertManager Kubernetes resources.

     $ kubectl get all -n cert-manager

    Your output should look like the one below:

     NAME                                           READY   STATUS    RESTARTS   AGE
     pod/cert-manager-5f68c9c6dd-stmp6              1/1     Running   0          35h
     pod/cert-manager-cainjector-57d6fc9f7d-gwqr5   1/1     Running   0          35h
     pod/cert-manager-webhook-5b7ffbdc98-sq8kg      1/1     Running   0          35h
    
     NAME                           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
     service/cert-manager           ClusterIP   10.102.38.47   <none>        9402/TCP   35h
     service/cert-manager-webhook   ClusterIP   10.97.255.91   <none>        443/TCP    35h
    
     NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
     deployment.apps/cert-manager              1/1     1            1           35h
     deployment.apps/cert-manager-cainjector   1/1     1            1           35h
     deployment.apps/cert-manager-webhook      1/1     1            1           35h
    
     NAME                                                 DESIRED   CURRENT   READY   AGE
     replicaset.apps/cert-manager-5f68c9c6dd              1         1         1       35h
     replicaset.apps/cert-manager-cainjector-57d6fc9f7d   1         1         1       35h
     replicaset.apps/cert-manager-webhook-5b7ffbdc98      1         1         1       35h

Deploy Backend Applications

In this section, deploy example applications to test your Nginx Ingress controller. For purposes of this article, deploy two example applications, hello-world and ding-world using the NginxDemos image that outputs a simple webpage with the server hostname, IP, and port.

  1. Using a text editor such as Nano, create a new file named hello-world-deploy.yaml with the following configurations.

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: hello-world
     spec:
      replicas: 2
     selector:
      matchLabels:
        app: hello-world
     template:
      metadata:
        labels:
          app: hello-world
      spec:
        containers:
        - name: hello-world
          image: nginxdemos/hello
          ports:
          - containerPort: 80

    Save and close the file.

  2. Create the second manifest file named ding-world-deploy.yaml with the following configurations.

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: ding-world
     spec:
      replicas: 2
     selector:
      matchLabels:
        app: ding-world
     template:
      metadata:
        labels:
          app: ding-world
      spec:
        containers:
        - name: ding-world
          image: nginxdemos/hello
          ports:
          - containerPort: 80

    Save and close the file.

  3. Apply the hello-world deployment.

     $ kubectl apply -f hello-world-deploy.yaml
  4. Apply the ding-world deployment.

     $ kubectl apply -f ding-world-deploy.yaml
  5. Verify that your deployments are successful.

     $ kubectl get deployments

    Your output should look like the one below.

     NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
     ding-world                 2/2     2            2           36h
     hello-world                2/2     2            2           36h
     ingress-nginx-controller   1/1     1            1           39h
  6. Create a new service-hello-world.yaml service manifest and add the following configurations to the file.

     apiVersion: v1
     kind: Service
     metadata:
      name: hello-world
     spec:
       ports:
         - name: http
           port: 80
           targetPort: 8080
       selector:
         app:hello-world

    Save and close the file.

  7. Create the service-ding-world.yaml file with the following contents.

     apiVersion: v1
     kind: Service
     metadata:
      name: ding-world
     spec:
       ports:
         - name: http
           port: 80
           targetPort: 8080
       selector:
         app:ding-world

    Save and close the file.

  8. Deploy the hello-world service.

     $ kubectl apply -f service-hello-world.yaml
  9. Deploy the ding-world service.

     $ kubectl apply -f ding-hello-world.yaml
  10. Verify that all services are running.

    $ kubectl get services

    Your output should look like the one below.

    NAME    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
    echo    ClusterIP   10.106.22.216   <none>        80/TCP    1m
    quote   ClusterIP   10.100.202.158    <none>        80/TCP    1m

Setup DNS Records

  1. Login to your DNS Provider account. For example, Vultr DNS.

  2. Access your domain.

  3. Set up a new domain A subdomain record with the value hello-world that points to your LoadBalancer’s external IP Address.

  4. Set up another A subdomain record with the value ding-world that points to the same IP Address.

    Setup a Domain A Record on Vultr

  5. In a web browser, visit your new subdomains and verify that they propagate to your Ingress Controller which returns a 404 Unknown error.

     hello-world.example.com

Configure the Nginx Ingress Controller to Expose backend Applications

When the Nginx Ingress controller is successfully installed, services running, and the DNS records correctly pointing to the Loadbalancer’s external IP. You need to create Ingress resources to define how external traffic routes to services in the cluster.

In this section, create Ingress resources for both applications in the cluster and test that each host maps to the correct service as described below.

  1. Create a new Ingress resource named hello-world-host.yaml with the following contents.

     apiVersion: networking.k8s.io/v1
     kind: Ingress
     metadata:
      name: ingress-hello-world
      annotations:
       cert-manager.io/issuer: letsencrypt-nginx
     spec:
       ingressClassName: nginx
       rules:
       - host: hello-world.example.com
         http:
           paths:
           - pathType: Prefix
           path: "/"
           backend:
             service:
               name: hello-world
               port:
                 number: 80
  2. Create the ding-world-host.yaml manifest with the following contents.

     apiVersion: networking.k8s.io/v1
     kind: Ingress
     metadata:
      name: ingress-ding-world
      annotations:
       cert-manager.io/issuer: letsencrypt-nginx
     spec:
       ingressClassName: nginx
       rules:
       - host: ding-world.example.com
         http:
           paths:
           - pathType: Prefix
           path: "/"
           backend:
             service:
               name: ding-world
               port:
                 number: 80
  3. Deploy the host manifests using the following command.

     $ kubectl apply -f hello-world-host.yaml
     $ kubectl apply -f ding-world-host.yaml
  4. Verify that the Ingress resources are available.

     $ kubectl get ingress

    Your output should look like the one below.

     NAME            CLASS   HOSTS                        ADDRESS          PORTS   AGE
     ingress-hello-world    nginx   hello-world.example.com   192.0.2.1  80      10m1s
     ingress-ding-world   nginx   ding-world.example.com   192.0.2.1  80      10m6s

    In the above output. Verify that the Address column matches the Nginx load balancer IP Address you created earlier.

Test

  1. Using a web browser such as Google Chrome, visit each of your configured domains as below.

     http://hello-world.example.com
     http://ding-world.example.com

    Both domains should load correctly with a different Server address and name returned by each of the applications.

    Example hello-world Kubernetes application

Setup Nginx Ingress Controller to use Production-Ready SSL Certificates

You can set up the Nginx Ingress controller to use trusted production-ready SSL certificates for your cluster services using an Issuer resource for CertManager. By default, CertManager creates Custom Resource Definitions (CRDs) to handle the certificate issuance process from a Certificate Authority (CA) such as Let’s Encrypt, and these include the following.

  • Issuer: Defines an issuer configuration such as the type of issuer like ACME, method of certificate issuance like DNS01 or HTTP01, and the necessary credentials in a single namespace.
  • Cluster Issuer: Functions like Issuer but can issue certificates to any namespace within the Kubernetes cluster. It’s important when using the same issuer configuration across multiple namespaces.
  • Certificate: Defines a namespaced resource with the desired properties of a TLS/SSL certificate such as the domain names, associated secrets to store the certificate, and the trusted CA issuer to use. The Certificate must reference an Issuer or Cluster Issuer resource.

In this section, set up the Nginx Ingress Controller to use trusted SSL certificates issued by CertManager.

  1. First, inspect the available CRDs by running the following command.

     $ kubectl get crd -l app.kubernetes.io/name=cert-manager

    Your output should look like the one below.

     NAME                                  CREATED AT
     certificaterequests.cert-manager.io   2023-06-12T23:01:59Z
     certificates.cert-manager.io          2023-06-12T23:02:00Z
     challenges.acme.cert-manager.io       2023-06-12T23:02:01Z
     clusterissuers.cert-manager.io        2023-06-12T23:02:03Z
     issuers.cert-manager.io               2023-06-12T23:02:04Z
     orders.acme.cert-manager.io           2023-06-12T23:02:05Z

    As displayed in the above output, the Issuer, ClusterIssuer, and Certificate CRDs must be available.

Setup Let’s Encrypt Certificates

  1. Create a new issuer manifest. For example cert-issuer.yaml.

     $ nano cert-issuer.yaml
  2. Add the following configuration to the file.

     apiVersion: cert-manager.io/v1
     kind: Issuer
     metadata:
      name: letsencrypt-nginx
     spec:
       acme:
         email: hello@example.com
         server: https://acme-v02.api.letsencrypt.org/directory
         privateKeySecretRef:
           name: letsencrypt-nginx-prod
         solvers:
          - http01:
             ingress:
               class: nginx

    The above configuration uses the ACME issuer with the following fields:

    • email: Active email address to associate with the ACME account.
    • server:: URL to access the ACME Let’s Encrypt server endpoint.
    • privateKeySecretRef: name: Kubernetes secret to store the generated ACME account private key.
    • solvers: Defines the certificate issuer challenge. You can either use the http01 challenge or the dns01 challenge. It’s recommended to use the http01 challenge unless issuing wildcard certificates.

    Enter your email address to replace the pre-filled value, then save and close the file.

  3. Apply the issuer resource to your cluster.

     $ kubectl apply -f cert-issuer.yaml
  4. Verify that the issuer resource is available and ready to use.

     $ kubectl get issuer

    Your output should look like the one below.

     NAME                      READY   AGE
     letsencrypt-nginx         True    2m
  5. To configure the Ingress controller to map hosts with TLS. Edit each of the host manifests you created earlier, update the annotations section, and add a new tls section as described below.

  6. Check the state of your Ingress resources.

     $ kubectl get ingress
  7. Edit the hello-world-host.yaml file you created earlier.

     $ nano hello-world-host.yaml
  8. Update the file with the following configurations.

     apiVersion: networking.k8s.io/v1
     kind: Ingress
     metadata:
       name: ingress-hello-world
       namespace: cert-manager
     annotations:
       cert-manager.io/issuer: letsencrypt-nginx
     spec:
       tls:
       - hosts:
        - hello-world.example.com
        secretName: letsencrypt-nginx-hello
     rules:
       - host: hello-world.example.com
         http:
           paths:
             - path: /
               pathType: Prefix
               backend:
                service:
                 name: echo
                 port:
                   number: 80
     ingressClassName: nginx

    Save and close the file.

  9. Edit the ding-world-host.yaml file.

     $ nano hello-world-host.yaml
  10. Update the file with the following configurations.

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: ingress-ding-world
      namespace: cert-manager
    annotations:
      cert-manager.io/issuer: letsencrypt-nginx
    spec:
      tls:
      - hosts:
       - ding-world.example.com
       secretName: letsencrypt-nginx-ding
    rules:
      - host: ding-world.example.com
        http:
          paths:
            - path: /
              pathType: Prefix
              backend:
               service:
                name: echo
                port:
                  number: 80
    ingressClassName: nginx

    Save and close the file.

    To avoid any Kubernetes manifest styling errors. Delete the existing files and recreate them with the above configurations.

  11. Apply the configurations to enable TLS on each of the hosts.

    $ kubectl apply -f hello-world-host.yaml
    $ kubectl apply -f ding-world-host.yaml
  12. Verify that your Ingress resources now have the TLS port 443 activated under the PORTS column.

    $ kubectl get ingress

    Output:

    NAME             CLASS    HOSTS                 ADDRESS         PORTS     AGE
    ingress-ding     nginx    ding-world.example.com   192.0.2.1   80,443   10m
    ingress-hello    nginx    hello-world.example.com   192.0.2.1   80,443   10m
  13. Verify that the certificate resources are available.

    $ kubectl get certificates

    Your output should look like the one below.

    NAME                     READY   SECRET                   AGE
    letsencrypt-nginx-hello        True   letsencrypt-nginx-hello      5m
    letsencrypt-nginx-ding  True    letsencrypt-nginx-ding   5m

    If the READY column returns True, your Let’s Encrypt certificates are successfully propagated. If False, please check your Ingress resource configuration and reapply changes to request for a new certificate.

Wildcard Certificates

To set up the Nginx Ingress controller to use Wildcard certificates, you need to create an Issuer or ClusterIssuer resource that uses the DNS-01 challenge. Depending on your DNS provider, you need to enter your account API key for CertManager to create necessary DNS TXT records required to pass the challenge.

In this section, you'll use Vultr DNS with your Vultr API key to create the necessary DNS records during the DNS-01 challenge. If your domain is with a different DNS provider such as Cloudflare, please use your provider API key to pass the challenge as described below.

Since Wildcard certificates cover your full domain *.example.com together with any subdomains. It’s important to cover all namespaces using a Cluster Issuer with a separate subdomain for every service.

  1. Create a new Kubernetes secret to store your Vultr API credentials.

     $ nano vultr-secret.yaml
  2. Add the following configurations to the file.

     apiVersion: v1
     kind: Secret
     metadata:
      name: vultr-dns
      namespace: cert-manager
     type: Opaque
     stringData:
       apiKey: <your-vultr-api-key>

    Save and close the file.

  3. Apply the secret.

     $ kubectl apply -f vultr-secret.yaml
  4. Create a Cluster Issuer manifest, for example cluster-issuer.yaml.

     $ nano cluster-issuer.yaml
  5. Add the following configurations to the file.

     apiVersion: cert-manager.io/v1
     kind: ClusterIssuer
     metadata:
      name: letsencrypt-prod-wcard
     spec:
      acme:
      email: hello@example.com
      server: https://acme-v02.api.letsencrypt.org/directory
      privateKeySecretRef:
        name: letsencrypt-prod-wcard-private
        solvers:
          - dns01:
              vultr:
                tokenSecretRef:
                  name: vultr-dns
          key: api-key

    Enter your email address, then save and close the file.

  6. Create the Cluster Issuer.

     $ kubectl apply -f cluster-issuer.yaml
  7. Verify that the Cluster Issuer is successfully created.

     $ kubectl get issuer
  8. Create a certificate resource that references the Cluster Issuer using the following command.

     $ nano wcard-certificate.yaml
  9. Add the following configurations to the file.

     apiVersion: cert-manager.io/v1
     kind: Certificate
     metadata:
      name: example-wcard
     spec:
      secretName: example-wcard
      issuerRef:
        name: letsencrypt-prod-wcard
        kind: ClusterIssuer
        group: cert-manager.io
      commonName: "*.example.com"
      dnsNames:
        - "example.com"
        - "*.example.com"

    The certificate manifest contains the following important fields:

    • secretName: Defines the secret name under which Cert-Manager stored the certificate and private key.
    • Issuer Ref: name: The Issuer resource to use for certificate issuance.
    • commonName: The Common Name (CN) to use on the SSL certificate.
    • dnsNames: List of DNS SANs (Subject Alternative Names) to include on the SSL certificate.
  10. Apply the certificate resource.

    $ kubectl apply -f wcard-certificate.yaml
  11. Check the certificate status.

    $ kubectl get certificate example.com

    Output:

    NAME                    READY   SECRET                  AGE
    example.com             True    example.com             8m18s

    As displayed in the above output, the READY column should return True if the certificate is successfully issued. If False, check your certificate configurations, and verify that you used the correct DNS configurations in your ClusterIssuer resource.

  12. Check the Kubernetes secret that contains your TLS certificate.

    $ kubectl describe secret example-wcard
  13. To configure Nginx to use the Wildcard certificates, create an Ingress manifest that combines multiple hosts with different paths, but the same SSL certificate.

    $ nano wcard-host-ingress.yaml
  14. Add the following configurations to the file.

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
     name: ingress-wcard-apps
    spec:
    tls:
      - hosts:
       - "*.example.com"
      secretName: example-wcard
    rules:
      - host: example.com
        http:
         paths:
          - path: /
            pathType: Prefix
            backend:
            service:
             name: ding-world
             port:
              number: 80
     - host: hello-world.example.com
       http:
        paths:
         - path: /
           pathType: Prefix
           backend:
             service:
               name: hello-world
               port:
                 number: 80
    ingressClassName: nginx

    Verify that the secretName: field references the Kubernetes secret containing your SSl certificate. Then, save and close the file.

  15. Verify that the Ingress resource is available.

    $ kubectl get ingress

    Your output should look like the one below.

    NAME              CLASS  HOSTS                                                   ADDRESS          PORTS     AGE
    ingress-wcard-apps nginx  hello-world.example.com,ding-world.example.com          192.0.2.1          80, 443   59s

    As displayed in the above output, verify that the hosts are TLS terminated on port 443.

You have configured Nginx Ingress Controller to use Wildcard SSL certificates, for more information on using Vultr DNS, visit the CertManager Webhook page for Vultr.

Import Commercial SSL Certificates

To import commercial SSL certificates purchased from a trusted certificate authority (CA), convert the certificate and private key to the base64 format, add them to a Kubernetes secret, then configure your Ingress hosts to use the certificates.

  1. Convert the commercial SSL certificate and private key to base64. Copy the resultant values to your clipboard.

     $ base64 -w 0 /path/ssl-certificate.pem
     $ base64 -w 0 /path/cert-private-key.pem

    Replace /path with the actual directory path to your commercial SSL certificate and private key.

  2. Create a new Kubernetes secrets manifest.

     $ nano ssl-secret.yaml
  3. Add the following configurations to the file.

     apiVersion: v1
     kind: Secret
     metadata:
      name: prod-ssl-secret
     type: kubernetes.io/tls
     data:
       tls.crt: <paste-base64-values>
       tls.key: <paste-base64-values>

    Paste your base64 encoded values to the respective fields as follows:

    • tls-crt: SSL certificate in base64
    • tls.key: Certificate private key in base64

    Save and close the file.

  4. To configure Nginx to use commercial SSL certificates. Create a new Ingress manifest.

     $ nano ingress-ssl.yaml
  5. Add the following configurations to the file.

     apiVersion: networking.k8s.io/v1
     kind: Ingress
     metadata:
      name: ingress-prod
     spec:
      tls:
        - hosts:
        - example.com
     secretName: prod-ssl-secret
     rules:
       - host: example.com
         http:
           paths:
             - path: /
               pathType: Prefix
               backend:
                 service:
                 name: hello-world
                 port:
                   number: 80

    Verify that the secretName: matches your Kubernetes secret. Then, save and close the file.

You have set up the Nginx Ingress Controller to use your commercial SSL certificates.

Test the SSL Configuration

Depending on your SSL certificate deployment method, verify that you can securely access your services over HTTPS using a web browser of your choice.

    https://hello-world.example.com
    https://ding-world.example.com

If you try to access your hosts over HTTP, the Ingress controller automatically redirects your request to HTTPS.

Troubleshooting

  1. Nginx 502 Gateway Error:

    Verify that your Ingress configuration is correct and the referenced services are available and running.

  2. Unable to generate Let’s Encrypt Certificates, READY column remains False:

    Verify that your Issuer, and Certificate configurations are correct. Or, check the CertManager Logs for further troubleshooting.

  3. Error from server (InternalError): error when creating "ingress-myapp.yaml": Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io":

    To fix the error, run the following command, then reapply your configuration.

     $ kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission

Conclusion

In this article, you have installed and set up Nginx Ingress Controller with SSL on a Vultr Kubernetes Engine (VKE) cluster. You can use the controller to correctly route external requests to cluster services securely over HTTPS. For more information about your cluster, visit the VKE reference page.

Next Steps

With the Nginx Ingress Controller correctly routing requests to your cluster services, you can set up multiple applications and services depending on your needs. For more information, visit the following resources.