Nginx – Securing Nginx proxy

http-proxynginxPROXYSecurity

I'm using Nginx as a proxy for a Java web service.

My config looks like this:

location /webservice {
    proxy_read_timeout 240;
    proxy_connect_timeout 240;
    proxy_pass      http://127.0.0.1:8080/;
}

In my logs I'm seeing a lot of entries like this:

xx.xx.xx.xx - - [18/Oct/2011:02:44:23 +0000] "GET http://l04.member.in2.yahoo.com/config/login?login=email@example.com&passwd=password HTTP/1.0" 200 9 "-" "Mozilla/4.0 (compatible; MSIE 5.0; Series60/2.8 Nokia6630/4.06.0 Profile/MIDP-2.0 Configuration/CLDC-1.1)"

I've done some testing, as far as I can see my proxy isn't forwarding requests to external sites, but I'd like to block these requests all together and/or return a status code other than 200.

I've done this:

if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; }

which blocks CONNECT attempts. Any ideas (beyond IP blocking) would be appreciated.

Best Answer

Nginx will accept connections and process them based on matching the server_name (which checks against the Host header). Nginx comes with a default server block configured to match all Hosts. This allows for processing of any request that comes to the server.

I like to setup a server block checking for an empty Host header, and also configure the default server to return a 403 error (e.g. if you try to access my server via its IP address). Each virtual host then gets its own configuration (i.e. any valid host matches a configuration, all others either hit the default server block or the empty host block).

Server for checking empty host:

server {
    listen       80;
    server_name  "";
    return       444;
}

Server for throwing a 403 on all not configured hosts:

server {
        listen       80;
        server_name  _;
        root /path/to/error/files;
        error_page 403 /403.html;
                location  /403.html {
                allow all;
        }
        deny all;
}

It should be noted that the listen directive isn't necessary above (nginx listens on port 80 by default) - but my nginx running behind varnish, so doesn't actually listen on port 80.

In your case, you will add a 3rd server that will handle your reverse proxy requests:

server {
server_name mydomain.com;
...your other blocks...
}

You can test your config in a variety of ways (I am sure there are more, but these come to mind at the moment):

(I am using google.com as my test domain below, change it to your site of choice):

Specify the entire request in one go:

telnet mydomain.com 80
GET http://google.com

Specify the request and host header separately:

telnet mydomain.com 80
GET / HTTP/1.1
Host: google.com

Setup an entry in your hosts file (on your server):

127.0.0.1 google.com

Use curl to try and fetch a page:

curl google.com

(In this case, the hosts file is telling your server that google.com can be found on your machine - which gets the request to nginx - remove the entry when done testing.)

Edit: It appears that the unintended consequence of the above is that invalid requests result in a 400 error. You can determine the root cause of this, if interested, by adding the 'info' parameter to your error_log directive. In my case, the following causes were associated with the 400 errors that I saw:

Using telnet with the one-line GET request (and no host header):

client sent invalid request while reading client request line

Random requests (non-standard) made:

client sent invalid method while reading client request line

Using telnet, waited too long:

client timed out (110: Connection timed out) while reading client request headers

Other common causes were:

client sent invalid host header while reading client request headers
recv() failed (104: Connection reset by peer) while reading client request line
client closed prematurely connection while reading client request line

Using curl produced the expected 444 error. I imagine there is some additional syntax to a valid request. At any rate, the 400 errors are processed before the 444s from my understanding, so it is likely that these will not go away for truly invalid requests.

I was able to successfully get a 444 error using telnet though, it required modifying my config a bit:

server {
    listen       80 default;
    server_name  _ "";
    return       444;
}

Note that in the above, the 'unspecified server name' (underscore) and blank host (double quotes) do not explicitly define the default server, so you must add 'default' to the listen line.

Telnet output:

telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Host: google.com

Access log output:

127.0.0.1 - - [19/Oct/2011:00:51:16 -0400] "GET / HTTP/1.1" 444 0 "-" "-" "-"