How to Fix Nginx 400 Bad Request Error for HTTPS Connections with Let’s Encrypt


2 views

When setting up HTTPS with Nginx and Let's Encrypt, you might encounter a situation where your server returns HTTP 400 errors for all HTTPS requests, while the logs show garbled binary data like:

mysite_nginx | 1.1.1.1 - - [04/Apr/2019:16:43:52 +0000] "\\x16\\x03\\x01\\x00\\xC6\\x01\\x00\\x00\\xC2\\x03\\x03\\x97\\x08D\\x08\\x87\\x5Cg\\xDB\\x85\\x8Ch\\x16\\xC9\\x1E\\x01\\xDB\\x9F\\x12\\x04\\x91e\\xB3P]4]\\xFE\\xEF\\xE5^\\xB7\\x07\\x00\\x00\\x1C" 400 157 "-" "-" "-"

The binary-looking output in logs suggests Nginx is receiving what it thinks is malformed HTTP traffic on the SSL port. This typically happens when:

  • The SSL configuration has errors
  • Port forwarding isn't set up correctly in Docker
  • SSL certificate paths are incorrect
  • Protocol mismatch occurs between client and server

Looking at the provided configuration, there are several areas to examine:

server {
    listen 443 ssl;  # Explicit SSL declaration is important
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
    # ... rest of configuration
}

The most obvious issue in the original setup is the missing ssl parameter in the listen directive:

# Wrong:
listen 443;

# Correct:
listen 443 ssl;

When running Nginx in Docker with SSL, you need to ensure:

  1. Ports are correctly exposed in docker-compose.yml
  2. Certificate paths match the container filesystem
  3. Nginx has proper permissions to access certificate files

Here's an improved docker-compose snippet:

nginx:
    image: nginx:1.15.9-alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./sites-enabled:/etc/nginx/conf.d
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    ports:
      - "80:80"
      - "443:443"
    command: "/bin/sh -c 'nginx -g \"daemon off;\"'"

Here's a fully functional Nginx configuration for HTTPS:

server {
    listen 80;
    server_name example.com www.example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
    location / {
        proxy_pass http://app:8080;
        include /etc/nginx/proxy_params;
    }
}

If you're still seeing 400 errors after making these changes:

  1. Verify certificate files exist in the container:
    docker exec -it nginx_container ls -la /etc/letsencrypt/live/example.com/
  2. Check Nginx configuration syntax:
    docker exec -it nginx_container nginx -t
  3. Inspect SSL handshake with OpenSSL:
    openssl s_client -connect example.com:443 -servername example.com

For production environments, consider adding these SSL optimizations:

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;

When setting up HTTPS with Nginx and Let's Encrypt, receiving a 400 Bad Request error for HTTPS connections typically indicates a fundamental SSL/TLS configuration issue. The error logs showing garbled characters (\x16\x03\x01...) suggest the server isn't properly handling the SSL handshake.

Let's examine the key components of a working Nginx SSL configuration:

server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
    # Other location blocks...
}

The Docker setup introduces several potential failure points:

  • Volume mounts not having correct permissions
  • Certificate files not being read properly
  • Missing ssl parameter in listen directive

Before making configuration changes, verify these aspects:

  1. Certificate files exist and are accessible:
    docker exec container_name ls -la /etc/letsencrypt/live/example.com/
  2. Check Nginx configuration syntax:
    docker exec container_name nginx -t

Here's a proven configuration that works with Docker and Let's Encrypt:

server {
    listen 80;
    server_name example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://app:8000;
        include /etc/nginx/proxy_params;
    }

    location /static/ {
        alias /static/;
        expires 30d;
        access_log off;
    }
}

Use these commands to diagnose SSL issues:

openssl s_client -connect example.com:443 -showcerts
curl -vI https://example.com