Opened 8 years ago

Closed 5 weeks ago

#195 closed enhancement (fixed)

Close connection if SSL not enabled for vhost

Reported by: svario.it/gioele Owned by: somebody
Priority: minor Milestone:
Component: nginx-module Version: 1.3.x
Keywords: Cc:
uname -a: Linux camelia.svario.it 3.2.0-29-virtual #46-Ubuntu SMP Fri Jul 27 17:23:50 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux
nginx -V: nginx version: nginx/1.1.19
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-log-path=/var/log/nginx/access.log --http-proxy-temp-path=/var/lib/nginx/proxy --lock-path=/var/lock/nginx.lock --pid-path=/var/run/nginx.pid --with-http_gzip_static_module --with-http_ssl_module --with-ipv6 --without-http_browser_module --without-http_geo_module --without-http_limit_req_module --without-http_limit_zone_module --without-http_memcached_module --without-http_referer_module --without-http_scgi_module --without-http_split_clients_module --with-http_stub_status_module --without-http_ssi_module --without-http_userid_module --without-http_uwsgi_module --add-module=/build/buildd/nginx-1.1.19/debian/modules/nginx-echo

Description

Instead of using the default SSL certificate, nginx should (by default or when configured) close the SSL connection as soon as it realizes that the requested domain has not been configured to be served over HTTPS.

For example,

server {
    listen 80 default_server;
    listen 443 ssl;

    server_name     aaa.example.net;

    ssl_certificate     /etc/ssl/certs/aaa.example.net.pem;
    ssl_certificate_key /etc/ssl/private/aaa.example.net.key;
}

server {
    listen 80;

    server_name     bbb.example.net;
}

If a client starts an HTTPS request for bbb.example.net, it will be greeted with a (correct) error/warning: "This certificate is untrusted, wrong domain". This is correct because nginx is serving the aaa.example.net certificate.

What nginx should do is to close the connection as soon as it discovers the domain that is being requested (after reading the SNI data, I suppose). This will communicate to the client and the user that there is HTTPS connectivity on bbb.example.net. Also, this solution will not disclose information about the fact that aaa.example.net is served by the same nginx server.

Change History (17)

comment:1 by Maxim Dounin, 8 years ago

Resolution: worksforme
Status: newclosed

Nobody stops you from configuring nginx in such a way which won't return anything for hosts not explicitly configured (on a listen socket in question) or will return some preconfigure error page. See here for an example.

comment:2 by svario.it/gioele, 8 years ago

Resolution: worksforme
Status: closedreopened

The examples you posted do not address at all the problem I raised.

Before nginx send an error page, the browser has to pass though the SSL handshake. If domain in the certificate does not match the requested domain it will first show the user a big fat warning about insecure connections. Only after the user has accepted the mismatching certificate it will finally show the error page.

The linked page show examples that work for plain HTTP traffic, not HTTPS traffic.

It would be OK if I could do

server {
    listen 80;

    server_name     bbb.example.net;
}

server {
    listen 443 ssl;

    server_name     bbb.example.net;

    refuse_connections;
}

This possibility does not currently exist, this is what I was requesting.

comment:3 by Maxim Dounin, 8 years ago

Status: reopenedaccepted

Ah, sorry, I see now what you are trying to achieve. This might indeed make sense as it might result in less scary messages in some cases.

comment:4 by Maxim Dounin, 8 years ago

See also #214, which is somewhat related.

comment:5 by ghprince@…, 5 years ago

Hi, it's been 3 years, any update on this?

comment:6 by Maxim Dounin, 5 years ago

sensitive: 0

Readily available workaround is to use unsatisfiable cipher specification. E.g., something like this will work:

server {
    listen 443 ssl;
    server_name bbb.example.com;
    ssl_ciphers aNULL;
    ssl_certificate /path/to/dummy.crt;
    ssl_certificate_key /path/to/dummy.key;
    return 444;
}

Given the workaround available, it's not a surprise that there are no developers willing to work on this. At least no one tried to submit a patch. Probably this ticket should be closed.

comment:7 by jonashaag@…, 5 years ago

I want to work on this, but I'm not familiar with the nginx code base or OpenSSL in general. My first attempt on finding the correct place to insert the new check lead me to ngx_http_ssl_servername; more specifically, I thought to insert the check here:

if (ngx_http_find_virtual_server(c, hc->addr_conf->virtual_names, &host,
                                 NULL, &cscf)
    != NGX_OK)
{
    return SSL_TLSEXT_ERR_NOACK;
}

/* HERE */

I decided to go for this place because we need SNI information to safely reject such 'wrong' SSL connection. (Is that assumption even correct?)

Turns out that in this place the routine always returns with SSL_TLSEXT_ERR_NOACK, but "the HTTPS connection still works" as I'm seeing the document I'm accessing in the browser. Doesn't SSL_TLSEXT_ERR_NOACK mean that we want to close the SSL connection? Why does it continue?

Could someone point me to the right place(s) where the new checks have to be inserted, and maybe even provide me with a very rough sketch of how that would look like?

comment:8 by BertrandBordage@…, 4 years ago

Is this reasonable to use the snakeoil certificate generated by openssl as a workaround?
Something like this (paths found on Ubuntu 16.04):

server {
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;
  ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
  ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
  return 444;
}

in reply to:  8 comment:9 by Maxim Dounin, 4 years ago

Replying to BertrandBordage@…:

