DEV Community

Cover image for Setting Up Nginx Ingress Controller with SSL on Kubernetes
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya Originally published at docs.vultr.com

Setting Up Nginx Ingress Controller with SSL on Kubernetes

The Nginx Ingress Controller is a popular Kubernetes Ingress controller that uses Nginx as a reverse proxy and load balancer to route external traffic into a cluster, acting as a single entry point with SSL/TLS termination, load balancing, session handling, and path-based routing. This guide sets up the Nginx Ingress Controller on a Kubernetes cluster, deploys two sample applications behind it, and issues Let's Encrypt certificates with cert-manager to secure them, plus covers importing commercial SSL certificates instead. By the end, you'll have two applications reachable over HTTPS through a shared Ingress controller.

Before you begin, you'll need a Kubernetes cluster with at least 2 nodes, kubectl installed and configured to access it, the Helm package manager installed on your computer, and a domain name (for example, example.com).


1. Install the Nginx Ingress Controller

1. Add the Nginx Ingress Helm repository:

$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
Enter fullscreen mode Exit fullscreen mode

2. Update Helm:

$ helm repo update
Enter fullscreen mode Exit fullscreen mode

3. Install the Nginx Ingress Controller:

$ helm install ingress-nginx ingress-nginx/ingress-nginx
Enter fullscreen mode Exit fullscreen mode

4. Check the load balancer that's automatically provisioned:

$ kubectl get services ingress-nginx-controller
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller   LoadBalancer   10.101.22.249   <pending>     80:31915/TCP,443:30217/TCP   106s
Enter fullscreen mode Exit fullscreen mode

It may take a few minutes for the service to get an EXTERNAL-IP, depending on your cloud provider. Some cloud providers also require a provider-specific annotation on the LoadBalancer service to configure things like health checks — check your provider's Kubernetes documentation if the external IP doesn't provision as expected.

2. Install cert-manager

1. Install the latest cert-manager release:

$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml
Enter fullscreen mode Exit fullscreen mode

Check the official cert-manager releases page for the latest version.

2. Inspect the cert-manager resources:

$ kubectl get all -n cert-manager
Enter fullscreen mode Exit fullscreen mode

You should see pods, services, replicasets, and deployments related to cert-manager.

3. Deploy Backend Applications

Deploy two sample applications, app1 and app2, using the http-echo image, which returns its command-line argument on an HTML page.

1. Create a manifest for the app1 Deployment:

$ nano app1-deploy.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
      - name: app1
        image: hashicorp/http-echo
        args: ["-text=Hello from App1"]
        ports:
        - containerPort: 5678
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

2. Create a manifest for the app2 Deployment:

$ sudo nano app2-deploy.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app2
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
      - name: app2
        image: hashicorp/http-echo
        args: ["-text=Hello from App2"]
        ports:
        - containerPort: 5678
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

3. Apply the app1 deployment:

$ kubectl apply -f app1-deploy.yaml
Enter fullscreen mode Exit fullscreen mode

4. Apply the app2 deployment:

$ kubectl apply -f app2-deploy.yaml
Enter fullscreen mode Exit fullscreen mode

5. Verify both deployments succeeded:

$ kubectl get deployments
Enter fullscreen mode Exit fullscreen mode

You should see app1 and app2 listed.

6. Create a service manifest for app1:

$ nano app1-svc.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: v1
kind: Service
metadata:
  name: app1-svc
spec:
  ports:
    - name: http
      port: 80
      targetPort: 8080
  selector:
    app: app1
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

7. Create a service manifest for app2:

$ nano app2-svc.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: v1
kind: Service
metadata:
  name: app2-svc
spec:
  ports:
    - name: http
      port: 80
      targetPort: 8080
  selector:
    app: app2
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

8. Deploy the app1-svc service:

$ kubectl apply -f app1-svc.yaml
Enter fullscreen mode Exit fullscreen mode

9. Deploy the app2-svc service:

$ kubectl apply -f app2-svc.yaml
Enter fullscreen mode Exit fullscreen mode

10. Verify both services are running:

$ kubectl get services
Enter fullscreen mode Exit fullscreen mode

You should see app1-svc and app2-svc listed.

4. Set Up DNS Records

1. Log in to your DNS provider's account and access your domain.

2. Create a new A subdomain record app1 pointing to your load balancer's external IP address.

3. Create another A subdomain record app2 pointing to the same IP address.

5. Configure the Nginx Ingress Controller to Expose the Backend Applications

1. Create a manifest for an Ingress resource for the app1 Deployment:

$ sudo nano app1-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-app1
  annotations:
    cert-manager.io/issuer: letsencrypt-nginx
spec:
  ingressClassName: nginx
  rules:
  - host: app1.example.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: app1-svc
            port:
              number: 80
Enter fullscreen mode Exit fullscreen mode

Replace app1.example.com with your domain name. Save and close the file.

2. Create a manifest for an Ingress resource for the app2 Deployment:

$ sudo nano app2-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-app2
  annotations:
    cert-manager.io/issuer: letsencrypt-nginx
spec:
  ingressClassName: nginx
  rules:
  - host: app2.example.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: app2-svc
            port:
              number: 80
Enter fullscreen mode Exit fullscreen mode

Replace app2.example.com with your domain name. Save and close the file.

3. Apply the ingress-app1 resource:

$ kubectl apply -f app1-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

4. Apply the ingress-app2 resource:

$ kubectl apply -f app2-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

5. Verify the Ingress resources are available:

