Implementing SSRF Allowlists and Metadata Service Protection: A Secure Coding Guide

Server-Side Request Forgery (SSRF) targeting cloud metadata endpoints remains a critical infrastructure compromise vector. Attackers exploit user-controlled URL parameters to pivot into internal networks, directly querying link-local addresses (e.g., 169.254.169.254, 100.100.100.200, metadata.google.internal) to exfiltrate temporary IAM credentials, instance metadata, and network topology. Traditional perimeter firewalls and regex-based denylists fail against encoding bypasses, DNS rebinding, IPv6 translation, and newly provisioned internal CIDR blocks. The only defensible baseline is strict, parsed URI allowlisting combined with mandatory metadata service hardening.

This guide provides exact implementation steps, secure validation patterns, and infrastructure-as-code enforcement for engineering and compliance teams. For foundational threat modeling, refer to Server-Side Request Forgery (SSRF) Prevention before deploying runtime controls. We will cover schema validation, IMDSv2 enforcement, CI/CD policy-as-code, and runtime egress controls to meet SOC 2, ISO 27001, and PCI-DSS 4.0 requirements.

1. Threat Modeling Metadata Exposure in SSRF Vectors

SSRF bypasses network segmentation by leveraging the application’s own outbound request capabilities. When an application fetches user-supplied URLs, image assets, or webhook endpoints, the underlying HTTP client resolves DNS and routes traffic through the host’s network stack. If the host resides in a cloud environment with default metadata routing, a single unvalidated request to http://169.254.169.254/latest/meta-data/iam/security-credentials/ grants the attacker the instance role’s temporary credentials.

Attack paths typically follow:

  1. Input Vector: File upload via URL, PDF generation, webhook registration, or image proxy.
  2. Resolution Phase: DNS lookup translates attacker-controlled domain to 169.254.169.254 via DNS rebinding or internal DNS override.
  3. Execution Phase: HTTP client follows redirects, bypasses IPv4/IPv6 translation, or exploits open redirects to reach the metadata endpoint.
  4. Credential Exfiltration: Temporary STS tokens are captured and used for lateral movement or cloud account takeover.

Network-level denylists are insufficient because they cannot account for dynamic IP allocation, IPv4-mapped IPv6 addresses (::ffff:169.254.169.254), or HTTP/2 multiplexing bypasses. Secure architecture requires explicit trust boundaries at the application layer, enforced before socket connection. Comprehensive mitigation strategies align with established Vulnerability Patterns & Web Mitigation Strategies frameworks, prioritizing allowlist validation and metadata endpoint session enforcement.

2. Architecting Strict URL Allowlists

Strict allowlisting requires parsing the URI, validating the scheme, resolving the hostname, verifying the resolved IP against an explicit allowlist of permitted destinations, and executing the request using the resolved IP to prevent DNS rebinding.

Implementation Requirements

  • Protocol Enforcement: Allow https only. Reject http, file, gopher, ftp, and custom schemes.
  • Domain Allowlist: Maintain a cryptographically signed or environment-managed list of allowed FQDNs.
  • IP Allowlist: Only IPs that resolve to explicitly trusted CIDRs (e.g., your own internal API network) are permitted. All RFC 1918, link-local, loopback, and cloud metadata ranges are blocked.
  • DNS Rebinding Mitigation: Resolve DNS before connection. Validate the resolved IP. Pass the resolved IP directly to the HTTP client with the original Host header to prevent re-resolution.
  • Punycode & Normalization: Convert internationalized domain names (IDN) to ASCII punycode before validation. Strip default ports and path traversal sequences.

Production Python Implementation

import socket
import ipaddress
import urllib.parse
from typing import Set, Tuple

# Explicit allowlist: domains and their expected CIDRs
ALLOWED_DOMAINS: Set[str] = {"api.trusted-partner.com", "cdn.internal-corp.net"}

# Blocked ranges: RFC 1918, loopback, link-local (metadata), and IPv6 private
BLOCKED_NETWORKS = [
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("169.254.0.0/16"),  # AWS/GCP/Azure metadata
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
    ipaddress.ip_network("fe80::/10"),
]

def _is_ip_blocked(ip_str: str) -> bool:
    try:
        ip_obj = ipaddress.ip_address(ip_str)
        return any(ip_obj in net for net in BLOCKED_NETWORKS)
    except ValueError:
        return True  # Reject unparseable addresses

