NGINX, Ghost, and hot-linked images

Wouldn't you know, I just got this site up and running again, and already somebody has hot-linked an image.

I'm new to using NGINX, and still haven't quite grokked it's flow, so I had to do a bit of digging into things. Trouble is, all of the examples seem to be geared towards NGINX directly serving content from the file system, rather than forwarding requests off to a proxy (as is the case with running Ghost on this site). Undeterred, I think I've figured out a solution. In order to illustrate (and possibly save you some time if you run a similar setup), here's my NGINX site file (edited to use www.example.com) for Ghost:

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

    server_name www.example.com;

    ssl_certificate /etc/ssl/certs/ssl-cert-chain-www-example-com.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-www-example-com.key;
    ssl_prefer_server_ciphers on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS;
    ssl_ecdh_curve secp384r1;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/ssl-ca-certs-startssl.pem;

    keepalive_timeout 70;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";

    # force TLS
    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
    }

    root /usr/share/nginx/html;
    index index.html;

    # prevent hotlinking
    # test http://www.htaccesstools.com/test-hotlink-protection/
    location ~* \.(gif|png|jpe?g)$ {
        valid_referers none blocked www.example.com;
        if ($invalid_referer) {
            return 444;
        }
        try_files $uri @ghost;
    }

    # try to get files from $root, otherwise redirect to Ghost
    location / {
        try_files $uri @ghost;
    }

    # proxy to Ghost
    location @ghost {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:2368;
    }
}

Let's walk through this file:

  • The TLS section upgrades all connections to use SSL, and caches verification requests (not related at all for preventing hot-linking, but handy as a reference to get TLS working).
  • I set the root directory to allow files on the filesystem to be directly served. At the moment, I don't actually serve any files, but handy if I need to throw something in there and bypass the proxy.
  • The anti-hot-link prevention I have in place will try to (case insensitive) match any file that ends in gif, png, jpg or jpeg. If it gets a hit, then it proceeds to compare the web page that is requesting it to see if originated here (i.e., www.example.com). If not, then $invalid_referer is set to 1 (otherwise it's blank).
  • If $invalid_referer is not blank (i.e., somebody is hot-linking the file), then return HTTP error code 444 (which is an NGINX extension that just closes the socket without sending any data).
  • Here's where I needed to get a little creative. How NGINX works is that it will execute location checks from most specific to least specific. In other words, if an URL matches the hot-link prevention, it will run what's in the location block, and then finish the response processing. The try_files $uri @ghost directive will take over, and replicate what the root location directive does (forward to the proxy). All the examples on the interwebs assume that you're serving up real files, and not proxying. Stumped me for a little bit until I looked at the logs... d'oh!
  • And what is the root location directive doing? It's checking to see if a file exists, and if not, forward the request to the Ghost proxy.
  • Finally, the Ghost proxy definition. I'll leave that as an exercise for the reader to figure out.

Non-responsive kitty If you want to hot-link the above cat, check out Status Cats. Don't hot-link from here... 😏

Now for the fun part: testing to see if this is truly stopping hot-links. First things first, make sure you haven't broken normal use. Go to some page with images, and hit refresh a couple of times. Things should look normal. If all is good, pay a visit to a hot-link tester like the one at .htaccess Tools. Stick in an URL for one of your images, and see what happens.

As for possible modifications, you can always serve up one of those annoying "don't hot-link me" images. I'll leave you to find that yourself. The more important modification would be to block other media types (like PDF or MP3/4 files, or even JavaScript files - I doubt you want to be a CDN). That's easy enough: just add their file extensions to the list.

One other thing to think about: this setup will also block "legitimate" hot-linking (i.e., image spiders). If you want your media to be indexed by Google, Bing, etc., you'll want to modify the valid_referers line to include them. An example would be:

valid_referers none blocked www.example.com ~\.google\. ~\.yahoo\. ~\.bing\. ~\.facebook\. ~\.fbcdn\.;  

Waste of bandwidth? Um... Sure... Enjoy a cup of irony with your anti-hot-linking image substitution.

Update 2015/09/20: I've now moved this to a Docker container. Details in the Moving to Docker: NGINX reverse proxy with SSL termination article.