nginxsslssh

Nginx SSL PreRead Not Extracting Server Name from SSH Connections


I am trying to implement SSL pre-read in Nginx. The goal is for incoming SSH connections to be routed to a different upstream based on the server name. When I try a connection, it's refused. I think Nginx is failing to extract the server name, possibly the SSH connection I'm attempting are not being done correctly for SSL preread to work?

I confirmed Nginx is build with --with-stream and --with-stream-ssl-preread-module.

My Nginx configuration is simple and Nginx accepts the config without errors:

stream {

    log_format  ssh  '|| SSL_PreRead $ssl_preread_server_name ||';
    access_log  /opt/homebrew/var/log/nginx/ssh_access.log  ssh;

    map $ssl_preread_server_name $ssh_upstream {
            app1.mydomain.com   app1-deviceIP:22;
            app2.mydomain.com   app2-deviceIP:22;
            app3.mydomain.com   app3-deviceIP:22;
            app4.mydomain.com   app4-deviceIP:22;
        app5.mydomain.com   app5-deviceIP:22;
        app6.mydomain.com   app6-deviceIP:22;
        app7.mydomain.com   app7-deviceIP:22;
        app8.mydomain.com   app8-deviceIP:22;
        app9.mydomain.com   app9-deviceIP:22;
    }

    server {
        listen 22000;  # Network router forwards incoming 22 to Nginx server port 22000
        proxy_pass $ssh_upstream;
        ssl_preread on;
    }

}

After attempting several connections, this is what I see in the ssh_access.log file. As you can see, the $ssl_preread_server_name variable is empty.

|| SSL_PreRead -  ||
|| SSL_PreRead -  ||
|| SSL_PreRead -  ||
|| SSL_PreRead -  ||
|| SSL_PreRead -  ||
|| SSL_PreRead -  ||

This is how I'm attempting to connect:

ssh -i my_private_key_file [email protected]

What am I missing?

Edit / Solution

With the help of Steffen Ullrich's response and more searching I've come to learn SSH simply does not Server Name Indication (SNI) that SSL does.

But you can wrap SSH inside SSL and include the SNI. This post was very informative. I now initiate my SSH sessions from the client machine with a command like this:

ssh -o ProxyCommand="openssl s_client -quiet -servername app1.mydomain.com -connect MyNginxServer:22000" [email protected] -i ssh_private_key_file

To receive this, in the Nginx stream block, you actually don't need to use $ssl_preread_server_name, you can just use $ssl_server_name to map to the upstream. Also since you're terminating an SSL connection here, you need to listen for SSL and provide the SSL certificate. My stream block now looks like below. My understanding is it's not best practice to map server name to upstream IPs directly like I have, instead you should map to a upstream and the upstream IP defined within like in the Nginx documentation.

stream {

log_format  ssh  '|| SSL_ServerName $ssl_server_name || Upstream $ssh_upstream ||';
access_log  /opt/homebrew/var/log/nginx/ssh_access.log  ssh;

map $ssl_server_name $ssh_upstream {
    app1.mydomain.com   app1-deviceIP:22;
    app2.mydomain.com   app2-deviceIP:22;
    app3.mydomain.com   app3-deviceIP:22;
    app4.mydomain.com   app4-deviceIP:22;
    app5.mydomain.com   app5-deviceIP:22;
    app6.mydomain.com   app6-deviceIP:22;
    app7.mydomain.com   app7-deviceIP:22;
    app8.mydomain.com   app8-deviceIP:22;
    app9.mydomain.com   app9-deviceIP:22;
}

server {
    listen 22000 ssl; # Network router forwards incoming 22 to Nginx server port 22000

    ssl_certificate /myPath/fullchain.pem;
    ssl_certificate_key /myPath/privkey.pem;

    proxy_pass $ssh_upstream;
}

}


Solution

  • The SSH protocol is different from SSL and thus cannot be handled by SSL prereading. Apart from that the target hostname is not part of the SSH handshake, so there is no way to route based on this even if one would use some kind of SSH prereading instead of SSL prereading.