IP Whitelisting at the API Gateway
In this tutorial, you'll learn about IP Whitelisting. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
IP whitelisting restricts API access to specific IP addresses or ranges, providing an additional security layer by blocking all traffic from unauthorized networks.
What You'll Learn
By the end of this lesson, you will implement IP allow lists, CIDR range matching, geo-blocking, and dynamic IP management at the gateway.
Why It Matters
IP whitelisting prevents unauthorized networks from reaching your API even if authentication credentials are compromised. It is a critical defense-in-depth layer.
Real-World Use
An admin API only allows connections from the corporate VPN CIDR range (10.0.0.0/8) and the office static IPs, blocking all other traffic at the gateway.
IP Filtering Architecture
flowchart TD
Request -->|Check IP| IPCheck{IP Allowed?}
IPCheck -->|Yes| Auth[Authentication]
IPCheck -->|No| Block[403 Forbidden]
Auth -->|Pass| Backend[Backend Service]
CIDR Range Matching
# cidr_matcher.py
import ipaddress
from typing import List, Optional, Union
class CIDRWhitelist:
def __init__(self):
self.allowed_networks: List[ipaddress.IPv4Network] = []
self.allowed_ips: List[ipaddress.IPv4Address] = []
def add_network(self, cidr: str):
self.allowed_networks.append(ipaddress.IPv4Network(cidr))
def add_ip(self, ip: str):
self.allowed_ips.append(ipaddress.IPv4Address(ip))
def is_allowed(self, client_ip: str) -> bool:
try:
ip = ipaddress.IPv4Address(client_ip)
except ValueError:
return False
if ip in self.allowed_ips:
return True
for network in self.allowed_networks:
if ip in network:
return True
return False
def allowed_ranges(self) -> List[str]:
ranges = [str(n) for n in self.allowed_networks]
ranges.extend([str(ip) for ip in self.allowed_ips])
return ranges
whitelist = CIDRWhitelist()
whitelist.add_network("10.0.0.0/8")
whitelist.add_network("192.168.1.0/24")
whitelist.add_ip("203.0.113.42")
test_ips = ["10.1.2.3", "192.168.1.100", "203.0.113.42", "8.8.8.8", "invalid"]
for ip in test_ips:
allowed = whitelist.is_allowed(ip)
print(f"{ip:20s} -> {'ALLOWED' if allowed else 'BLOCKED'}")
Expected output:
10.1.2.3 -> ALLOWED
192.168.1.100 -> ALLOWED
203.0.113.42 -> ALLOWED
8.8.8.8 -> BLOCKED
invalid -> BLOCKED
Geo-Blocking
# geo_blocking.py
from typing import Dict, List, Optional
class GeoBlocker:
def __init__(self):
self.blocked_countries: set = set()
self.allowed_countries: set = set()
def block_country(self, country_code: str):
self.blocked_countries.add(country_code.upper())
def allow_country(self, country_code: str):
self.allowed_countries.add(country_code.upper())
def is_allowed(self, country_code: str) -> bool:
country_code = country_code.upper()
if self.allowed_countries:
return country_code in self.allowed_countries
if self.blocked_countries:
return country_code not in self.blocked_countries
return True
def check_request(self, request_ip: str, country_code: str) -> dict:
allowed = self.is_allowed(country_code)
return {
"ip": request_ip,
"country": country_code,
"allowed": allowed,
"reason": None if allowed else f"Traffic from {country_code} is blocked",
}
geo = GeoBlocker()
geo.block_country("KP")
geo.block_country("IR")
geo.block_country("SY")
requests = [
("10.0.0.1", "US"),
("10.0.0.2", "KP"),
("10.0.0.3", "IR"),
("10.0.0.4", "DE"),
]
for ip, country in requests:
result = geo.check_request(ip, country)
print(f"{ip:15s} [{country}] -> {'ALLOWED' if result['allowed'] else 'BLOCKED (' + result['reason'] + ')'}")
Expected output:
10.0.0.1 [US] -> ALLOWED
10.0.0.2 [KP] -> BLOCKED (Traffic from KP is blocked)
10.0.0.3 [IR] -> BLOCKED (Traffic from IR is blocked)
10.0.0.4 [DE] -> ALLOWED
Dynamic IP Management
# dynamic_ip_mgmt.py
import time
from typing import Dict, List, Optional, Set
class DynamicIPManager:
def __init__(self, cleanup_interval: int = 300):
self.temp_whitelist: Dict[str, float] = {}
self.perm_whitelist: Set[str] = set()
self.cleanup_interval = cleanup_interval
self.last_cleanup = time.time()
def add_permanent(self, ip: str):
self.perm_whitelist.add(ip)
def add_temporary(self, ip: str, ttl_seconds: int = 3600):
self.temp_whitelist[ip] = time.time() + ttl_seconds
def remove(self, ip: str):
self.perm_whitelist.discard(ip)
self.temp_whitelist.pop(ip, None)
def is_allowed(self, ip: str) -> bool:
self._cleanup()
if ip in self.perm_whitelist:
return True
if ip in self.temp_whitelist and time.time() < self.temp_whitelist[ip]:
return True
return False
def _cleanup(self):
now = time.time()
if now - self.last_cleanup < self.cleanup_interval:
return
expired = [ip for ip, exp in self.temp_whitelist.items() if exp < now]
for ip in expired:
del self.temp_whitelist[ip]
self.last_cleanup = now
manager = DynamicIPManager()
manager.add_permanent("192.168.1.100")
manager.add_temporary("10.0.0.50", ttl_seconds=5)
print("Immediately:")
print(f" 192.168.1.100: {manager.is_allowed('192.168.1.100')}")
print(f" 10.0.0.50: {manager.is_allowed('10.0.0.50')}")
print(f" 1.2.3.4: {manager.is_allowed('1.2.3.4')}")
Expected output:
Immediately:
192.168.1.100: True
10.0.0.50: True
1.2.3.4: False
Common Mistakes
1. Not Handling X-Forwarded-For
The client IP may be in X-Forwarded-For header, not the connection IP when behind a load balancer. Parse the correct IP.
2. Overly Broad Ranges
Allowing 0.0.0.0/0 effectively disables IP filtering. Use the most specific CIDR ranges possible.
3. No Fallback for Misconfiguration
If the whitelist configuration is invalid, the gateway should default to deny, not allow.
4. Ignoring IPv6
Many clients use IPv6. Ensure your IP filtering handles both IPv4 and IPv6 addresses.
5. Not Monitoring Blocked Attempts
Blocked IP attempts can indicate scanning or attack patterns. Log and monitor blocked requests.
Practice Questions
1. What is a CIDR notation and how does it work?
CIDR (Classless Inter-Domain Routing) notation specifies an IP range using the base IP and prefix length (e.g., 10.0.0.0/8 means all IPs from 10.0.0.0 to 10.255.255.255).
2. Why combine IP whitelisting with authentication?
IP whitelisting blocks network-level attacks even if credentials are stolen. Authentication prevents unauthorized users within the allowed network.
3. How do you handle dynamic IPs for remote workers?
Use a temporary whitelist with TTLs that remote workers renew periodically, or require VPN access with static IPs.
4. What is geo-blocking and when should you use it?
Geo-blocking restricts traffic based on geographic region. Use it when your API targets specific regions to reduce attack surface.
Challenge
Design an IP access control system that combines permanent whitelist, temporary access with TTL, geo-blocking, and CIDR matching with proper X-Forwarded-For handling.
FAQ
Mini Project: IP Access Controller
# ip_controller.py
import ipaddress
import time
from typing import Dict, List, Optional, Set
class IPAccessController:
def __init__(self):
self.whitelist = []
self.blacklist = []
self.temp_access = {}
def add_whitelist(self, cidr: str):
self.whitelist.append(ipaddress.IPv4Network(cidr))
def add_blacklist(self, cidr: str):
self.blacklist.append(ipaddress.IPv4Network(cidr))
def grant_temp(self, ip: str, ttl: int = 300):
self.temp_access[ip] = time.time() + ttl
def check(self, client_ip: str) -> dict:
try:
ip = ipaddress.IPv4Address(client_ip)
except ValueError:
return {"allowed": False, "reason": "Invalid IP"}
for net in self.blacklist:
if ip in net:
return {"allowed": False, "reason": f"IP in blacklist {net}"}
if client_ip in self.temp_access and time.time() < self.temp_access[client_ip]:
return {"allowed": True, "reason": "Temporary grant"}
if not self.whitelist:
return {"allowed": True, "reason": "No restrictions"}
for net in self.whitelist:
if ip in net:
return {"allowed": True, "reason": f"IP in whitelist {net}"}
return {"allowed": False, "reason": "IP not in whitelist"}
ctrl = IPAccessController()
ctrl.add_whitelist("10.0.0.0/8")
ctrl.add_blacklist("10.0.0.0/24")
ctrl.grant_temp("10.0.0.50", 60)
tests = ["10.0.0.50", "10.0.0.1", "10.0.1.1", "192.168.1.1"]
for ip in tests:
result = ctrl.check(ip)
print(f"{ip:20s} -> {'ALLOWED' if result['allowed'] else 'BLOCKED'}: {result['reason']}")
Expected output:
10.0.0.50 -> ALLOWED: Temporary grant
10.0.0.1 -> BLOCKED: IP in blacklist 10.0.0.0/24
10.0.1.1 -> ALLOWED: IP in whitelist 10.0.0.0/8
192.168.1.1 -> BLOCKED: IP not in whitelist
What's Next
You understand IP whitelisting. Next, learn about circuit breaker pattern, then explore gateway caching.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro