Set up NGINX as an SSL termination proxy with Redis backend for session storage, enabling scalable load balancing and persistent user sessions across multiple application servers.
Prerequisites
- Root or sudo access
- Basic understanding of NGINX configuration
- Redis knowledge helpful
- SSL certificate (self-signed or CA-issued)
What this solves
SSL termination with NGINX offloads encryption workload from your application servers while Redis session storage enables horizontal scaling by sharing user sessions across multiple backend servers. This configuration is essential for load-balanced applications that need persistent user sessions and efficient SSL handling.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you have the latest security patches and package versions.
sudo apt update && sudo apt upgrade -y
Install NGINX and Redis
Install NGINX for SSL termination and reverse proxy functionality, along with Redis for session storage.
sudo apt install -y nginx redis-server nginx-module-http-redis2 nginx-module-http-set-misc nginx-module-http-lua
sudo apt install -y lua-resty-redis
Configure Redis for session storage
Configure Redis with security settings and session-optimized parameters for reliable session storage.
bind 127.0.0.1 ::1
port 6379
timeout 0
tcp-keepalive 300
daemonize yes
supervised systemd
pidfile /var/run/redis/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-server.log
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
requirepass your_redis_password_here
maxmemory 256mb
maxmemory-policy allkeys-lru
maxclients 10000
Enable and start Redis
Start Redis and enable it to automatically start on system boot.
sudo systemctl enable --now redis-server
sudo systemctl status redis-server
Generate SSL certificates
Create self-signed SSL certificates for testing, or use existing certificates from your CA or Let's Encrypt.
sudo mkdir -p /etc/ssl/nginx
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/nginx/nginx-selfsigned.key \
-out /etc/ssl/nginx/nginx-selfsigned.crt \
-subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=example.com"
sudo chmod 600 /etc/ssl/nginx/nginx-selfsigned.key
sudo chmod 644 /etc/ssl/nginx/nginx-selfsigned.crt
Create Lua session management script
Create a Lua script for handling Redis session operations including creation, validation, and cleanup.
local redis = require "resty.redis"
local red = redis:new()
-- Redis connection configuration
red:set_timeout(1000) -- 1 second timeout
local function connect_redis()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return false
end
local res, err = red:auth("your_redis_password_here")
if not res then
ngx.log(ngx.ERR, "Failed to authenticate with Redis: ", err)
return false
end
return true
end
local function generate_session_id()
local session_id = ngx.var.remote_addr .. "-" .. ngx.time() .. "-" .. math.random(10000, 99999)
return ngx.encode_base64(session_id)
end
local function get_session()
local session_cookie = ngx.var.cookie_sessionid
if not session_cookie then
return nil
end
if not connect_redis() then
return nil
end
local session_data, err = red:get("session:" .. session_cookie)
if not session_data or session_data == ngx.null then
return nil
end
-- Extend session TTL
red:expire("session:" .. session_cookie, 3600)
red:set_keepalive(10000, 100)
return session_data
end
local function create_session()
local session_id = generate_session_id()
local session_data = {
created = ngx.time(),
remote_addr = ngx.var.remote_addr,
user_agent = ngx.var.http_user_agent
}
if not connect_redis() then
return nil
end
local ok, err = red:setex("session:" .. session_id, 3600, ngx.encode_base64(ngx.var.remote_addr))
if not ok then
ngx.log(ngx.ERR, "Failed to create session: ", err)
return nil
end
red:set_keepalive(10000, 100)
-- Set session cookie
ngx.header["Set-Cookie"] = "sessionid=" .. session_id .. "; Path=/; HttpOnly; Secure; Max-Age=3600"
return session_id
end
-- Main session handler
local action = ngx.var.arg_action
if action == "create" then
local session_id = create_session()
if session_id then
ngx.say("Session created: " .. session_id)
else
ngx.status = 500
ngx.say("Failed to create session")
end
elseif action == "validate" then
local session_data = get_session()
if session_data then
ngx.header["X-Session-Valid"] = "true"
ngx.say("Session valid")
else
ngx.header["X-Session-Valid"] = "false"
ngx.status = 401
ngx.say("Invalid session")
end
else
ngx.status = 400
ngx.say("Invalid action")
end
sudo mkdir -p /etc/nginx/lua
sudo chown root:root /etc/nginx/lua/session.lua
sudo chmod 644 /etc/nginx/lua/session.lua
Configure NGINX with SSL termination and Redis integration
Create the main NGINX configuration with SSL termination, Redis session handling, and upstream backend servers.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
use epoll;
multi_accept on;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'"$http_x_session_id" $request_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# Upstream backend servers
upstream app_backend {
least_conn;
server 127.0.0.1:8080 weight=1 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# Lua package path
lua_package_path "/etc/nginx/lua/?.lua;;";
# Redis connection pool
lua_shared_dict redis_cluster_slot_locks 100k;
include /etc/nginx/sites-enabled/*;
}
Create SSL virtual host with Redis session handling
Configure the virtual host that handles SSL termination and integrates Redis session management with backend applications.
server {
listen 80;
server_name example.com www.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL certificates
ssl_certificate /etc/ssl/nginx/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/nginx/nginx-selfsigned.key;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Session management endpoint
location /api/session {
access_log /var/log/nginx/session.log main;
# CORS headers for session API
add_header Access-Control-Allow-Origin "https://example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = 'OPTIONS') {
return 204;
}
content_by_lua_file /etc/nginx/lua/session.lua;
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Application proxy with session validation
location / {
# Session validation
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local session_cookie = ngx.var.cookie_sessionid
if session_cookie then
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
red:auth("your_redis_password_here")
local session_data, err = red:get("session:" .. session_cookie)
if session_data and session_data ~= ngx.null then
-- Valid session, extend TTL
red:expire("session:" .. session_cookie, 3600)
ngx.req.set_header("X-Session-ID", session_cookie)
ngx.req.set_header("X-Session-Valid", "true")
else
-- Invalid session
ngx.req.set_header("X-Session-Valid", "false")
end
red:set_keepalive(10000, 100)
end
else
ngx.req.set_header("X-Session-Valid", "false")
end
}
# Proxy settings
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
# Error handling
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
}
# Static files with caching
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Security: deny access to sensitive files
location ~ /\. {
deny all;
access_log off;
}
location ~ ~$ {
deny all;
access_log off;
}
}
sudo ln -s /etc/nginx/sites-available/ssl-redis /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
Test NGINX configuration
Validate the NGINX configuration syntax before starting the service to catch any configuration errors.
sudo nginx -t
Enable and start services
Start NGINX and ensure both services are enabled for automatic startup on system boot.
sudo systemctl enable --now nginx
sudo systemctl status nginx
sudo systemctl status redis-server
Configure firewall rules
Open the necessary ports for HTTPS traffic while maintaining security by blocking unnecessary access.
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw status
Verify your setup
Test the SSL termination and Redis session integration to ensure everything is working correctly.
# Test Redis connectivity
redis-cli -a your_redis_password_here ping
Test NGINX SSL configuration
curl -k -I https://example.com/health
Create a session via API
curl -k -X POST "https://example.com/api/session?action=create"
Validate the session
curl -k -X GET "https://example.com/api/session?action=validate" -H "Cookie: sessionid=YOUR_SESSION_ID"
Check NGINX error logs
sudo tail -f /var/log/nginx/error.log
Check Redis session data
redis-cli -a your_redis_password_here KEYS "session:*"
redis-cli -a your_redis_password_here TTL "session:YOUR_SESSION_ID"
Test backend server simulation
Create a simple test backend server to verify the complete SSL termination and session flow.
#!/usr/bin/env python3
import http.server
import socketserver
import json
from urllib.parse import urlparse, parse_qs
class SessionHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'method': self.command,
'path': self.path,
'headers': dict(self.headers),
'session_valid': self.headers.get('X-Session-Valid', 'false'),
'session_id': self.headers.get('X-Session-ID', 'none'),
'ssl_terminated': self.headers.get('X-Forwarded-Proto') == 'https'
}
self.wfile.write(json.dumps(response, indent=2).encode())
Start test server on port 8080
with socketserver.TCPServer(("", 8080), SessionHandler) as httpd:
print("Test server running on port 8080")
httpd.serve_forever()
chmod +x /tmp/test_server.py
python3 /tmp/test_server.py &
Test the complete flow
curl -k https://example.com/ | jq
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Backend servers not running | Start your application servers on ports 8080, 8081 or update upstream configuration |
| SSL certificate errors | Invalid certificate paths or permissions | Check certificate files exist and are readable: sudo ls -la /etc/ssl/nginx/ |
| Redis connection failed | Redis not running or wrong password | Check Redis status: sudo systemctl status redis-server and verify password in config |
| Lua script errors | Missing lua-resty-redis package | Install Redis Lua library: sudo apt install lua-resty-redis |
| Session not persisting | Cookie domain or HTTPS issues | Check browser developer tools for cookie settings and ensure HTTPS is working |
| High memory usage | Redis not configured with memory limits | Set maxmemory and maxmemory-policy in Redis configuration |
| Session timeouts | TTL too short or not being refreshed | Increase session TTL in Lua script or check session refresh logic |
Performance optimization
Fine-tune your setup for production workloads with these additional configurations.
Configure Redis clustering for high availability
For production environments, consider setting up Redis clustering or master-slave replication.
# Check current Redis memory usage
redis-cli -a your_redis_password_here INFO memory
Monitor Redis performance
redis-cli -a your_redis_password_here --latency-history -i 1
NGINX performance tuning
Optimize NGINX worker processes and connection handling for your specific workload.
# Add to http block for better performance
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
Add connection pooling for upstream
upstream app_backend {
least_conn;
server 127.0.0.1:8080 weight=1 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 weight=1 max_fails=3 fail_timeout=30s;
keepalive 64;
keepalive_requests 100;
keepalive_timeout 60s;
}
Security considerations
Implement additional security measures for production deployments.
Secure Redis configuration
Harden Redis with additional security settings and monitoring.
# Add these security settings
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command EVAL ""
rename-command CONFIG ""
protected-mode yes
tcp-backlog 511
timeout 300
sudo systemctl restart redis-server
Enable rate limiting
Add rate limiting to prevent abuse of the session API endpoints.
# Add to http block in main nginx.conf
limit_req_zone $binary_remote_addr zone=session_api:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=general:10m rate=100r/m;
Add to session API location block
location /api/session {
limit_req zone=session_api burst=5 nodelay;
# ... existing configuration
}
Next steps
- Automate SSL certificate management with Let's Encrypt
- Set up NGINX monitoring with Prometheus and Grafana
- Configure advanced rate limiting and DDoS protection
- Monitor Redis performance and session metrics
- Scale with HAProxy for multi-server load balancing
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'
# Default values
DOMAIN="${1:-example.com}"
REDIS_PASSWORD="${2:-$(openssl rand -base64 32)}"
# Usage message
usage() {
echo "Usage: $0 [domain] [redis_password]"
echo "Example: $0 mysite.com mySecurePassword123"
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 function
cleanup() {
log_error "Script failed! Check logs above for details."
exit 1
}
trap cleanup ERR
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
REDIS_SERVICE="redis-server"
REDIS_CONFIG="/etc/redis/redis.conf"
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
NGINX_USER="www-data"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
REDIS_SERVICE="redis"
REDIS_CONFIG="/etc/redis.conf"
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_USER="nginx"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
REDIS_SERVICE="redis"
REDIS_CONFIG="/etc/redis.conf"
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_USER="nginx"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
log_info "Detected distribution: $ID"
log_info "Domain: $DOMAIN"
echo "[1/8] Updating system packages..."
if [ "$PKG_MGR" = "apt" ]; then
apt update && apt upgrade -y
else
$PKG_INSTALL epel-release || true
$PKG_MGR update -y
fi
echo "[2/8] Installing NGINX and Redis..."
if [ "$PKG_MGR" = "apt" ]; then
$PKG_INSTALL nginx redis-server lua-resty-redis
else
$PKG_INSTALL nginx redis
fi
echo "[3/8] Configuring Redis for session storage..."
cp "$REDIS_CONFIG" "${REDIS_CONFIG}.backup"
cat > "$REDIS_CONFIG" << EOF
bind 127.0.0.1 ::1
port 6379
timeout 0
tcp-keepalive 300
daemonize yes
supervised systemd
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
requirepass $REDIS_PASSWORD
maxmemory 256mb
maxmemory-policy allkeys-lru
maxclients 10000
EOF
if [ "$PKG_MGR" = "apt" ]; then
echo "pidfile /var/run/redis/redis-server.pid" >> "$REDIS_CONFIG"
echo "logfile /var/log/redis/redis-server.log" >> "$REDIS_CONFIG"
echo "dir /var/lib/redis" >> "$REDIS_CONFIG"
echo "dbfilename dump.rdb" >> "$REDIS_CONFIG"
else
echo "pidfile /var/run/redis.pid" >> "$REDIS_CONFIG"
echo "logfile /var/log/redis/redis.log" >> "$REDIS_CONFIG"
echo "dir /var/lib/redis" >> "$REDIS_CONFIG"
echo "dbfilename dump.rdb" >> "$REDIS_CONFIG"
fi
echo "[4/8] Starting Redis service..."
systemctl enable --now "$REDIS_SERVICE"
sleep 2
systemctl is-active --quiet "$REDIS_SERVICE" || { log_error "Redis failed to start"; exit 1; }
echo "[5/8] Generating SSL certificates..."
mkdir -p /etc/ssl/nginx
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/nginx/nginx-selfsigned.key \
-out /etc/ssl/nginx/nginx-selfsigned.crt \
-subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=$DOMAIN" 2>/dev/null
chmod 600 /etc/ssl/nginx/nginx-selfsigned.key
chmod 644 /etc/ssl/nginx/nginx-selfsigned.crt
chown root:root /etc/ssl/nginx/nginx-selfsigned.*
echo "[6/8] Creating Lua session script..."
mkdir -p /etc/nginx/lua
cat > /etc/nginx/lua/session.lua << EOF
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local function connect_redis()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return false
end
local res, err = red:auth("$REDIS_PASSWORD")
if not res then
ngx.log(ngx.ERR, "Failed to authenticate with Redis: ", err)
return false
end
return true
end
local function get_session()
local session_cookie = ngx.var.cookie_sessionid
if not session_cookie or not connect_redis() then
return nil
end
local session_data, err = red:get("session:" .. session_cookie)
if session_data == ngx.null then
return nil
end
red:expire("session:" .. session_cookie, 3600)
red:set_keepalive(10000, 100)
return session_data
end
if get_session() then
ngx.header["X-Session-Status"] = "valid"
else
ngx.header["X-Session-Status"] = "invalid"
end
EOF
chown root:root /etc/nginx/lua/session.lua
chmod 644 /etc/nginx/lua/session.lua
echo "[7/8] Configuring NGINX..."
if [ "$PKG_MGR" = "apt" ]; then
NGINX_CONFIG_FILE="$NGINX_CONF_DIR/$DOMAIN"
else
NGINX_CONFIG_FILE="$NGINX_CONF_DIR/$DOMAIN.conf"
fi
cat > "$NGINX_CONFIG_FILE" << EOF
upstream backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name $DOMAIN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $DOMAIN;
ssl_certificate /etc/ssl/nginx/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/nginx/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
access_log /var/log/nginx/$DOMAIN.access.log;
error_log /var/log/nginx/$DOMAIN.error.log;
location / {
access_by_lua_file /etc/nginx/lua/session.lua;
proxy_pass http://backend;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
EOF
if [ "$PKG_MGR" = "apt" ] && [ -d "$NGINX_ENABLED_DIR" ]; then
ln -sf "$NGINX_CONFIG_FILE" "$NGINX_ENABLED_DIR/"
rm -f "$NGINX_ENABLED_DIR/default"
fi
nginx -t || { log_error "NGINX configuration test failed"; exit 1; }
echo "[8/8] Starting services and final verification..."
systemctl enable --now nginx
systemctl reload nginx
# Verify services
systemctl is-active --quiet nginx || { log_error "NGINX failed to start"; exit 1; }
systemctl is-active --quiet "$REDIS_SERVICE" || { log_error "Redis is not running"; exit 1; }
# Test Redis connection
redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG || { log_error "Redis connection failed"; exit 1; }
log_info "Installation completed successfully!"
log_info "Domain: $DOMAIN"
log_info "Redis password: $REDIS_PASSWORD"
log_info "SSL certificate: /etc/ssl/nginx/nginx-selfsigned.crt"
log_warn "Using self-signed certificates - replace with CA-issued certificates for production"
log_warn "Configure your application backend to listen on port 8080"
Review the script before running. Execute with: bash install.sh