def validate_and_prepare_request(user_url: str) -> Tuple[str, dict]:
    parsed = urllib.parse.urlparse(user_url)

    # 1. Protocol enforcement
    if parsed.scheme.lower() != "https":
        raise ValueError("Only HTTPS is permitted")

    # 2. Domain allowlist check
    domain = (parsed.hostname or "").lower()
    if domain not in ALLOWED_DOMAINS:
        raise ValueError(f"Domain '{domain}' not in allowlist")

    # 3. DNS resolution — must happen before socket connect (prevents DNS rebinding)
    try:
        resolved_ips = socket.getaddrinfo(domain, 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
        if not resolved_ips:
            raise RuntimeError("No DNS results returned")
        resolved_ip = resolved_ips[0][4][0]
    except socket.gaierror as e:
        raise RuntimeError(f"DNS resolution failed: {e}")

    # 4. Block any resolved IP in restricted ranges (metadata, RFC 1918, loopback)
    if _is_ip_blocked(resolved_ip):
        raise ValueError(f"Resolved IP {resolved_ip} is in a blocked range")

    # 5. Build request using the pinned IP to prevent re-resolution
    safe_url = f"https://{resolved_ip}{parsed.path}"
    if parsed.query:
        safe_url += f"?{parsed.query}"
    headers = {"Host": domain}  # Preserve original Host for TLS SNI / virtual hosting

    return safe_url, headers

Security Boundary: Never pass the original user-supplied URL directly to requests.get() or httpx. Always use the resolved, validated IP with a sanitized Host header. The function above is correct: it resolves to an IP, then blocks any IP that falls in restricted ranges before constructing the safe request URL.

3. Metadata Service Hardening & IMDSv2 Enforcement

Cloud providers expose metadata endpoints via link-local routing. Hardening requires disabling legacy IMDSv1, enforcing token-based authentication, and restricting network hop counts to prevent container or VM escape.

AWS IMDSv2 Enforcement

Instance Metadata Service v2 (IMDSv2) requires a PUT request to retrieve a session token before any GET request can return credentials. Enforce this at the infrastructure layer to prevent IMDSv1 credential retrieval via a single unauthenticated GET.

Terraform Implementation (AWS, Azure, GCP)

# AWS EC2: Enforce IMDSv2, disable fallback, restrict hops
resource "aws_instance" "secure_app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"

  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"  # Disables IMDSv1
    http_put_response_hop_limit = 1           # Prevents container escape (limit to 1 hop)
    instance_metadata_tags      = "disabled"
  }
}

# Azure VM: Use SystemAssigned managed identity (no static credentials needed)
resource "azurerm_linux_virtual_machine" "secure_vm" {
  name                = "secure-vm"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  size                = "Standard_B2s"
  admin_username      = "azureuser"

  identity {
    type = "SystemAssigned"
  }

  admin_ssh_key {
    username   = "azureuser"
    public_key = file("~/.ssh/id_rsa.pub")
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "20_04-lts"
    version   = "latest"
  }
}

# GCP Compute: Restrict scopes and enable OS Login
resource "google_compute_instance" "secure_instance" {
  name         = "secure-instance"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  metadata = {
    enable-oslogin = "TRUE"
  }

  service_account {
    email  = google_service_account.app.email
    scopes = ["https://www.googleapis.com/auth/cloud-platform"]
  }

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    network = "default"
  }
}

Compliance Mapping: CIS AWS Foundations Benchmark v1.5.0 requires http_tokens = "required". PCI-DSS 4.0 Requirement 1.3.1 mandates network segmentation and least-privilege access, enforced here via hop limits and scoped IAM roles.

4. CI/CD Integration & Automated Compliance Checks

Shift-left enforcement prevents metadata exposure before deployment. Integrate static analysis (SAST) to flag unvalidated HTTP clients, and deploy policy-as-code to block insecure infrastructure provisioning.

OPA/Rego Policy for IaC Scanning

This Rego policy blocks Terraform plans that enable IMDSv1 or set hop limits above 1.

package terraform.metadata_security

import rego.v1

deny[msg] if {
    resource := input.plan.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.metadata_options.http_tokens != "required"
    msg := sprintf("AWS Instance '%s' does not enforce IMDSv2. Set http_tokens = 'required'.", [resource.address])
}

deny[msg] if {
    resource := input.plan.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.metadata_options.http_put_response_hop_limit > 1
    msg := sprintf("AWS Instance '%s' hop_limit > 1. Restrict to 1 to prevent SSRF pivot.", [resource.address])
}

