Http request not caught by nginx server block?

I have an nginx server with exactly one site configuration (the default site).

I keep seeing the following lines in my access.log

xxx.xxx.xxx.xxx - - [10/Jul/2018:16:32:14 +0200] "GET / HTTP/1.1" 400 37 "-" "-"

I know for a fact that this happens if the request doesn't send a Host:header, as in curl -v -H "Host:" https://{{ server_ip }}

root@mypc:~# curl -v -H "Host:" http://{{ server_ip }}
* Rebuilt URL to: http://{{ server_ip }}/
*   Trying {{ server_ip }}...
* Connected to {{ server_ip }} ({{ server_ip }}) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx/1.14.0 (Ubuntu)
< Date: Wed, 11 Jul 2018 08:14:07 GMT
< Content-Type: text/html
< Content-Length: 182
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.14.0 (Ubuntu)</center>
</body>
</html>
* Closing connection 0

I did configure my site to respond to requests with no Host: header with an 444 error code. Obviously, this doesn't work. I assume I did misunderstand the docs and my config is incorrect. I would be glad if someone could point out what's missing:

ssl_certificate /etc/letsencrypt/live/{{ hostname_fqdn }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ hostname_fqdn }}/privkey.pem;

# Respond to requests without a host header with HTTP status 444
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name "";
    return 444;
}

server {
    listen 80;
    listen [::]:80;

    server_name {{ hostname_fqdn }};

    if ($allowed_country = no) {
        return 444;
    }

    return 301 https://{{ hostname_fqdn }}$request_uri;
}

server {
    listen 443 ssl;
    #listen [::]:443 ssl;

    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    server_name {{ hostname_fqdn }};

    if ($allowed_country = no) {
        return 444;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/app/{{ django_project_name }}/socket;
    }

    location /static/ {
        root {{ django_static_root }};
    }
}

server {
    listen 80;
    listen [::]:80;

    listen 443 ssl;
    #listen [::]:443 ssl;

    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    server_name api.zeitschrift.hausbesitzerverlag.de;

    if ($allowed_country = no) {
        return 444;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/app/{{ django_project_name }}/socket;
    }

    location /pmx-api/ {
        auth_basic "Restricted Content";
        auth_basic_user_file /etc/nginx/htpasswd;

        limit_req zone=api burst=5;

        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/app/{{ django_project_name }}/socket;
    }

    location /komtrigon-api/ {
        limit_req zone=api burst=5;

        include uwsgi_params;
        uwsgi_pass unix:/run/uwsgi/app/{{ django_project_name }}/socket;
    }

    location /static/ {
        root {{ django_static_root }};
    }
}

Solution 1:

HTTP requests without a Host: header are not valid in HTTP/1.1 (or later), and nginx is correctly sending a 400 Bad Request error without even attempting to serve it with any server block. From RFC 7230 section 5.4:

A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field or a Host header field with an invalid field-value.

Your server block which serves requests without a Host: header will only be hit when a request comes in with the ancient HTTP/1.0. You can simulate this using curl with the -0 command line option:

curl -v -0 http://203.0.113.87/

From the man page:

       -0, --http1.0
              (HTTP)  Tells  curl to use HTTP version 1.0 instead of using its
              internally preferred HTTP version.