How to Implement IP-Based Access Control Using X-Forwarded-For Headers in Nginx on Kubernetes


1 views

When running Nginx in containerized environments like Kubernetes on GCP, client IP addresses are typically only available through the X-Forwarded-For (XFF) header. While basic IP filtering works for individual addresses, managing CIDR ranges requires a more sophisticated approach.

The XFF header often contains multiple IPs in a comma-separated format (client, proxy1, proxy2). The leftmost non-internal IP is typically the original client. For GCP environments, the format usually appears as:

X-Forwarded-For: client_ip, load_balancer_ip

Nginx's geo module provides the most efficient way to handle IP ranges. Here's a complete implementation:

geo $client_ip {
    default         0;
    123.233.233.0/24 1;
    192.168.1.0/24   1;
    10.0.0.0/8       0; # Deny internal ranges
    include /etc/nginx/conf.d/ip-ranges.conf;
}

map $http_x_forwarded_for $real_client_ip {
    ~^([^,]+) $1;
    default  $remote_addr;
}

server {
    listen 80;
    
    set $allow false;
    if ($client_ip = 1) {
        set $allow true;
    }
    
    if ($allow = false) {
        return 403;
    }
    
    # Your regular configuration
}

For more complex scenarios, you can use regex patterns with the XFF header:

set $block_me 0;

# Block known bad actors
if ($http_x_forwarded_for ~* "(123\.45\.67\.[0-9]+|evilbot\.com)") {
    set $block_me 1;
}

# Allow only from specific countries
geoip_country /etc/nginx/geoip/GeoIP.dat;
if ($geoip_country_code != US) {
    set $block_me 1;
}

In GKE environments, you'll need to account for:

  • Cloud Load Balancer IPs in the XFF chain
  • Pod-to-pod communication IPs
  • GCP health check IP ranges

Here's a GKE-optimized configuration snippet:

map $http_x_forwarded_for $forwarded_client_ip {
    ~^(?[^,]+),?.*$ $client;
}

geo $valid_client {
    default 0;
    # Include your allowed CIDR ranges
    include /etc/nginx/allow-ranges.conf;
    # Exclude GCP internal IPs
    10.128.0.0/20 0;
    35.191.0.0/16 0;
    130.211.0.0/22 0;
}

When running Nginx in a Kubernetes cluster on GCP, client IP addresses are typically passed through the X-Forwarded-For header rather than appearing in the standard remote address variable. While simple IP-based restrictions work for individual addresses, managing entire IP ranges requires a more sophisticated approach.

The X-Forwarded-For header often contains comma-separated IP addresses, with the original client IP being the leftmost entry:

X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip

Here's a complete Nginx configuration snippet that implements CIDR-based IP filtering:

geo $real_client_ip {
    default 0;
    192.168.1.0/24 1;
    10.0.0.0/8 1;
    172.16.0.0/12 1;
    # Add more CIDR ranges as needed
}

map $http_x_forwarded_for $real_client_ip {
    default "";
    ~^(?<first_ip>[^,]+) $first_ip;
}

server {
    listen 80;
    
    if ($real_client_ip = "") {
        set $real_client_ip $remote_addr;
    }
    
    if ($real_client_ip ~ "^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") {
        set $block_access 0;
    } else {
        set $block_access 1;
    }
    
    if ($block_access = 1) {
        return 403;
    }
    
    location / {
        # Your normal configuration
    }
}

For more complex scenarios, consider using Nginx's GeoIP module:

load_module modules/ngx_http_geoip_module.so;

http {
    geoip_country /etc/nginx/geoip/GeoIP.dat;
    
    map $http_x_forwarded_for $real_ip {
        ~^(?P<first>[^,]+) $first;
        default $remote_addr;
    }
    
    server {
        if ($geoip_country_code = CN) {
            return 403;
        }
        
        # Additional geo-based rules
    }
}

In complex environments with multiple proxy layers, use this enhanced parsing approach:

map $http_x_forwarded_for $client_ip {
    "~*(?<ip>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(?:,|$)" $ip;
    default $remote_addr;
}

geo $client_ip $allowed_ip {
    ranges;
    default 0;
    10.0.0.0-10.255.255.255 1;
    192.168.0.0-192.168.255.255 1;
    # Add more ranges
}

server {
    if ($allowed_ip = 0) {
        return 403;
    }
}

For large IP range lists, consider these optimizations:

  • Use geo with CIDR notation instead of regex patterns
  • Place frequently matched ranges at the top
  • Consider using separate configuration files for IP ranges
  • Benchmark with nginx -T to test configuration