Implementing SSH over HTTPS with Nginx Reverse Proxy and Proxytunnel: A Complete Guide


3 views

When trying to establish an SSH connection through HTTPS using Nginx as a reverse proxy and proxytunnel as the client-side proxy, many developers encounter the 400 Bad Request error from Nginx. This typically occurs because Nginx isn't properly configured to handle CONNECT requests.

// Sample error from nginx access log
203.xxx.xxx.xxx - - [08/Feb/2012:15:17:39 +1100] "CONNECT localhost:22 HTTP/1.0" 400 173 "-" "-"

SSH over HTTPS requires three main components:

  1. Client-side proxytunnel configuration
  2. Nginx reverse proxy server
  3. Backend SSH server

The key is to properly configure Nginx to handle CONNECT method requests:

server {
    listen 443 ssl;
    server_name ssh.example.com;
    
    ssl_certificate /etc/ssl/certs/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;

    # Special handling for CONNECT method
    error_page 418 = @proxy_connect;
    if ($request_method = CONNECT) {
        return 418;
    }

    location @proxy_connect {
        proxy_pass http://localhost:22;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
    
    # For regular HTTPS traffic
    location / {
        return 403; # or serve regular content
    }
}

Here's the corrected .ssh/config setup:

Host example.com
  Hostname example.com
  ProxyCommand /usr/bin/proxytunnel -p ssh.example.com:443 -d %h:%p -E -v -H "User-Agent: Mozilla/5.0"
  ServerAliveInterval 30
  TCPKeepAlive yes

If the above doesn't work, consider these alternatives:

Using socat instead of proxytunnel

ProxyCommand socat - PROXY:ssh.example.com:%h:%p,proxyport=443,proxyauth=user:pass

NGINX Stream Module

For NGINX Plus or compiled with stream module:

stream {
    server {
        listen 443;
        proxy_pass localhost:22;
        proxy_protocol on;
    }
}
  • Verify Nginx has HTTP CONNECT support (compile with --with-http_ssl_module)
  • Check firewall rules for both TCP 443 and 22
  • Test basic SSH connectivity before adding proxy layer
  • Enable debug logging in Nginx: error_log /var/log/nginx/error.log debug;

When implementing this setup:

  1. Always use SSL with strong ciphers
  2. Implement rate limiting in Nginx
  3. Consider client certificate authentication
  4. Regularly update both Nginx and proxytunnel



When attempting to establish an SSH connection tunneled through HTTPS using Nginx as a reverse proxy, many developers encounter the frustrating 400 Bad Request error. The core issue lies in how Nginx handles CONNECT requests differently from standard HTTP traffic.


The key error pattern we're seeing:
<- 
<- 400 Bad Request
<- 
<- 

400 Bad Request

<-
nginx/1.0.5
<- <-

This occurs because Nginx by default doesn't support HTTP CONNECT method required for tunneling. The current configuration attempts to proxy_pass the CONNECT request as regular HTTP traffic.

Here's the correct way to configure Nginx as a TLS termination point for SSH:

server {
    listen 443 ssl;
    server_name ssh.example.com;

    ssl_certificate /etc/ssl/certs/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;

    # TCP proxy configuration
    location / {
        proxy_pass http://127.0.0.1:22;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # These are crucial for SSH over HTTPS
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 1d;
    }
}

The client-side ~/.ssh/config needs these adjustments:

Host example.com
    HostName example.com
    Port 443
    ProxyCommand /usr/bin/proxytunnel -p ssh.example.com:443 -d %h:%p -E -H "User-Agent: Mozilla/5.0"

The key differences from the original:
1. Removed DynamicForward which isn't needed for basic tunneling
2. Simplified the ProxyCommand line
3. Updated User-Agent string

If you specifically need CONNECT method support, consider this Nginx stream module configuration:

stream {
    server {
        listen 443;
        ssl_preread on;
        proxy_pass 127.0.0.1:22;
        proxy_protocol on;
    }
}

This requires Nginx compiled with --with-stream and --with-stream_ssl_preread_module.

To verify the setup:

1. Check Nginx error logs:

tail -f /var/log/nginx/error.log

2. Test with curl first:

curl -vk https://ssh.example.com

3. For detailed proxytunnel debugging:

ssh -vvv user@example.com

Important security measures to implement:

1. Rate limiting:

limit_conn_zone $binary_remote_addr zone=ssh:10m;
limit_conn ssh 3;

2. IP restrictions:

allow 192.168.1.0/24;
deny all;

3. Strong SSL configuration:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;