$ kubectl get ingress
Enter fullscreen mode Exit fullscreen mode

You should see ingress-app1 and ingress-app2 listed. Wait for the ADDRESS column to populate with your load balancer's IP address.

6. Set Up Production-Ready SSL Certificates

cert-manager creates Custom Resource Definitions (CRDs) to handle certificate issuance from a CA such as Let's Encrypt:

  • Issuer: Defines an issuer configuration (e.g., ACME), the challenge method (DNS01 or HTTP01), and credentials, scoped to a single namespace.
  • Cluster Issuer: Works like Issuer but can issue certificates to any namespace in the cluster.
  • Certificate: A namespaced resource describing the desired TLS certificate — domain names, the secret to store it in, and the Issuer/ClusterIssuer to use.

1. Inspect the available CRDs:

$ kubectl get crd -l app.kubernetes.io/name=cert-manager
Enter fullscreen mode Exit fullscreen mode

You should see resources such as issuers.cert-manager.io, certificates.cert-manager.io, and clusterissuers.cert-manager.io.

Set Up Let's Encrypt Certificates

1. Create an issuer manifest:

$ nano cert-issuer.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

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
Enter fullscreen mode Exit fullscreen mode

Replace the email with a real, active address — @example.com addresses will keep the Issuer from becoming active. Save and close the file.

2. Apply the Issuer resource:

$ kubectl apply -f cert-issuer.yaml
Enter fullscreen mode Exit fullscreen mode

3. Verify the Issuer is ready:

$ kubectl get issuer
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                READY   AGE
letsencrypt-nginx   False   4s
Enter fullscreen mode Exit fullscreen mode

The READY status changes to True after some time.

4. Check the state of your Ingress resources:

$ kubectl get ingress
Enter fullscreen mode Exit fullscreen mode

5. Edit app1-ingress.yaml:

$ nano app1-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

Add a spec.tls section:

  tls:
  - hosts:
    - app1.example.com
    secretName: letsencrypt-nginx-app1
Enter fullscreen mode Exit fullscreen mode

The full manifest should look like:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-app1
  annotations:
    cert-manager.io/issuer: letsencrypt-nginx
spec:
  ingressClassName: nginx
  rules:
  - host: app1.example.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: app1-svc
            port:
              number: 80
  tls:
  - hosts:
    - app1.example.com
    secretName: letsencrypt-nginx-app1
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

6. Add the equivalent tls block to app2-ingress.yaml:

  tls:
  - hosts:
    - app1.example.com
    secretName: letsencrypt-nginx-app2
Enter fullscreen mode Exit fullscreen mode

7. Apply both configurations to enable TLS:

$ kubectl apply -f app1-ingress.yaml
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f app2-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

8. Verify TLS port 443 is active in the PORTS column:

$ kubectl get ingress
Enter fullscreen mode Exit fullscreen mode

Output:

NAME             CLASS    HOSTS               ADDRESS     PORTS    AGE
app1-ingress     nginx    app1.example.com    192.0.2.1   80,443   10m
app2-ingress     nginx    app2.example.com    192.0.2.1   80,443   10m
Enter fullscreen mode Exit fullscreen mode

9. Verify the certificate resources:

$ kubectl get certificates
Enter fullscreen mode Exit fullscreen mode

Output:

NAME              READY   SECRET                   AGE
letsencrypt-app1  True    letsencrypt-nginx-app1   5m
letsencrypt-app2  True    letsencrypt-nginx-app2   5m
Enter fullscreen mode Exit fullscreen mode

If READY is True, the certificates propagated successfully. If False, check your Ingress configuration and reapply.

Test the SSL Configuration

Verify you can reach both services securely over HTTPS:

https://app1.example.com
Enter fullscreen mode Exit fullscreen mode
https://app2.example.com
Enter fullscreen mode Exit fullscreen mode

HTTP requests to either host are automatically redirected to HTTPS.

Import Commercial SSL Certificates

To use a commercial SSL certificate from a trusted CA, convert the certificate and private key to base64, store them in a Kubernetes secret, and reference that secret from your Ingress resources.

1. Convert the certificate and private key to base64:

$ base64 -w 0 /path/ssl-certificate.pem
Enter fullscreen mode Exit fullscreen mode
$ base64 -w 0 /path/cert-private-key.pem
Enter fullscreen mode Exit fullscreen mode

Replace /path with the actual path to your certificate and private key. Copy both resulting values.

2. Create a Kubernetes secret manifest:

$ nano ssl-secret.yaml
Enter fullscreen mode Exit fullscreen mode

Add the following:

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

Paste your base64-encoded certificate into tls.crt and the private key into tls.key. Save and close the file.

3. Reference the prod-ssl-secret secret name in your Ingress resource's spec.tls.secretName field to use the commercial certificate.

7. Troubleshooting

  • Nginx 502 Gateway Error: Verify your Ingress configuration is correct and the referenced services are running.
  • Let's Encrypt certificate stuck with READY = False:
    • Verify your Issuer and Certificate configurations are correct.
    • Confirm you provided a valid email address.
    • Check the Issuer resource logs for more detail.

Next Steps

  • Add rate limiting and WAF rules at the Ingress layer for public-facing services.
  • Set up a ClusterIssuer instead of a per-namespace Issuer if you'll issue certificates across multiple namespaces.
  • Configure automatic certificate renewal monitoring and alerting.
  • Layer in canary or blue-green routing using Ingress annotations or a service mesh.

For the full guide with additional tips, visit the original article on Vultr Docs.

Top comments (0)