Building a Reverse Proxy to Patch AWS's Metadata Security Hole (Before IMDSv2 Fixed It)
Hook
For years, a simple curl command exploiting an SSRF vulnerability could steal your AWS credentials in seconds. This tool fought back with 500 lines of Go and a clever iptables trick—until AWS finally shipped a proper fix.
Context
Before November 2019, AWS's EC2 metadata service was a sitting duck for Server-Side Request Forgery (SSRF) attacks. Every EC2 instance runs a special HTTP server at 169.254.169.254 that hands out sensitive information—including temporary IAM credentials that applications use to access AWS services. The problem? Anyone who could trick your application into making an HTTP request to that IP could steal those credentials.
The attack pattern was devastatingly simple: find any web application feature that fetches remote URLs (image processors, webhook validators, URL preview generators), point it at http://169.254.169.254/latest/meta-data/iam/security-credentials/, and watch the JSON credentials roll in. Capital One's 2019 breach famously exploited exactly this vector. While AWS eventually released Instance Metadata Service Version 2 (IMDSv2) requiring session tokens, countless legacy systems remained vulnerable. Stefan Sundin's ec2-metadata-filter emerged as a stopgap solution, implementing user-agent validation and custom headers to block unauthorized metadata access—a reverse proxy standing between attackers and your IAM credentials.
Technical Insight
The architecture is elegantly simple: intercept all traffic to 169.254.169.254 using iptables, route it through a Go proxy that validates requests, then forward legitimate traffic to the real metadata service. The magic happens in how it avoids intercepting its own forwarding requests, which would create an infinite loop.
Here's the iptables rule that makes it work:
iptables -t nat -A OUTPUT \
-d 169.254.169.254/32 \
-p tcp -m tcp --dport 80 \
-m owner ! --uid-owner ec2-metadata-filter \
-j REDIRECT --to-ports 8000
The critical piece is -m owner ! --uid-owner ec2-metadata-filter. This tells iptables to redirect all metadata service requests except those made by the proxy's own system user. When your application makes a request, it hits the proxy. When the proxy forwards that request to the real metadata service, it bypasses the iptables rule entirely because it's running as a dedicated user. This user-scoped exemption is far cleaner than IP-based exceptions or loopback tricks.
The validation logic implements two complementary strategies inspired by Google Cloud and Netflix:
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Strategy 1: Require custom header (Google Cloud style)
if h.config.RequireMetadataFlavor {
if r.Header.Get("Metadata-Flavor") != "Amazon" {
http.Error(w, "Missing Metadata-Flavor header", 400)
return
}
}
// Strategy 2: Whitelist known AWS SDK User-Agents
userAgent := r.Header.Get("User-Agent")
if !h.isWhitelisted(userAgent) {
http.Error(w, "User-Agent not whitelisted", 400)
return
}
// Forward valid requests
h.proxy.ServeHTTP(w, r)
}
The User-Agent whitelist approach works because AWS SDKs identify themselves predictably. The Go SDK sends aws-sdk-go/1.44.0, Python's boto3 sends Botocore/1.27.0, and so on. Most SSRF attacks use generic HTTP clients (curl, wget, or language standard libraries) that don't match these patterns. The configuration file lets you define regex patterns:
user_agent_whitelist:
- '^aws-sdk-.*'
- '^Botocore/.*'
- '^aws-cli/.*'
- '^Cloud-Init/.*' # For instance bootstrapping
The dual-strategy approach is crucial: User-Agent whitelisting handles the 95% case where you're using AWS SDKs, while the Metadata-Flavor header requirement catches everything else. Code you control can add one line (request.headers['Metadata-Flavor'] = 'Amazon'), but remote attackers exploiting blind SSRF vulnerabilities typically can't control headers.
The proxy implementation uses Go's httputil.ReverseProxy with minimal customization. It preserves the original request path, so calls to /latest/meta-data/instance-id work identically through the proxy. Response streaming works correctly for large responses (user data can be several KB). The entire filtering layer adds less than 1ms of latency since it's just header inspection before proxying—no body parsing or complex logic.
Gotcha
The biggest limitation is right in the README: this entire approach is obsolete. IMDSv2 solves the same problem at the platform level by requiring applications to first request a session token using a PUT request (which SSRF attacks usually can't exploit) before accessing metadata. If you can migrate to IMDSv2, you absolutely should—it's more secure, requires no proxy maintenance, and works with all tools out of the box.
Even if you're stuck on IMDSv1, deployment is tricky. You need to identify every legitimate tool accessing metadata and either add its User-Agent to the whitelist or modify its code to send the Metadata-Flavor header. This sounds straightforward until you discover that your custom deployment scripts use curl, your monitoring agent uses a non-standard HTTP library, or Elastic Beanstalk's infrastructure code won't work without whitelist additions. Each failure mode looks like your instance mysteriously breaking—applications can't assume IAM roles, user data scripts fail silently, and instance tags become inaccessible. The iptables rule also needs to survive reboots, requiring systemd service configuration or init scripts. One misconfigured iptables rule could lock you out of your own metadata service or, worse, create the appearance of security while not actually filtering anything.
Verdict
Skip if: You're running any modern EC2 instance. Enable IMDSv2 with aws ec2 modify-instance-metadata-options --instance-id i-xxx --http-tokens required and move on with your life. This is the correct solution AWS should have shipped from day one. Also skip if you're starting a new project—there's zero reason to deploy this proxy in 2024.
Use if: You're operating a legacy fleet stuck on IMDSv1 where upgrading to IMDSv2 would break critical applications you can't immediately refactor, and you need a temporary security improvement while planning your migration. Even then, use this as a six-month bridge, not a permanent solution. The real value today is educational: clone the repo, read the 500 lines of Go code, and understand how elegant reverse proxies with iptables integration work. The user-scoped owner matching technique is genuinely clever and applicable to other local proxy scenarios beyond metadata services.