SSH Reverse Proxy with Name-Based Virtual Hosting: A Secure Alternative to Port Forwarding


2 views

In modern development environments, we often need to access multiple SSH servers while maintaining strict firewall policies. Traditional approaches like port forwarding (e.g., mapping server1:22 → gateway:2222, server2:22 → gateway:2223) quickly become unmanageable and violate security best practices by exposing multiple ports.

The HTTP world solved this elegantly with name-based virtual hosting in reverse proxies like Nginx or HAProxy. A single port (80/443) serves multiple websites based on the Host header. We need similar functionality for SSH connections.

While HAProxy doesn't natively support SSH proxying, several specialized tools can achieve this:

1. SSH ProxyCommand with Host Headers

Combine SSH's ProxyCommand with a lightweight HTTP server to route connections:

# ~/.ssh/config
Host *.dev.example.com
    ProxyCommand curl -s -H "Host: %h" http://gateway.example.com/ssh-route | nc -U /var/run/sshgate.sock

2. SNI-Based SSH Routing

Leverage TLS SNI extension with tools like:

# socat example for SNI routing
socat openssl-listen:2222,reuseaddr,cert=gateway.pem,fork \
    openssl-connect:$(openssl s_client -connect $DESTINATION:2222 -servername $SNI -tlsextdebug 2>&1 | grep -i '^subject' | cut -d= -f4).internal:22

3. Custom SSH Gateway with Go

Here's a basic implementation in Go:

package main

import (
    "net"
    "strings"
)

func main() {
    ln, _ := net.Listen("tcp", ":2222")
    for {
        conn, _ := ln.Accept()
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    // Parse initial SSH protocol exchange to extract hostname
    buf := make([]byte, 256)
    n, _ := conn.Read(buf)
    hostname := extractHostname(buf[:n])
    
    // Route to backend
    backend, _ := net.Dial("tcp", hostname+":22")
    go copyStream(conn, backend)
    go copyStream(backend, conn)
}

For enterprise environments, consider:

  • Teleport: Open-source SSH gateway with RBAC
  • Apache Guacamole: Web-based SSH gateway
  • Bastillion: Centralized SSH management

When implementing any SSH proxy solution:

  • Always use TLS for the outer connection
  • Implement rate limiting
  • Use certificate-based authentication
  • Log all connection attempts

In modern development environments, managing SSH access for multiple teams or projects often leads to port explosion. While HTTP reverse proxies like Nginx or HAProxy elegantly solve this for web traffic using name-based virtual hosting, SSH traditionally requires either:

  • Different ports for each backend (e.g., 22, 2222, 2223)
  • Complex SSH tunneling configurations
  • Individual user management on jump hosts

We can implement name-based SSH routing using:

Client → (SSH on port 22) → Reverse Proxy → (Proper backend based on SNI/hostname)

Modern SSH clients (OpenSSH 7.7+) support TLS-style SNI extensions. Here's how to leverage this:

# On the proxy server (using OpenSSH 8.0+)
Match Host *.dev-env.example.com
    ProxyJump backend-%h:22

# In ssh_config for clients
Host *.dev-env.example.com
    HostName ssh-gateway.example.com
    Port 22
    RequestTTY force
    RemoteCommand ssh $(echo %h | sed 's/\\.dev-env//')

While HAProxy primarily handles HTTP, its TCP mode can route SSH:

frontend ssh-in
    bind *:22
    mode tcp
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    
    use_backend team1_ssh if { req_ssl_sni -i team1.dev-env.example.com }
    use_backend team2_ssh if { req_ssl_sni -i team2.dev-env.example.com }

backend team1_ssh
    mode tcp
    server team1-server 192.168.1.10:22 check

backend team2_ssh
    mode tcp
    server team2-server 192.168.1.20:22 check

For restricted networks, consider wrapping SSH in HTTPS:

# Client-side config using corkscrew
Host restricted-ssh
    HostName your-proxy.example.com
    Port 443
    ProxyCommand corkscrew %h %p ~/.ssh/proxy-auth

# Server-side (Nginx config)
location /ssh-proxy {
    proxy_pass http://backend-ssh;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}
  • Always use SSH certificate-based authentication at the proxy level
  • Implement rate limiting to prevent brute force attacks
  • Log all connection attempts with timestamps and source IPs
  • Consider using port knocking for additional security layer