Nginx Hotlink Protection: valid_referers and secure_link Against Resource Theft
Hotlinked images, video, and downloads burn bandwidth. Two Nginx anti-hotlink designs: valid_referers source checks and secure_link time-limited signed URLs. Full config, curl verification, and false-positive troubleshooting for static-heavy sites.
Images, video, and download files referenced directly by third-party sites consume origin bandwidth for free. That is classic hotlinking. This article covers two core Nginx anti-hotlink designs: valid_referers source checks and the secure_link module for time-limited signed URLs. Full config, verification, and troubleshooting. Aimed at static-heavy sites.
Applicable Scenarios
- Site images, audio/video, archives and other static assets are frequently referenced by third-party sites; bandwidth cost spikes
- Download / asset sites that must stop resource URLs from being bulk-reposted to forums and drive-index sites
- CDN-distributed assets where you want origin-side intercept of illegal sources to cut origin pull
- Resource URLs that must expire so links cannot spread indefinitely
Prerequisites
- Nginx 1.10+ (secure_link is compiled in with the source build; no extra install)
- Edit and reload rights on the site server config
- A test static file, e.g. `/data/www/static/demo.jpg`
How It Works
Anti-hotlink is verifying that the request comes from an authorized context. Nginx provides two layers:
Option 1: Referer check (`valid_referers`). Browsers send a Referer header identifying the source page. Nginx declares legal sources with `valid_referers`; requests outside the allowlist set `$invalid_referer` to 1, and `if` returns 403. Simple to deploy. Referer can be forged. Baseline protection only.
Option 2: Signed URL (`secure_link`). Backend concatenates resource path, secret, and timestamp per a shared algorithm and emits an MD5 signature. The client must present that signature. Nginx recomputes with the same secret and compares. Mismatch or expiry is denied. Signatures cannot be forged. Strict anti-leech. Fits VOD, downloads, and other high-value assets.
Procedure
Option 1: valid_referers
Edit the server block. Add Referer checks in the static location:
server {
listen 80;
server_name example.com;
location ~* \.(jpg|jpeg|png|gif|mp4|zip)$ {
valid_referers none blocked server_names
*.example.com example.com
*.baidu.com *.bing.com;
if ($invalid_referer) {
return 403;
}
root /data/www/static;
expires 7d;
}
}
Parameters: none allows requests with no Referer (address-bar open, in-app WebView are common); blocked allows requests whose Referer was stripped by a firewall or proxy; server_names allows this server's server_name; wildcard `*.example.com` covers all subdomains. Whether to allow search-engine domains is a business call; allowing them avoids broken images in search results.
Option 2: secure_link signed URLs
Step 1. Define signature verification in Nginx:
location /download/ {
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri MY_SECRET_KEY";
if ($secure_link = "") {
return 403; # missing or mismatched signature
}
if ($secure_link = "0") {
return 410; # signature valid but expired
}
alias /data/www/files/;
}
Step 2. Backend generates the signed link. PHP example:
<?php
$secret = 'MY_SECRET_KEY';
$expires = time() + 300; // 5 minutes validity
$uri = '/download/report-2026.zip';
$md5 = base64_encode(md5($expires . $uri . $secret, true));
$md5 = strtr($md5, '+/', '-_');
$md5 = rtrim($md5, '=');
$url = $uri . '?md5=' . $md5 . '&expires=' . $expires;
echo 'https://example.com' . $url;
Key points: `secure_link_md5` concatenation order must match the generator exactly. Signature uses base64 URL variant (`-_` replaces `+ /`); trailing `=` stripped. `$uri` is the decoded request path; if the path has Chinese or spaces, confirm encoding matches on both ends.
Verification
Option 1: `nginx -t` then `nginx -s reload`. Simulate sources:
curl -I -H "Referer: https://evil.com/" https://example.com/demo.jpg
# expect 403
curl -I https://example.com/demo.jpg
# expect 200 (none allows empty Referer)
Option 2: correct signature returns 200; flip any md5 character returns 403; set expires to a past timestamp returns 410. Wrap the checks in a script and cron them so a later config change cannot silently break anti-hotlink.
FAQ
FAQ 1: After Referer checks, images fail to load in WeChat and some apps?
Some clients send no Referer. If the config omits `none`, those requests are false-positived. Keep `none blocked` in valid_referers, or switch to secure_link for precise control.
FAQ 2: secure_link always returns 403. How to debug?
90% is concatenation order or encoding. Check order: `secure_link_md5` variable order matches the generator; secret is identical on both ends (watch trailing spaces); base64 used raw output plus URL-safe replace; set error_log to `notice` to see the secure_link module verification path.
Summary
valid_referers is a low-cost first line for site-wide static assets; it can be forged. secure_link enforces strict anti-leech with time-limited signatures; use it for downloads, VOD, and other core assets. Combine for layered protection. After rollout, watch access logs for false positives and tighten the allowlist gradually.