Configuring Nginx as SSL Reverse Proxy for Upstream Servers with Self-Signed Certificates


2 views

When implementing a reverse proxy for internal APIs, we often encounter certificate management challenges. The core problem emerges when:

  • Internal clients need to connect securely without managing self-signed certificates
  • The upstream API server requires SSL connections
  • You need application-level authentication before proxying

Here's the optimal solution using Nginx's proxy capabilities:

server {
    listen 443 ssl;
    server_name api-gateway.example.com;
    
    # Client-facing SSL
    ssl_certificate /path/to/public.crt;
    ssl_certificate_key /path/to/private.key;
    
    location /validate {
        # Rails authentication endpoint
        proxy_pass http://rails-app:3000;
        proxy_set_header X-Original-URI $request_uri;
    }
    
    location /api/ {
        # SSL proxy to upstream
        proxy_pass https://upstream-api.internal/;
        proxy_ssl_certificate /path/to/upstream-client.crt;
        proxy_ssl_certificate_key /path/to/upstream-client.key;
        proxy_ssl_trusted_certificate /path/to/upstream-ca.crt;
        proxy_ssl_verify on;
        proxy_ssl_verify_depth 2;
        
        # Pass through auth headers
        proxy_set_header Authorization $http_authorization;
    }
}

Three practical approaches for certificate management:

1. Full SSL Termination

# Simple termination (client to nginx SSL only)
server {
    listen 443 ssl;
    proxy_pass http://upstream-api.internal;
    # ... SSL config for client side only
}

2. SSL Re-encryption

# Full SSL path (client to nginx to upstream)
server {
    listen 443 ssl;
    proxy_pass https://upstream-api.internal;
    proxy_ssl_server_name on;
    # ... Both client and upstream SSL config
}

3. Conditional SSL Passthrough

# Hybrid approach based on authentication
map $upstream_http_x_ssl_required $backend_scheme {
    "1" "https";
    default "http";
}

server {
    listen 443 ssl;
    if ($http_authorization = "") {
        return 401;
    }
    proxy_pass $backend_scheme://upstream-api.internal;
}

When implementing SSL proxy:

  • Enable keepalive connections to upstream: proxy_http_version 1.1;
  • Adjust buffer sizes: proxy_buffer_size 16k; proxy_buffers 4 32k;
  • Consider SSL session reuse: ssl_session_cache shared:SSL:10m;

Essential Nginx debugging directives:

error_log /var/log/nginx/proxy_error.log debug;
proxy_intercept_errors on;
proxy_next_upstream error timeout invalid_header;

For troubleshooting SSL handshakes:

openssl s_client -connect upstream-api.internal:443 -showcerts -debug

Recommended security practices:

# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:...';

# Proxy security
proxy_hide_header X-Powered-By;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

When implementing a reverse proxy for internal APIs with self-signed certificates, we face a dual SSL challenge:

  1. Client-to-Nginx SSL termination
  2. Nginx-to-upstream SSL passthrough

Here's the complete nginx configuration that handles both SSL termination and upstream SSL:


server {
    listen 443 ssl;
    server_name api-gateway.internal;
    
    # SSL termination for client connections
    ssl_certificate /etc/nginx/ssl/public.crt;
    ssl_certificate_key /etc/nginx/ssl/private.key;
    
    location / {
        # Rails auth via X-Sendfile
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        
        # Upstream SSL configuration
        proxy_ssl_certificate /etc/nginx/ssl/upstream_client.crt;
        proxy_ssl_certificate_key /etc/nginx/ssl/upstream_client.key;
        proxy_ssl_trusted_certificate /etc/nginx/ssl/upstream_ca.crt;
        proxy_ssl_verify on;
        proxy_ssl_verify_depth 2;
        proxy_ssl_session_reuse on;
        
        proxy_pass https://upstream-api.internal:8443;
    }
}

For environments using self-signed certificates between nginx and upstream servers:


proxy_ssl_trusted_certificate /etc/nginx/ssl/upstream_ca.crt;
proxy_ssl_verify off; # Only for development!

To optimize SSL handshake performance between nginx and upstream:


proxy_ssl_session_reuse on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_ciphers HIGH:!aNULL:!MD5;
keepalive_timeout 75s;
keepalive_requests 100;

Debug upstream SSL problems with these nginx directives:


error_log /var/log/nginx/ssl_error.log debug;
proxy_ssl_server_name on;
proxy_ssl_name $proxy_host;

For scenarios requiring HTTP between client and nginx, but SSL to upstream:


server {
    listen 80;
    server_name api-gateway.internal;
    
    location / {
        proxy_pass https://upstream-api.internal:8443;
        proxy_ssl_certificate /etc/nginx/ssl/client.pem;
        proxy_ssl_verify off; # For self-signed certs
    }
}

Remember to configure appropriate headers and timeouts based on your API requirements.