Nginx Geo-Blocking: Allow Only Mainland China IPs
This guide shows how to configure Nginx to allow only mainland China IPv4/IPv6 addresses using the geo module, with automated IP list fetching from ipdeny.com, CDN real-IP handling, testing procedures, and safe update practices.
Prepare IPv4 and IPv6 Whitelists
Nginx's geo directive assigns values based on IP ranges. For example, 1.0.1.0/24 marks a CIDR block; addresses inside get value 1. The standard Nginx package includes this module.
Save the following script as make-cn-geo.sh. With curl and Python 3 installed, run sudo bash make-cn-geo.sh. The script downloads two aggregated zone files from ipdeny.com, validates format and address type, then generates /etc/nginx/geo/cn.conf. On download failure, empty list, or invalid format, the previous file is retained.
#!/usr/bin/env bash
set -euo pipefail
dir=/etc/nginx/geo
install -d -m 0755 "$dir"
tmp=$(mktemp -d "$dir/.cn.XXXXXX")
trap 'rm -rf -- "$tmp"' EXIT
curl -fsSL --retry 2 --max-time 30 \
https://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone \
-o "$tmp/v4"
curl -fsSL --retry 2 --max-time 30 \
https://www.ipdeny.com/ipv6/ipaddresses/aggregated/cn-aggregated.zone \
-o "$tmp/v6"
python3 - "$tmp/v4" "$tmp/v6" > "$tmp/cn.conf" <<'PY'
import sys
from ipaddress import ip_network
from pathlib import Path
for filename, version in zip(sys.argv[1:], (4, 6)):
lines = Path(filename).read_text(encoding="ascii").splitlines()
nets = {ip_network(line.strip()) for line in lines if line.strip()}
if not nets or any(net.version != version or net.prefixlen == 0 for net in nets):
raise SystemExit(f"Invalid or empty IPv{version} list")
for net in sorted(nets):
print(f"{net} 1;")
PY
chmod 0644 "$tmp/cn.conf"
mv -fT -- "$tmp/cn.conf" "$dir/cn.conf"IPv6 must be handled together. If the site serves IPv6, missing IPv6 ranges would cause legitimate mainland IPv6 users to receive 403. Even if only IPv4 is used now, keep the dual-stack list so it works when IPv6 is enabled later.
Add Blocking Rules to Target Site
Place the following geo block inside the existing http {} in nginx.conf. If your http includes /etc/nginx/conf.d/*.conf, you can store this as /etc/nginx/conf.d/geo-cn.conf. default 0 covers all unmatched addresses.
geo $site_ip_allowed {
default 0;
include /etc/nginx/geo/cn.conf;
}Then add the following check inside the target server {}, before any existing rewrite/redirect rules. Apply only to sites that need restriction. If a domain serves both HTTP and HTTPS, check both entry points. For panel- or container-managed Nginx, run nginx -T in the actual runtime environment to locate the active server_name and config file.
if ($site_ip_allowed = 0) {
return 403;
}The rule at the server level checks homepage, APIs, and static assets. Before going live, verify health checks, third-party payment callbacks, and certificate renewals still work. If using HTTP-01 validation, geo-blocking may interfere; switch to DNS-01 instead.
Test on a staging domain first: homepage, static files, APIs. Separate subdomain APIs need individual verification. For third-party callbacks from abroad, allow their fixed egress IPs in the geo block and keep the API's own signature verification.
Behind a CDN: Restore Real Client IP First
Direct access gives $remote_addr as the address connected to Nginx. With a CDN or load balancer, that address belongs to the proxy. Feeding the proxy address to the whitelist makes all traffic through that node get the same result.
Run nginx -V to confirm http_realip module is enabled. For Cloudflare, configure inside the same server: set real_ip_header and add actual origin-pull CIDRs with set_real_ip_from. The two ranges below are format examples only.
# Both ranges are examples; replace with CDN's actual origin-pull CIDRs
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 2001:db8::/32;
real_ip_header CF-Connecting-IP;For other CDNs, use the header they actually send. Only trust controlled proxy CIDRs because clients can forge headers. Log both $remote_addr and $realip_remote_addr to compare client address and original peer. Update the trusted list and origin security groups together when origin-pull CIDRs change. If Cloudflare's Pseudo IPv4 is set to Overwrite Headers, it rewrites the client header to a pseudo IPv4; for dual-stack checks, disable that mode or switch to Add Header.
When CDN caches a hit, the request may return from the edge. To restrict cached content, add a geo rule in the CDN's WAF. In Cloudflare, create a custom rule with expression http.host eq "www.example.com" and ip.src.country ne "CN", replace the domain, and choose Block. If all traffic goes through CDN, restrict the origin security group to accept only CDN origin-pull and required business sources.
After Reload, Test from Both Sides
Run nginx -t; on success, reload. Without systemd, use sudo nginx -s reload after a passing test.
sudo nginx -t && sudo systemctl reload nginxReplace the domain below with yours. Send one request from mainland China and one from abroad, using a page that previously worked. Mainland should return the original status code; abroad should return 403. If the domain supports IPv6, test both with -6. If the test machine uses an HTTP proxy, add --noproxy "*" to bypass curl's proxy and ensure the correct egress.
curl -4sS -o /dev/null -w '%{http_code}
' \
https://www.example.com/
curl -6sS -o /dev/null -w '%{http_code}
' \
https://www.example.com/Local Nginx test. Trusted proxy feeds sample IPs; whitelist hits return 200, others return 403.
If both sides return 403, define a trace log format in http:
log_format geo_trace '$remote_addr $site_ip_allowed $status';then add access_log /var/log/nginx/geo.log geo_trace; to the target server. If the log shows the whitelist variable as 1, check the application's own access control. If the origin has no matching log, check the CDN's block records first.
When Updating Ranges, Keep the Old List
IP range ownership changes; proxies or VPNs alter user egress. When legitimate access is blocked, first confirm the actual egress IP in the logs. To allow a fixed office egress, add a line in the geo block, e.g., 203.0.113.10/32 1; (replace with real public egress). Keep exceptions in the main config so they persist across list regenerations.
Before periodic refresh, back up the last verified cn.conf. Run the script, then verify and reload in the same order. If the new list causes false blocks, restore the backup and reload. To remove geo-restriction, delete the blocking check from the target site, then test and reload. Remember: restricting by egress region does not replace authentication for admin login and sensitive endpoints.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