Is this reasonable to use the snakeoil certificate generated by openssl as a workaround?

Use ssl_ciphers aNULL; as suggested in comment:6, it works fine. Just an incorrect certificate will result in a warning in the browser due to incorrect certificate.

comment:10 by Maxim Dounin, 2 years ago

See also #1571.

comment:11 by nh2@…, 2 years ago

I came here to mention that the described workaround

server {
    listen 443 ssl;
    server_name bbb.example.com;
    ssl_ciphers aNULL;
    ssl_certificate /path/to/dummy.crt;
    ssl_certificate_key /path/to/dummy.key;
    return 444;
}

can be problematic for HTTPS clients that don't support server name indication (SNI).

Notably, nginx's own HTTP client used for proxy_pass does not support SNI by default unless you enable proxy_ssl_server_name on; (docs).

So if you point a default proxy_pass at an HTTPS server that uses the above workaround, then that proxying will fail immediately with ssl3_get_client_hello:no shared cipher.

This is because if the client doesn't support SNI, the two sides have to establish the SSL connection before the Host field is sent to nginx which matches it against server_name.

A workaround is to enable the mentioned (for nginx's own HTTP client) is as mentioned

    proxy_pass https://myrealdomain.../
    proxy_ssl_server_name on;

but other TLS clients without SNI support will still have this problem.

And as per the the the issue description

after reading the SNI data, I suppose

it's not only the workaround that's problematic, but the "real" solution would suffer from the same problem.


This is not to discourage anybody from working on this issue; I think giving nginx a clean way to shut down SNI with something refuse_connections; can make for better error messages in case SNI is supported (as most clients do support it).

I just wanted to leave this info here for others that, like me, mistakenly thought that this (or the workaround) might work for all (including non-SNI) clients; in retrospect it's quite obvious that it cannot.

(I also wonder why proxy_ssl_server_name isn't on by default.)

comment:12 by Maxim Dounin, 2 years ago

can be problematic for HTTPS clients that don't support server name indication (SNI).

This depends on how you've configured things. If you've used ssl_ciphers aNULL; in the default server block, this will prevent non-SNI clients from connecting. If you've used it in a name-based virtual server, it will only block SNI-enabled connections to the server in question.

comment:13 by Maxim Dounin, 2 years ago

See also #1612.

comment:14 by JemmyLoveJenny@…, 2 years ago

Try this patch. You needn't set HTTPS default_server in the config file.

+diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c
--- a/src/http/ngx_http_request.c
+++ b/src/http/ngx_http_request.c
@@ -882,7 +882,7 @@
     servername = SSL_get_servername(ssl_conn, TLSEXT_NAMETYPE_host_name);
 
     if (servername == NULL) {
-        return SSL_TLSEXT_ERR_NOACK;
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
     }
 
     c = ngx_ssl_get_connection(ssl_conn);
@@ -897,7 +897,7 @@
     host.len = ngx_strlen(servername);
 
     if (host.len == 0) {
-        return SSL_TLSEXT_ERR_NOACK;
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
     }
 

     host.data = (u_char *) servername;
@@ -912,7 +912,7 @@
                                      NULL, &cscf)
         != NGX_OK)
     {
-        return SSL_TLSEXT_ERR_NOACK;
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
     }
 
     hc->ssl_servername = ngx_palloc(c->pool, sizeof(ngx_str_t));

comment:15 by uschindler@…, 9 months ago

The hack with aNULL or eNULL no longer work as soon as TLS1.3 is enabled, because TLS1.3 does not support that CIPHER. Clients successfully connect and exchange cert bringing browser warning.

So strict SNI should really be implemented. It's just a few if statements, is this too much for a long-standing issue?

comment:16 by Maxim Dounin <mdounin@…>, 5 weeks ago

In 7732:59e1c73fe02b/nginx:

SSL: ssl_reject_handshake directive (ticket #195).

In some cases it might be needed to reject SSL handshake based on SNI
server name provided, for example, to make sure an invalid certificate
is not returned to clients trying to contact a name-based virtual server
without SSL configured. Previously, a "ssl_ciphers aNULL;" was used for
this. This workaround, however, is not compatible with TLSv1.3, in
particular, when using BoringSSL, where it is not possible to configure
TLSv1.3 ciphers at all.

With this change, the ssl_reject_handshake directive is introduced,
which instructs nginx to reject SSL handshakes with an "unrecognized_name"
alert in a particular server block.

For example, to reject handshake with names other than example.com,
one can use the following configuration:

server {

listen 443 ssl;
ssl_reject_handshake on;

}

server {

listen 443 ssl;
server_name example.com;
ssl_certificate example.com.crt;
ssl_certificate_key example.com.key;

}

The following configuration can be used to reject all SSL handshakes
without SNI server name provided:

server {

listen 443 ssl;
ssl_reject_handshake on;

}

server {

listen 443 ssl;
server_name ~;
ssl_certificate example.crt;
ssl_certificate_key example.key;

}

Additionally, the ssl_reject_handshake directive makes configuring
certificates for the default server block optional. If no certificates
are configured in the default server for a given listening socket,
certificates must be defined in all non-default server blocks with
the listening socket in question.

comment:17 by Maxim Dounin, 5 weeks ago

Resolution: fixed
Status: acceptedclosed

The ssl_reject_handshake directive was introduced to address this. Thanks to all involved.

Note: See TracTickets for help on using tickets.