Skip to content

Instantly share code, notes, and snippets.

@mohashari
Created July 4, 2026 01:02
Show Gist options
  • Select an option

  • Save mohashari/8a73ec2977ba8b9cb0dbf95243077d09 to your computer and use it in GitHub Desktop.

Select an option

Save mohashari/8a73ec2977ba8b9cb0dbf95243077d09 to your computer and use it in GitHub Desktop.
Mitigating SSRF in Serverless: Designing a Dynamic Envoy Proxy Sidecar for AWS ECS Tasks — code snippets
package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"time"
)
// FlawedFetch attempts to prevent SSRF by checking the IP of a hostname before fetching.
// This function is vulnerable to DNS Rebinding (TOCTOU) and HTTP Redirection bypasses.
func FlawedFetch(rawURL string) ([]byte, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
// 1. Resolve host to check IP (Time of Check)
ips, err := net.LookupIP(parsedURL.Hostname())
if err != nil {
return nil, fmt.Errorf("DNS resolution failed: %w", err)
}
for _, ip := range ips {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() {
return nil, errors.New("request blocked: destination resolved to private IP")
}
}
// 2. Perform the actual HTTP fetch (Time of Use)
// The HTTP client will resolve the host AGAIN if the TTL expired or was 0.
// If the host is attacker-controlled, it can yield an internal IP on the second lookup.
// Furthermore, the client will follow 3xx redirects to internal resources.
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(context.Background(), "GET", rawURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http error: %d", resp.StatusCode)
}
return []byte("data fetched"), nil
}
#!/usr/bin/env bash
# Configure iptables rules to redirect outgoing TCP traffic to Envoy.
# Runs inside the ECS task initialization or via an init container.
set -euo pipefail
ENVOY_PORT="15001"
ENVOY_USER_ID="1337" # UID of the Envoy container user
# Create target chain
iptables -t nat -N ENVOY_EGRESS
# Rule 1: Allow Envoy container itself to bypass routing rules (prevents infinite loop)
iptables -t nat -A ENVOY_EGRESS -m owner --uid-owner "${ENVOY_USER_ID}" -j RETURN
# Rule 2: Skip redirection for local inter-container traffic (e.g. local APIs, databases)
iptables -t nat -A ENVOY_EGRESS -d 127.0.0.1/32 -j RETURN
# Rule 3: Redirect standard outbound HTTP (80) and HTTPS (443) to Envoy
iptables -t nat -A ENVOY_EGRESS -p tcp --dport 80 -j REDIRECT --to-ports "${ENVOY_PORT}"
iptables -t nat -A ENVOY_EGRESS -p tcp --dport 443 -j REDIRECT --to-ports "${ENVOY_PORT}"
# Apply the egress chain to all outbound traffic
iptables -t nat -A OUTPUT -p tcp -j ENVOY_EGRESS
echo "iptables rules applied. Egress traffic redirected to port ${ENVOY_PORT}."
node:
id: ecs-egress-proxy
cluster: sidecar-egress
static_resources:
listeners:
- name: egress_http_listener
address:
socket_address:
address: 127.0.0.1
port_value: 15001
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: egress_http
route_config:
name: egress_route
virtual_hosts:
- name: any_external_host
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: secure_egress_cluster
timeout: 10s
http_filters:
# 1. RBAC Filter: Blocks SSRF vectors based on request parameters
- name: envoy.filters.http.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: DENY
policies:
"block-aws-metadata":
permissions:
- any: true
principals:
- any: true
condition:
or_rules:
rules:
# Block literal requests to AWS IMDSv2
- destination_ip:
address_prefix: 169.254.169.254
prefix_len: 32
# Block literal requests to VPC RFC 1918 CIDRs
- destination_ip:
address_prefix: 10.0.0.0
prefix_len: 8
- destination_ip:
address_prefix: 172.16.0.0
prefix_len: 12
- destination_ip:
address_prefix: 192.168.0.0
prefix_len: 16
# 2. Dynamic Forward Proxy Filter: Resolves hostnames securely
- name: envoy.filters.http.dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig
dns_cache_config:
name: secure_dns_cache
dns_lookup_family: V4_ONLY
dns_resolution_config:
dns_resolver_options:
use_tcp_for_dns_lookups: false
no_default_search_domain: true
# 3. Standard Router Filter
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: secure_egress_cluster
connect_timeout: 5s
lb_policy: CLUSTER_PROVIDED
cluster_type:
name: envoy.clusters.dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig
dns_cache_config:
name: secure_dns_cache
dns_lookup_family: V4_ONLY
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
validation_context:
trusted_ca:
filename: /etc/ssl/certs/ca-certificates.crt
# Upstream network-level filter configuration within Envoy clusters.
# This checks resolved IPs before opening TCP sockets, blocking DNS rebinding.
upstream_config:
upstream_filters:
- name: envoy.filters.upstream.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: DENY
policies:
"enforce-public-ips-only":
permissions:
- any: true
principals:
- any: true
condition:
or_rules:
rules:
# Block connection if the resolved IP is private or link-local
- destination_ip:
address_prefix: 169.254.0.0
prefix_len: 16
- destination_ip:
address_prefix: 10.0.0.0
prefix_len: 8
- destination_ip:
address_prefix: 172.16.0.0
prefix_len: 12
- destination_ip:
address_prefix: 192.168.0.0
prefix_len: 16
- destination_ip:
address_prefix: 127.0.0.0
prefix_len: 8
{
"family": "secure-app-task",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "envoy-sidecar",
"image": "envoyproxy/envoy:v1.28.0",
"essential": true,
"user": "1337",
"portMappings": [
{
"containerPort": 15001,
"hostPort": 15001,
"protocol": "tcp"
}
],
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://127.0.0.1:15001/ready || exit 1"
],
"interval": 5,
"timeout": 2,
"retries": 3,
"startPeriod": 10
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/secure-app-envoy",
"awslogs-region": "ap-southeast-1",
"awslogs-stream-prefix": "envoy"
}
}
},
{
"name": "application-api",
"image": "your-org/api-service:latest",
"essential": true,
"dependsOn": [
{
"containerName": "envoy-sidecar",
"condition": "HEALTHY"
}
],
"environment": [
{
"name": "HTTP_PROXY",
"value": "http://127.0.0.1:15001"
},
{
"name": "HTTPS_PROXY",
"value": "http://127.0.0.1:15001"
},
{
"name": "NO_PROXY",
"value": "127.0.0.1,localhost,169.254.170.2"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/secure-app",
"awslogs-region": "ap-southeast-1",
"awslogs-stream-prefix": "app"
}
}
}
]
}
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// NewSecureClient returns an HTTP client configured to route traffic explicitly
// through the local Envoy sidecar proxy.
func NewSecureClient(proxyURLStr string) (*http.Client, error) {
proxyURL, err := url.Parse(proxyURLStr)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL: %w", err)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
// Configure TLS configuration to enforce safe defaults
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}, nil
}
func main() {
// Rooted HTTP Proxy pointing to local Envoy sidecar
client, err := NewSecureClient("http://127.0.0.1:15001")
if err != nil {
panic(err)
}
req, err := http.NewRequestWithContext(context.Background(), "GET", "https://api.stripe.com/v1/charges", nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Connection error (blocked or offline): %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response code: %d, body length: %d\n", resp.StatusCode, len(body))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment