21

I have a web server with many virtual servers. Only 1 of which is SSL. The problem is, because there is no catchall server block listening for SSL, any https request to the other sites is served by the 1 SSL block.

My configuration, essentially, looks like this:

# the catch all
server {
  listen 80 default;

  # I could add this, but since I have no default cert, I cannot enable SSL,
  # and this listen ends up doing nothing (apparently).
  # listen 443; 

  server_name _;
  # ...
}

# some server
server {
  listen 80;
  server_name server1.com;
  # ...
}

# some other server ...
server {
  listen 80;
  server_name server2.com;
  # ...
}

# ... and it's https equivalent
server {
  listen 443;
  ssl on;
  server_name server2.com;
  # ...
}

Now as there's no default listener for 443, a request like https://server1.com will end up being served by the server2.com https block. This follows the logic for server_name in the docs.

If there is no match, a server { ... } block in the configuration file will be used based on the following order:

  1. the server block with a matching listen directive marked as [default|default_server]
  2. the first server block with a matching listen directive (or implicit listen 80;)

What is the preferred solution for this problem? Do I need to set up dummy cert for my catch all server block just so I can listen on 443 and handle the bad requests? Is there a parameter I'm unaware of that forces an exact hostname match with server?

numbers1311407
  • 333
  • 3
  • 10

7 Answers7

9

Ideally either I'd like nginx to not serve https at all unless the hostname matches, or for it to redirect to http at the same host.

Neither is possible. The connection from a client that goes to https://foo.example.com/ cannot be accepted by anything but an SSL certificate with "foo.example.com" as one of its names. There is no opportunity to redirect until the SSL connection is accepted.

If you configure each site for SSL, a user who clicks through the certificate error will get the site they requested. If you configure a "catch all" site for SSL that provides only an error page and configure name-based virtual hosting for the one site that is supposed to support SSL, you can serve an error page to clients.

SSL and HTTP virtual hosting just don't play nicely together.

David Schwartz
  • 31,449
  • 2
  • 55
  • 84
  • 1
    This is what I gathered after reading the docs. I was just hoping I'd missed something. I don't care about SSL warnings at all. I just don't want someone to enter https://server1.com and find themselves looking at the homepage of server2.com... Is there truly no way to tell nginx to **not** accept a request? – numbers1311407 Oct 27 '11 at 23:03
  • If it doesn't accept the request, the first site won't work. It has to accept the request to find out what site the user is trying to access. – David Schwartz Oct 27 '11 at 23:06
  • 3
    "The connection from a client that goes to https://foo.example.com/ cannot be accepted by anything but an SSL certificate with "foo.example.com" as one of its names." - This is not correct, the server will accept the request and it is up to the client to verify that the requested DN matches the server certificate DN. – ColinM May 02 '18 at 17:39
6

The only way to do is to create a self-signed SSL certificate and use it to gain control on incoming https requests. You can create your self-signed SSL certificate in a few simple steps outlined in this post.

Let's say you create a self-signed certificate with a filename of server.crt. You would then append the following in your nginx configuration:

server {
    listen  443;

    ssl    on;
    ssl_certificate         /etc/nginx/ssl/server.crt;
    ssl_certificate_key     /etc/nginx/ssl/server.key;

    server_name server1.com;

    keepalive_timeout 60;
    rewrite ^       http://$server_name$request_uri? permanent;
}

You will still get the browser SSL warning message, but at least you'll have control over what happens next.

user1973679
  • 161
  • 1
  • 1
  • 2
    I agree with this. There's the problem of the browser showing a warning message about untrusted certificates, but if you're only trying to prevent users going to https:// to get serverd an equally invalid certificate for one of your real vhosts (invalid because the hostname won't match), then you're better off serving them an invalid self-signed dummy certificate. That kind of tells them "there's nothing to see here, not even a certificate from another host". – Daniel F Jun 09 '16 at 21:32
3

This is how I solved the problem:

  1. Create self-signed certificate:

openssl req -nodes -x509 -newkey rsa:4096 -keyout self_key.pem -out self_cert.pem -days 3650

  1. Copy it where NginX can find it:

cp self*.pem /etc/nginx/ssl/

  1. Set up a catch-all route:
server {
    listen 443 default_server ssl;

    ssl on;
    ssl_certificate /etc/nginx/ssl/self_cert.pem;
    ssl_certificate_key /etc/nginx/ssl/self_key.pem;

    return 301 http://$host;
}

What this will do: it will give you a warning (no way around it) on any server that doesn't have its own certificate, but the warning won't say the wrong certificate name. If user clicks "visit anyway" they'll be redirected to the non-ssl version of the site they've typed.

caveat:

if your SLL-enabled site only defines www.example.com (and not example.com) then your catch-all route will end up serving https://example.com with the self-signed certificate and corresponding warning.

ierdna
  • 131
  • 4
2

Add a catch-all server block and return status code 444. It tells nginx to close the connection before sending any data.

server {
    listen 443 default_server ssl;
    server_name _;
    return 444;
}
ATLief
  • 306
  • 2
  • 12
1

These days you can use the TLS Server Name Indication extension (SNI, RFC 6066). The HTTPS listener will be able to recognize the domain name before serving the appropriate certificate.

This means that you will need to have certificates for ALL your domains, and when SNI is used to recognize one of the other domains you can just use HTTP 301 redirect to the HTTP non-encrypted version unless the server name matches the single one which needs encryption.

More information about SNI available in nginx documentation http://nginx.org/en/docs/http/configuring_https_servers.html

Evgeny
  • 599
  • 5
  • 10
1

Map the requested hostname to valid hostnames in the http {} block:

map $ssl_server_name $correct_hostname_example {
  default 0;
  example.com 1;
  www.example.com 1;
}

And then in the server {} block kill connections with the wrong hostname:

if ($correct_hostname_example = 0) {
  return 444;
}

Use multiple maps as necessary for multiple server blocks. The connection will still be established using one of your certificates but if this last block is present in every server block that serves SSL then effectively you will "block" connections with invalid hostnames. It may only be necessary in the first server block but adding it to every server block will ensure that order does not matter.

The $ssl_server_name variable is present in nginx 1.7 or greater.

ColinM
  • 701
  • 8
  • 19
-2

Redirect to http:

server {
    listen       443;
    server_name  *.com;
    rewrite        ^ http://$host$request_uri? permanent;
}    

Return 404:

server {
    listen       443;
    server_name  *.com;
    return 404;
}    
freestyler
  • 138
  • 4
  • 1
    That will still result in an SSL warning, since the SSL tunnel needs to be established happens before any HTTP redirect will occur. See David Schwartz's accepted answer. – cjc Feb 02 '12 at 12:47