Set up NGINX Ingress Controller with automated SSL certificate management using cert-manager for secure routing of external traffic to your Kubernetes services. Configure custom domains, SSL automation, and advanced routing rules for production workloads.
Prerequisites
- Running Kubernetes cluster with kubectl access
- Cluster admin permissions
- Helm 3 installed
- Domain name with DNS management access
What this solves
Kubernetes services run inside your cluster but need a way to receive external traffic. An ingress controller acts as the entry point, routing HTTP and HTTPS requests to the correct services based on domain names and paths. This tutorial sets up NGINX Ingress Controller with cert-manager to automatically provision and renew SSL certificates from Let's Encrypt, giving you secure HTTPS endpoints without manual certificate management.
Prerequisites and cluster setup
You need a running Kubernetes cluster with kubectl access and cluster-admin permissions. Your cluster should have at least 2 CPU cores and 4GB RAM available. External traffic must be able to reach your cluster nodes on ports 80 and 443.
Verify cluster access
Confirm your kubectl configuration and cluster connectivity.
kubectl cluster-info
kubectl get nodes
kubectl get pods --all-namespacesCheck cluster resources
Verify you have sufficient resources for the ingress controller components.
kubectl top nodes
kubectl describe nodes | grep -A 5 "Allocated resources"Install NGINX Ingress Controller
Add the NGINX Ingress Helm repository
We'll use Helm to install the ingress controller for easier configuration management.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo updateCreate ingress namespace
Organize ingress components in a dedicated namespace.
kubectl create namespace ingress-nginxInstall NGINX Ingress Controller
Deploy the controller with cloud provider integration for external load balancer provisioning.
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \
--set controller.service.type=LoadBalancer \
--set controller.service.externalTrafficPolicy=LocalVerify ingress controller deployment
Check that the controller pods are running and the LoadBalancer service has an external IP.
kubectl get pods -n ingress-nginx
kubectl get services -n ingress-nginx
kubectl logs -n ingress-nginx deployment/ingress-nginx-controllercontroller.service.type to NodePort and expose ports 80 and 443 through your firewall or external load balancer.Install and configure cert-manager
Add cert-manager Helm repository
Add the Jetstack repository that maintains cert-manager.
helm repo add jetstack https://charts.jetstack.io
helm repo updateInstall cert-manager CRDs
Install the Custom Resource Definitions required by cert-manager.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.crds.yamlCreate cert-manager namespace
Create a dedicated namespace for certificate management components.
kubectl create namespace cert-managerDeploy cert-manager
Install cert-manager with webhook validation and resource monitoring enabled.
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.13.3 \
--set installCRDs=false \
--set webhook.timeoutSeconds=4Verify cert-manager installation
Confirm all cert-manager components are running correctly.
kubectl get pods -n cert-manager
kubectl get apiservices | grep cert-manager
kubectl logs -n cert-manager deployment/cert-managerConfigure Let's Encrypt certificate issuer
Create staging certificate issuer
Start with Let's Encrypt staging environment to test certificate issuance without rate limits.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginxkubectl apply -f /tmp/letsencrypt-staging.yamlCreate production certificate issuer
Configure the production Let's Encrypt issuer for valid SSL certificates.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginxkubectl apply -f /tmp/letsencrypt-prod.yamlVerify certificate issuers
Check that both issuers are registered and ready.
kubectl get clusterissuers
kubectl describe clusterissuer letsencrypt-staging
kubectl describe clusterissuer letsencrypt-prodadmin@example.com with your actual email address. Let's Encrypt uses this for certificate expiration notices and account recovery.Configure ingress resources with SSL certificates
Deploy a sample application
Create a simple web application to test ingress routing and SSL termination.
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: app
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
configMap:
name: sample-app-html
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sample-app-html
namespace: default
data:
index.html: |
Sample App
Hello from Kubernetes!
This application is served through NGINX Ingress with SSL.
---
apiVersion: v1
kind: Service
metadata:
name: sample-app-service
namespace: default
spec:
selector:
app: sample-app
ports:
- port: 80
targetPort: 80
type: ClusterIPkubectl apply -f /tmp/sample-app.yamlCreate ingress with SSL certificate
Configure an ingress resource that automatically requests and uses SSL certificates.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sample-app-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
tls:
- hosts:
- app.example.com
secretName: sample-app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sample-app-service
port:
number: 80kubectl apply -f /tmp/sample-ingress.yamlMonitor certificate provisioning
Watch cert-manager automatically request and install the SSL certificate.
kubectl get certificates
kubectl get certificaterequests
kubectl describe certificate sample-app-tls
kubectl logs -n cert-manager deployment/cert-manager -fTest SSL certificate
Verify the certificate was issued and is accessible through HTTPS.
kubectl get secrets sample-app-tls -o yaml
kubectl get ingress sample-app-ingress
curl -k https://app.example.comAdvanced ingress configuration and routing
Configure path-based routing
Route different URL paths to different services within the same domain.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-path-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
tls:
- hosts:
- api.example.com
secretName: api-example-tls
rules:
- host: api.example.com
http:
paths:
- path: /v1(/|$)(.*)
pathType: Prefix
backend:
service:
name: api-v1-service
port:
number: 8080
- path: /v2(/|$)(.*)
pathType: Prefix
backend:
service:
name: api-v2-service
port:
number: 8080
- path: /health
pathType: Exact
backend:
service:
name: health-service
port:
number: 80kubectl apply -f /tmp/multi-path-ingress.yamlConfigure rate limiting
Implement rate limiting to protect your services from traffic spikes.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rate-limited-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit-requests: "10"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
nginx.ingress.kubernetes.io/rate-limit-connections: "5"
nginx.ingress.kubernetes.io/limit-whitelist: "203.0.113.0/24"
spec:
tls:
- hosts:
- secure.example.com
secretName: secure-example-tls
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-service
port:
number: 80kubectl apply -f /tmp/rate-limited-ingress.yamlConfigure custom headers and CORS
Add security headers and CORS configuration for web applications.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: headers-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
spec:
tls:
- hosts:
- webapp.example.com
secretName: webapp-example-tls
rules:
- host: webapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webapp-service
port:
number: 3000kubectl apply -f /tmp/headers-ingress.yamlMonitoring and troubleshooting ingress traffic
Enable ingress controller metrics
Configure Prometheus metrics collection for monitoring ingress performance.
helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.metrics.enabled=true \
--set controller.metrics.serviceMonitor.enabled=true \
--set controller.podAnnotations."prometheus\.io/scrape"="true" \
--set controller.podAnnotations."prometheus\.io/port"="10254"View ingress controller logs
Monitor real-time traffic and troubleshoot routing issues through controller logs.
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller -f
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --previousCheck certificate status
Monitor SSL certificate health and renewal status.
kubectl get certificates --all-namespaces
kubectl describe certificate sample-app-tls
kubectl get events --field-selector reason=Issuing
kubectl get events --field-selector reason=ReadyTest ingress connectivity
Verify ingress routing and SSL termination from inside the cluster.
kubectl run test-pod --image=curlimages/curl --rm -it --restart=Never -- /bin/sh
Inside the pod:
curl -I http://sample-app-service.default.svc.cluster.local
curl -k -I https://app.example.com
exitFor comprehensive monitoring setup, refer to our guide on setting up Kubernetes monitoring with Prometheus Operator.
Verify your setup
# Check ingress controller status
kubectl get pods -n ingress-nginx
kubectl get services -n ingress-nginx
Verify cert-manager
kubectl get pods -n cert-manager
kubectl get clusterissuers
Check certificates
kubectl get certificates --all-namespaces
kubectl get secrets | grep tls
Test HTTPS connectivity
curl -I https://app.example.com
openssl s_client -connect app.example.com:443 -servername app.example.comCommon issues
| Symptom | Cause | Fix |
|---|---|---|
| Certificate stuck in "Pending" | DNS not pointing to ingress | Verify A record points to LoadBalancer external IP |
| 502 Bad Gateway | Service not found or wrong port | Check service exists: kubectl get svc |
| Rate limit errors in logs | Let's Encrypt API limits hit | Use staging issuer for testing, then switch to production |
| SSL certificate not trusted | Using staging certificates | Switch annotation to letsencrypt-prod issuer |
| Ingress not getting external IP | Cloud provider LoadBalancer not provisioned | Check cloud provider quotas and LoadBalancer service logs |
| Path routing not working | Incorrect regex or path matching | Test with nginx.ingress.kubernetes.io/rewrite-target annotation |
Next steps
- Configure Kubernetes network policies for enhanced cluster security
- Set up Kubernetes persistent volume snapshots and backup automation
- Configure Kubernetes horizontal pod autoscaler for dynamic scaling
- Set up comprehensive Kubernetes monitoring with Prometheus and Grafana
- Implement Kubernetes secrets management with External Secrets Operator
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
EMAIL=""
DOMAIN=""
# Usage function
usage() {
echo "Usage: $0 --email <email> --domain <domain>"
echo " --email Email for Let's Encrypt certificates"
echo " --domain Domain name for testing ingress"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup on failure
cleanup() {
log_error "Installation failed. Cleaning up..."
kubectl delete namespace ingress-nginx --ignore-not-found=true 2>/dev/null || true
kubectl delete namespace cert-manager --ignore-not-found=true 2>/dev/null || true
helm uninstall ingress-nginx -n ingress-nginx 2>/dev/null || true
helm uninstall cert-manager -n cert-manager 2>/dev/null || true
exit 1
}
trap cleanup ERR
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--email)
EMAIL="$2"
shift 2
;;
--domain)
DOMAIN="$2"
shift 2
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
# Validate arguments
if [[ -z "$EMAIL" || -z "$DOMAIN" ]]; then
log_error "Both --email and --domain are required"
usage
fi
# Detect distribution
echo "[1/10] Detecting Linux distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected: $PRETTY_NAME using $PKG_MGR"
else
log_error "Cannot detect Linux distribution"
exit 1
fi
# Check prerequisites
echo "[2/10] Checking prerequisites..."
if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
log_error "This script requires root privileges or passwordless sudo"
exit 1
fi
# Update package manager
echo "[3/10] Updating package manager..."
if [[ $EUID -eq 0 ]]; then
$PKG_UPDATE
else
sudo $PKG_UPDATE
fi
# Install required packages
echo "[4/10] Installing required packages..."
PACKAGES="curl wget"
if [[ $EUID -eq 0 ]]; then
$PKG_INSTALL $PACKAGES
else
sudo $PKG_INSTALL $PACKAGES
fi
# Install kubectl if not present
if ! command -v kubectl &> /dev/null; then
log_info "Installing kubectl..."
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod 755 kubectl
if [[ $EUID -eq 0 ]]; then
mv kubectl /usr/local/bin/
else
sudo mv kubectl /usr/local/bin/
fi
fi
# Install Helm if not present
if ! command -v helm &> /dev/null; then
log_info "Installing Helm..."
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
fi
# Verify Kubernetes cluster access
echo "[5/10] Verifying Kubernetes cluster access..."
if ! kubectl cluster-info &>/dev/null; then
log_error "Cannot access Kubernetes cluster. Please configure kubectl first."
exit 1
fi
if ! kubectl auth can-i create namespaces &>/dev/null; then
log_error "Insufficient permissions. Need cluster-admin access."
exit 1
fi
log_info "Cluster access verified"
# Install NGINX Ingress Controller
echo "[6/10] Installing NGINX Ingress Controller..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
kubectl create namespace ingress-nginx --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \
--set controller.service.type=LoadBalancer \
--set controller.service.externalTrafficPolicy=Local \
--wait
# Verify ingress controller
echo "[7/10] Verifying NGINX Ingress Controller..."
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=300s
log_info "NGINX Ingress Controller installed successfully"
# Install cert-manager
echo "[8/10] Installing cert-manager..."
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.crds.yaml
kubectl create namespace cert-manager --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.13.3 \
--set installCRDs=false \
--set webhook.timeoutSeconds=4 \
--wait
# Verify cert-manager
kubectl wait --namespace cert-manager \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=cert-manager \
--timeout=300s
log_info "cert-manager installed successfully"
# Configure Let's Encrypt issuers
echo "[9/10] Configuring Let's Encrypt certificate issuers..."
# Create staging issuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: $EMAIL
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
EOF
# Create production issuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: $EMAIL
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
log_info "Certificate issuers configured"
# Create test application and ingress
echo "[10/10] Creating test application..."
# Deploy test app
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: test-app
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-app-service
namespace: default
spec:
selector:
app: test-app
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-app-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-staging"
spec:
tls:
- hosts:
- $DOMAIN
secretName: test-app-tls
rules:
- host: $DOMAIN
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-app-service
port:
number: 80
EOF
# Final verification
log_info "Waiting for ingress controller external IP..."
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=300s
EXTERNAL_IP=$(kubectl get service ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "pending")
echo
log_info "Installation completed successfully!"
echo
echo "Next steps:"
echo "1. Point your domain '$DOMAIN' to the external IP: $EXTERNAL_IP"
echo "2. Wait for DNS propagation (may take a few minutes)"
echo "3. Test SSL certificate with: curl -v https://$DOMAIN"
echo "4. Check certificate status: kubectl describe certificate test-app-tls"
echo
echo "To switch to production certificates, change the annotation to:"
echo " cert-manager.io/cluster-issuer: \"letsencrypt-prod\""
Review the script before running. Execute with: bash install.sh