Pipeline Integration Steps

  1. Pre-commit: Run tfsec or checkov with custom Rego policies. Fail on CRITICAL metadata misconfigurations.
  2. SAST Scan: Configure Semgrep rules to detect requests.get(user_input) or fetch(user_input) without preceding validation middleware.
  3. Dynamic Validation: Deploy a canary container during integration tests that attempts GET http://169.254.169.254/latest/meta-data/. Assert that the response is a connection refusal or HTTP 403, confirming the firewall/IMDSv2 controls are active.
  4. Policy Enforcement Gate: Integrate opa eval into the CI pipeline. Reject PRs if any deny rule triggers.

5. Edge-Case Handling & Runtime Mitigation

Strict allowlists and IaC policies cover baseline scenarios, but advanced bypass techniques require runtime defenses and egress controls.

Advanced Bypass Vectors

  • Open Redirect Chaining: Attacker-controlled domains redirect to metadata endpoints after initial validation. Mitigation: Disable automatic redirect following (allow_redirects=False in Python requests, redirect: 'error' in the Fetch API) or re-validate the final destination URL against the allowlist after each 3xx response.
  • WebSocket SSRF: ws:// or wss:// protocols bypass HTTP allowlists. Mitigation: Explicitly block ws/wss schemes unless explicitly required, and enforce origin validation on WebSocket upgrades.
  • IPv6 Loopback & Link-Local Bypasses: Attackers supply ::1, ::ffff:169.254.169.254, or 0x7f000001 (hex-encoded loopback). Mitigation: Use ipaddress.ip_address() (Python) or net.ParseIP() (Go) to canonicalize before comparison; both libraries handle IPv4-mapped IPv6 addresses correctly.

Runtime Egress Architecture

Deploy an egress proxy (e.g., Envoy, Squid) with TLS termination and outbound allowlisting. Configure the application to route all external traffic through the proxy. The proxy enforces:

  • Strict domain allowlists at the network layer
  • Certificate pinning for critical endpoints
  • Request rate limiting and anomaly detection

Implement structured audit logging for all outbound requests. Retain logs for 12 months with WORM storage to satisfy SOC 2 CC7.2 and ISO 27001 A.12.4.

Fallback Behavior

When validation fails, return a generic 400 Bad Request or 403 Forbidden. Never expose internal DNS resolution errors, resolved IP addresses, or stack traces. Implement circuit breakers to prevent resource exhaustion from malicious validation loops.

Common Implementation Mistakes

  1. Relying on regex denylists instead of strict domain/IP allowlists: Denylists cannot enumerate all malicious encodings, IPv6 variants, or newly allocated cloud IPs.
  2. Validating the hostname but not the resolved IP: DNS rebinding attacks swap the resolved IP post-validation. Always resolve DNS and validate the resolved IP immediately before socket connection.
  3. Ignoring IPv6 link-local and IPv4-mapped IPv6 address bypasses: ::ffff:169.254.169.254 and fe80::/10 bypass IPv4-only filters. Use canonical IP address parsing before comparison.
  4. Deploying IMDSv2 without setting http_tokens = "required": The default "optional" allows legacy IMDSv1 access. Always set "required" to enforce the session token flow.
  5. Hardcoding allowlists without environment-aware configuration management: Static allowlists break across staging/production. Use environment variables, secret managers, or dynamic configuration services with cryptographic signing.

FAQ

Why are denylists insufficient for SSRF mitigation against metadata services? Denylists fail due to encoding bypasses (%2e%2e/), DNS rebinding, IPv6/IPv4 translation (::ffff:169.254.169.254), and dynamic cloud IP allocation. Attackers can easily rotate domains or use newly provisioned internal CIDRs. Allowlists enforce explicit trust boundaries, reducing the attack surface to known, audited endpoints.

How do I prevent DNS rebinding when validating SSRF allowlists? Resolve the domain to an IP address, verify the IP is not in any blocked range, and then initiate the HTTP request using the resolved IP directly while preserving the original Host header. This pins the connection to the validated IP rather than letting the OS re-resolve during the TCP handshake.

What compliance frameworks mandate metadata service protection? SOC 2 CC6.1 and CC7.2 require network segmentation and monitoring of credential access. ISO 27001 A.13.1.1 mandates network controls to prevent unauthorized access. PCI-DSS 4.0 Requirements 1.3.1 and 2.2.7 explicitly require least-privilege IAM and metadata endpoint hardening to prevent credential exfiltration. CIS Benchmarks provide technical baselines for IMDSv2 enforcement across AWS, Azure, and GCP.