Configure NGINX SSL termination with Redis session storage

Intermediate 45 min Apr 30, 2026 73 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -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
sudo dnf install -y nginx redis epel-release
sudo dnf install -y nginx-mod-http-lua nginx-mod-http-set-misc

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
Note: For production, use certificates from a trusted CA or set up automated Let's Encrypt certificates.

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
Important: Fix any configuration errors before proceeding. Common issues include missing semicolons, incorrect file paths, or invalid SSL certificate paths.

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
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

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

SymptomCauseFix
502 Bad GatewayBackend servers not runningStart your application servers on ports 8080, 8081 or update upstream configuration
SSL certificate errorsInvalid certificate paths or permissionsCheck certificate files exist and are readable: sudo ls -la /etc/ssl/nginx/
Redis connection failedRedis not running or wrong passwordCheck Redis status: sudo systemctl status redis-server and verify password in config
Lua script errorsMissing lua-resty-redis packageInstall Redis Lua library: sudo apt install lua-resty-redis
Session not persistingCookie domain or HTTPS issuesCheck browser developer tools for cookie settings and ensure HTTPS is working
High memory usageRedis not configured with memory limitsSet maxmemory and maxmemory-policy in Redis configuration
Session timeoutsTTL too short or not being refreshedIncrease 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
Scaling tip: For high-traffic applications, consider setting up Redis cluster sharding for better performance and availability.

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

Running this in production?

Need this handled for you? Setting up SSL termination with Redis once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.