MÓDULO 09 · CONCEITO 05 DE 14

Reverse Proxy

TLS termination · load balancing · health checks · nginx · Envoy · HAProxy

Tempo de leitura ~22 min Pré-requisito Conceito 04 — GraphQL Próximo 06 · API Gateway

Forward proxy vs Reverse proxy

A distinção entre os dois tipos de proxy é sobre quem o proxy representa:

Posição na topologia

# Forward proxy — cliente configura explicitamente
Cliente → [Forward Proxy] → Internet → Servidor

# Reverse proxy — transparente ao cliente
Internet → [Reverse Proxy] → Backend 1
                           → Backend 2
                           → Backend 3

# Proxy em camadas (comum em produção)
Internet
  ↓
CDN / Load Balancer externo (ex: CloudFront, Cloudflare)
  ↓
nginx / Envoy (edge proxy — TLS termination, roteamento)
  ↓
Serviço A (porta 8080)
Serviço B (porta 8081)
  ↓
nginx / Envoy (sidecar proxy — service mesh, mTLS interno)

Funções do reverse proxy

A posição do reverse proxy na topologia permite centralizar funcionalidades transversais que seriam redundantes se implementadas em cada serviço:

TLS Termination

TLS termination é o processo de descriptografar conexões HTTPS na borda do sistema, mantendo comunicação HTTP simples (ou mTLS) dentro da rede interna. Os benefícios são: simplificação dos backends (não gerenciam certificados), centralização de renovação de certificados, e possibilidade de inspecionar e manipular o conteúdo HTTP (o que seria impossível com tráfego criptografado ponta a ponta).

TLS termination vs SSL passthrough vs TLS re-encryption

ModoComo funcionaQuando usar
TLS termination Proxy descriptografa, encaminha HTTP para o backend Rede interna confiável, backend não suporta TLS, inspeção de conteúdo necessária
SSL passthrough Proxy encaminha bytes TLS sem descriptografar (roteamento por SNI) Compliance exige criptografia ponta a ponta, backend gerencia seus próprios certificados
TLS re-encryption Proxy descriptografa, re-criptografa com certificado interno antes de encaminhar Segurança em profundidade, regulatório, zero trust networks
mTLS Proxy e backend se autenticam mutuamente com certificados cliente Service mesh, comunicação entre serviços com identidade verificada

OCSP Stapling e session tickets

Para TLS de alta performance, dois recursos reduzem latência:

Configuração de TLS no nginx

server {
    listen 443 ssl http2;
    server_name api.empresa.com;

    ssl_certificate      /etc/letsencrypt/live/api.empresa.com/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/api.empresa.com/privkey.pem;

    # Protocolos e ciphers modernos
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:CHACHA20;
    ssl_prefer_server_ciphers off;  # TLS 1.3 ignora isso; bom para 1.2

    # Session resumption
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  1d;
    ssl_session_tickets  off;  # sem session tickets = melhor forward secrecy

    # OCSP Stapling
    ssl_stapling         on;
    ssl_stapling_verify  on;
    ssl_trusted_certificate /etc/letsencrypt/live/api.empresa.com/chain.pem;
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 5s;

    # HSTS — força HTTPS no browser por 1 ano
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Security headers
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;

    location / {
        proxy_pass http://backend_upstream;
    }
}

Algoritmos de Load Balancing

O algoritmo de load balancing determina como as requisições são distribuídas entre as instâncias do backend. A escolha impacta diretamente latência, utilização de recursos e consistência de sessão.

Round Robin

Distribui requisições sequencialmente entre as instâncias — 1, 2, 3, 1, 2, 3... É o padrão mais simples e funciona bem quando as requisições têm custo homogêneo e as instâncias têm capacidade equivalente. Problema: se uma instância está processando uma requisição custosa, continua recebendo novas ao mesmo ritmo que instâncias livres.

# nginx — round robin é o padrão
upstream orders_backend {
    server orders-1:8080;
    server orders-2:8080;
    server orders-3:8080;
}

# Com pesos — weighted round robin
upstream orders_backend {
    server orders-1:8080 weight=3;  # recebe 3x mais tráfego
    server orders-2:8080 weight=1;
    server orders-3:8080 weight=1;  # instância menor
}

Least Connections

Envia a próxima requisição para a instância com o menor número de conexões ativas. Funciona melhor que round robin quando as requisições têm durações variáveis — instâncias que terminam mais rápido ficam disponíveis para mais trabalho.

upstream orders_backend {
    least_conn;
    server orders-1:8080;
    server orders-2:8080;
    server orders-3:8080;
}

# HAProxy — equivalente
backend orders_backend
    balance leastconn
    server orders-1 orders-1:8080 check
    server orders-2 orders-2:8080 check
    server orders-3 orders-3:8080 check

IP Hash (Sticky Sessions)

O hash do IP do cliente determina qual instância atende — o mesmo IP sempre vai para a mesma instância. Necessário quando a aplicação tem estado em memória (sessões HTTP, WebSocket) que não é compartilhado entre instâncias. É uma solução paliativa — o ideal é externalizar estado (Redis para sessões) e remover a necessidade de sticky sessions.

upstream orders_backend {
    ip_hash;
    server orders-1:8080;
    server orders-2:8080;
    server orders-3:8080;
}

# Problema com ip_hash atrás de NAT corporativo:
# Todos os clientes de uma empresa têm o mesmo IP externo
# → todos vão para a mesma instância → desequilíbrio

Consistent Hashing

Variant do hashing que usa um anel virtual para minimizar remapeamentos quando instâncias são adicionadas ou removidas. Com hashing simples, adicionar 1 instância a um pool de N remapeia 1/N das keys para novas instâncias. Com consistent hashing, apenas 1/(N+1) das keys são remapeadas — importante para cache backends onde remapeamento = cache miss.

# nginx upstream com hash por URL — útil para cache proxies
upstream cache_backend {
    hash $request_uri consistent;
    server cache-1:6379;
    server cache-2:6379;
    server cache-3:6379;
}

# Envoy — consistent hash com ring size
load_assignment:
  policy:
    hash_policy:
    - query_parameter:
        name: "user_id"  # hash por parâmetro da query
    ring_hash_lb_config:
      minimum_ring_size: 1024
      maximum_ring_size: 8388608

Least Response Time

Combina conexões ativas e tempo de resposta médio para escolher a instância. Disponível em nginx Plus (pago) e em Envoy via LEAST_REQUEST. Especialmente útil quando instâncias têm latências diferentes (ex: instâncias em zonas de disponibilidade diferentes, ou heterogeneidade de hardware).

Random with Two Choices (Power of Two Choices)

Seleciona 2 instâncias aleatoriamente e encaminha para a com menos conexões ativas. Matematicamente, esse algoritmo produz distribuição quase tão boa quanto Least Connections global mas com custo O(1) vs O(N) — fundamental para proxies que gerenciam milhares de backends.

Health Checks — ativos e passivos

Um health check determina se uma instância de backend está saudável e deve receber tráfego. Existem dois paradigmas com trade-offs complementares.

Passive health checks (circuit breaker passivo)

O proxy detecta falhas baseado em respostas de tráfego real — se uma instância retorna erros consecutivos, é marcada como não-saudável e removida temporariamente do pool. Sem overhead adicional (não há probe requests), mas só detecta falha após clientes reais serem afetados.

# nginx — passive health check via max_fails e fail_timeout
upstream orders_backend {
    server orders-1:8080 max_fails=3 fail_timeout=30s;
    server orders-2:8080 max_fails=3 fail_timeout=30s;
    server orders-3:8080 max_fails=3 fail_timeout=30s;
    # Após 3 falhas, a instância fica fora por 30 segundos
    # Depois, um request de teste é enviado — se ok, volta ao pool
}

# HAProxy — passive com observação de erros
backend orders_backend
    option httpchk
    server orders-1 orders-1:8080 check inter 5s rise 2 fall 3
    # rise 2: precisa de 2 respostas ok para voltar ao pool
    # fall 3: precisa de 3 falhas para sair do pool
    # inter 5s: intervalo entre checks

Active health checks

O proxy envia periodicamente requisições de probe para um endpoint dedicado de saúde (ex: /health ou /ready) — independente do tráfego real. Detecta falhas antes que clientes sejam afetados, mas adiciona overhead de conexões e pode mascarar problemas que só aparecem com tráfego real de alta frequência.

# nginx Plus (pago) — active health checks
upstream orders_backend {
    zone backend 64k;
    server orders-1:8080;
    server orders-2:8080;

    health_check interval=5s fails=2 passes=1 uri=/health;
    # probe a /health a cada 5s
    # 2 falhas → remove do pool
    # 1 sucesso → retorna ao pool
}

# Envoy — active health check nativo
health_checks:
- timeout: 1s
  interval: 5s
  unhealthy_threshold: 2   # falhas para remover
  healthy_threshold: 1     # sucessos para retornar
  http_health_check:
    path: "/ready"
    expected_statuses:
    - start: 200
      end: 299

Readiness vs Liveness vs Startup probes

Embora o conceito seja mais conhecido em Kubernetes, o proxy deve respeitar a semântica:

Connection Pooling e Keepalive

Criar uma conexão TCP + TLS para cada requisição é caro — envolve 3-way handshake TCP (~1 RTT) mais o handshake TLS (~1-2 RTTs adicionais em TLS 1.2). Em sistemas de alta carga, conexões efêmeras consomem CPU e portas disponíveis (limite de 65535 por IP de origem).

Keepalive entre proxy e backend

O proxy mantém um pool de conexões TCP persistentes com o backend — quando uma requisição termina, a conexão volta ao pool em vez de ser fechada. Novas requisições reutilizam conexões do pool sem overhead de handshake.

# nginx — keepalive para conexões com o upstream
upstream orders_backend {
    server orders-1:8080;
    server orders-2:8080;

    keepalive 32;          # máximo de conexões keepalive por worker
    keepalive_timeout 60s; # fechar conexão idle após 60s
    keepalive_requests 1000; # reciclar conexão após N requests (evitar memory leaks)
}

server {
    location /api/ {
        proxy_pass http://orders_backend;

        # Habilitar keepalive com o upstream (HTTP/1.1 necessário)
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # remover Connection: close do default

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;
    }
}

Keepalive com clientes

HTTP/1.1 tem keepalive por padrão. HTTP/2 vai além — multiplexa múltiplas requisições sobre uma única conexão TCP. O proxy deve suportar HTTP/2 do lado cliente (com clientes que suportam) e pode usar HTTP/1.1 para o backend — ou HTTP/2 se o backend suportar.

# nginx — HTTP/2 do cliente, HTTP/1.1 para o backend
server {
    listen 443 ssl http2;  # aceita HTTP/2 de clientes

    location / {
        proxy_pass http://orders_backend;    # upstream usa HTTP/1.1
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

# Para usar HTTP/2 também com o backend (nginx 1.25.1+)
upstream orders_backend {
    server orders-1:8080;
}
location / {
    grpc_pass grpc://orders_backend;  # para gRPC (sempre HTTP/2)
    # ou para HTTP/2 regular: proxy_pass https://orders_backend;
    # com proxy_ssl_protocols TLSv1.2 TLSv1.3;
}

Caching no Reverse Proxy

O proxy pode cachear respostas do backend e servi-las diretamente para requisições subsequentes — eliminando latência de backend e reduzindo carga. A eficácia depende da taxa de hit: respostas muito específicas ou com alta variação (por usuário, por parâmetro) têm baixas taxas de hit.

Cache keys e Cache-Control

A cache key determina quando duas requisições compartilham a mesma resposta cacheada. Por padrão, o nginx usa URL completa (incluindo query string) como chave. Headers de resposta Cache-Control controlam se e por quanto tempo o proxy pode cachear.

# nginx — configuração de proxy cache
proxy_cache_path /var/cache/nginx
    levels=1:2
    keys_zone=api_cache:10m   # 10MB de metadados (aprox. 80k entradas)
    max_size=1g               # tamanho máximo do cache em disco
    inactive=60m              # remover entradas não acessadas em 60min
    use_temp_path=off;

server {
    location /api/products/ {
        proxy_pass http://products_backend;
        proxy_cache api_cache;

        # Cache key — inclui método, host e URI
        proxy_cache_key "$request_method$host$request_uri";

        # Respeitar Cache-Control do backend, mas com fallback
        proxy_cache_valid 200 10m;    # cachear 200 por 10 min
        proxy_cache_valid 404 1m;     # cachear 404 por 1 min
        proxy_cache_valid any 5m;     # outros status por 5 min

        # Servir cache stale enquanto backend está indisponível (até 1h)
        proxy_cache_use_stale error timeout updating http_503;
        proxy_cache_background_update on;  # revalidar em background

        # Lock — apenas um request de miss por key, outros aguardam
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;

        # Expor status do cache no header de resposta
        add_header X-Cache-Status $upstream_cache_status;
        # Valores: MISS, HIT, EXPIRED, BYPASS, REVALIDATED, STALE, UPDATING
    }

    location /api/orders/ {
        proxy_pass http://orders_backend;
        # Não cachear orders — dados altamente específicos por usuário
        proxy_cache_bypass $http_authorization;  # bypass se tem auth header
        proxy_no_cache $http_authorization;
    }
}
atenção Caching de respostas autenticadas: nunca cachear respostas que contêm dados específicos de usuário sem incluir a identidade do usuário na cache key. Um erro comum é cachear /api/orders sem considerar que usuários diferentes têm pedidos diferentes. Use $http_authorization ou um cookie de sessão na cache key — ou simplesmente não cachear endpoints privados no proxy.

Compressão

O proxy comprime respostas do backend antes de enviá-las ao cliente, reduzindo bandwidth e melhorando tempo de transferência — especialmente para payloads JSON grandes. O backend não precisa implementar compressão.

# nginx — gzip e brotli
gzip on;
gzip_comp_level 4;    # 1 (rápido/baixa compressão) a 9 (lento/alta compressão)
gzip_min_length 1000; # não comprimir respostas menores que 1KB
gzip_proxied any;     # comprimir mesmo para respostas de proxy
gzip_types
    application/json
    application/javascript
    application/xml
    text/css
    text/html
    text/plain
    text/xml;
gzip_vary on;  # adiciona Vary: Accept-Encoding ao response

# Brotli (melhor compressão que gzip para texto, precisa de módulo)
# brotli on;
# brotli_comp_level 4;
# brotli_types application/json text/html text/css;

# Desabilitar compressão para streams e SSE
location /events/ {
    proxy_pass http://events_backend;
    gzip off;  # não comprimir streams de eventos
    proxy_buffering off;  # desabilitar buffer para SSE
    proxy_cache off;
}
nota Compressão e CPU: gzip nível 4-6 oferece boa relação compressão/CPU. Níveis mais altos (7-9) reduzem tamanho marginalmente mas aumentam CPU do proxy significativamente. Em sistemas de alta carga, comprimir no proxy com nível 4 é melhor que comprimir no backend com nível 9 — o proxy é otimizado para I/O, o backend para lógica de negócio.

Headers de Proxy

Quando o proxy encaminha uma requisição ao backend, o backend vê o IP do proxy como origem — não o IP do cliente real. Headers especiais preservam informações do cliente original que o backend pode precisar para logging, geo-localização, rate limiting ou decisões de negócio.

Headers padrão

# nginx — headers forwarded
location / {
    proxy_pass http://orders_backend;

    # IP real do cliente
    proxy_set_header X-Real-IP $remote_addr;

    # Cadeia completa de IPs (cliente, proxies intermediários)
    # Formato: "IP-cliente, IP-proxy1, IP-proxy2"
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # Protocolo original (http ou https) — o backend só vê http
    proxy_set_header X-Forwarded-Proto $scheme;

    # Host original — sem isso, o backend pode ver o nome interno
    proxy_set_header Host $host;

    # Port original
    proxy_set_header X-Forwarded-Port $server_port;

    # Remover o cabeçalho de upgrade do cliente (segurança)
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}

Forwarded (RFC 7239) — o padrão moderno

O RFC 7239 padroniza um único header Forwarded que substitui todos os headers X-Forwarded-*:

# Formato do header Forwarded
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43;host=example.com
Forwarded: for="[2001:db8::cafe]", for=unknown
# para IPv6, envolver em aspas

# Envoy produz Forwarded automaticamente por padrão
atenção IP spoofing via X-Forwarded-For: um cliente malicioso pode enviar X-Forwarded-For: 127.0.0.1 em sua requisição, enganando o backend a acreditar que a requisição vem de localhost. O nginx com proxy_add_x_forwarded_for anexa o IP real ao header existente — não substitui. O backend deve sempre usar o último IP adicionado pelo proxy confiável, não o primeiro (que pode ser forjado). Melhor ainda: configurar o backend para confiar apenas no IP do proxy e extrair o cliente de um header customizado que o proxy define e o cliente não pode forjar.

Buffering de request e response

Por padrão, o nginx bufferiza requisições e respostas em disco/memória antes de encaminhar. Isso protege backends de clientes lentos (slow HTTP attacks) mas adiciona latência para streaming. Configure conforme o caso:

# Para APIs REST normais — buffering padrão
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;

# Para Server-Sent Events ou streaming
location /stream {
    proxy_pass http://streaming_backend;
    proxy_buffering off;            # enviar bytes ao cliente conforme chegam
    proxy_cache off;
    proxy_read_timeout 3600s;       # manter conexão por 1h para SSE
    chunked_transfer_encoding on;
}

# Para upload de arquivos grandes — buffer em disco
proxy_request_buffering off;  # encaminhar body conforme chega (útil para uploads)

WebSocket e gRPC

WebSocket e gRPC requerem configuração específica porque ambos fazem upgrade ou usam protocolo de transporte diferente do HTTP/1.1 padrão.

WebSocket proxying

# WebSocket usa HTTP Upgrade mechanism
# O nginx precisa passar os headers de Upgrade e Connection

# Mapa para upgrade de conexão
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl http2;

    location /ws/ {
        proxy_pass http://websocket_backend;

        # Headers de upgrade
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # IPs
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;

        # Timeout longo — WebSocket é conexão persistente
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;

        # Desabilitar proxy buffering para WebSocket
        proxy_buffering off;
    }
}

gRPC proxying

# gRPC usa HTTP/2 com framing binário
# nginx tem suporte nativo via directiva grpc_pass

upstream grpc_orders {
    server orders-grpc-1:9090;
    server orders-grpc-2:9090;
}

server {
    listen 443 ssl http2;

    location /orders.v1.OrderService/ {
        grpc_pass grpc://grpc_orders;

        # Para gRPC com TLS no backend
        # grpc_pass grpcs://grpc_orders;
        # grpc_ssl_certificate ...

        # gRPC tem seus próprios timeouts
        grpc_read_timeout 30s;
        grpc_send_timeout 30s;
        grpc_connect_timeout 5s;

        # Health check path (gRPC health protocol)
        # proxy para grpc.health.v1.Health/Check
    }

    # Tratamento de erros gRPC como HTTP/2
    error_page 502 = /error502grpc;
    location = /error502grpc {
        internal;
        default_type application/grpc;
        add_header grpc-status 14;      # UNAVAILABLE
        add_header grpc-message "Serviço indisponível";
        return 204;
    }
}

nginx — configuração de produção

nginx é o reverse proxy mais usado no mundo — surgiu em 2004 focado em alta concorrência com modelo event-driven (vs Apache's process-per-connection). Configuração completa de um servidor nginx para um serviço de API:

# /etc/nginx/nginx.conf

worker_processes auto;           # um worker por CPU core
worker_rlimit_nofile 65535;      # max file descriptors por worker

events {
    worker_connections 4096;     # max conexões por worker
    use epoll;                   # I/O multiplexing (Linux)
    multi_accept on;             # aceitar múltiplas conexões por vez
}

http {
    # Limitar tamanho do corpo de requisição
    client_max_body_size 10m;
    client_body_timeout 30s;
    client_header_timeout 10s;

    # Timeouts de keepalive com clientes
    keepalive_timeout 65s;
    keepalive_requests 1000;

    # Performance
    sendfile on;
    tcp_nopush on;      # envia headers + início do body em um segmento TCP
    tcp_nodelay on;     # desabilita Nagle algorithm para baixa latência

    # Rate limiting — definir zonas
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    # Upstream do serviço de orders
    upstream orders_backend {
        least_conn;
        server orders-1:8080 max_fails=3 fail_timeout=30s;
        server orders-2:8080 max_fails=3 fail_timeout=30s;
        server orders-3:8080 max_fails=3 fail_timeout=30s;
        keepalive 32;
        keepalive_timeout 60s;
    }

    # Redirect HTTP → HTTPS
    server {
        listen 80;
        server_name api.empresa.com;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name api.empresa.com;

        # TLS
        ssl_certificate      /etc/letsencrypt/live/api.empresa.com/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/api.empresa.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_session_cache shared:SSL:10m;
        ssl_stapling on;
        ssl_stapling_verify on;

        # Security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Content-Type-Options nosniff always;

        # Rate limiting
        limit_req zone=api_limit burst=200 nodelay;
        limit_conn conn_limit 20;

        # Gzip
        gzip on;
        gzip_types application/json text/html text/css;
        gzip_comp_level 4;

        # API proxy
        location /api/v1/ {
            proxy_pass http://orders_backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 5s;
            proxy_send_timeout    30s;
            proxy_read_timeout    30s;

            # Retries em erros de conexão (não em 5xx)
            proxy_next_upstream error timeout;
            proxy_next_upstream_tries 2;

            # Sem retry em POST/PUT/PATCH/DELETE (não idempotentes)
            proxy_next_upstream_timeout 10s;
        }

        # Health check endpoint do próprio nginx
        location /nginx-health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

Envoy — proxy L7 moderno

Envoy é um proxy de alto desempenho desenvolvido pelo Lyft (open source desde 2016) que se tornou o data plane padrão de service meshes (Istio, Consul Connect) e API Gateways modernos. Diferente do nginx (configuração estática), o Envoy foi projetado para configuração dinâmica via API (xDS — Discovery Service).

Arquitetura xDS

xDS é um conjunto de APIs gRPC que permitem configurar o Envoy em runtime sem restart:

Configuração estática do Envoy

# envoy.yaml — configuração estática (para desenvolvimento e entendimento)
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    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: ingress_http
          codec_type: AUTO  # detecta HTTP/1.1 e HTTP/2

          route_config:
            name: local_route
            virtual_hosts:
            - name: orders_service
              domains: ["api.empresa.com"]
              routes:
              # Rota para orders API
              - match:
                  prefix: "/api/v1/orders"
                route:
                  cluster: orders_cluster
                  timeout: 30s
                  retry_policy:
                    retry_on: "5xx,reset,connect-failure"
                    num_retries: 2
                    per_try_timeout: 10s
              # Rota default
              - match:
                  prefix: "/"
                route:
                  cluster: default_cluster

          http_filters:
          # Rate limiting via ratelimit service externo
          - name: envoy.filters.http.ratelimit
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
              domain: "api"
              rate_limit_service:
                grpc_service:
                  envoy_grpc:
                    cluster_name: ratelimit_cluster
          # JWT authentication
          - name: envoy.filters.http.jwt_authn
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
              providers:
                provider_a:
                  issuer: "https://auth.empresa.com"
                  audiences: ["api.empresa.com"]
                  remote_jwks:
                    http_uri:
                      uri: "https://auth.empresa.com/.well-known/jwks.json"
                      cluster: auth_cluster
                      timeout: 5s
          - name: envoy.filters.http.router

  clusters:
  - name: orders_cluster
    connect_timeout: 5s
    type: STRICT_DNS
    lb_policy: LEAST_REQUEST
    load_assignment:
      cluster_name: orders_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: orders-service
                port_value: 8080
    health_checks:
    - timeout: 1s
      interval: 5s
      unhealthy_threshold: 2
      healthy_threshold: 1
      http_health_check:
        path: "/ready"

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901
  # Endpoint /stats, /config_dump, /clusters, /listeners para observabilidade

Envoy como sidecar — o modelo de service mesh

No modelo de service mesh (Istio, Consul), um Envoy é injetado como sidecar em cada Pod Kubernetes. O tráfego de e para o serviço passa pelo sidecar, que aplica mTLS, coleta métricas, faz tracing, e pode fazer circuit breaking — tudo sem o serviço saber. O plano de controle (Istiod, Consul agent) configura todos os sidecars via xDS.

HAProxy e Caddy

HAProxy — o especialista em TCP/HTTP load balancing

HAProxy é um load balancer extremamente maduro (desde 2001), com foco em alta disponibilidade e performance determinística. Excelente para casos onde o nginx é superdimensionado e o Envoy é complexo demais.

# haproxy.cfg
global
    maxconn 100000
    log /dev/log local0
    user haproxy
    group haproxy

defaults
    log global
    mode http
    option httplog
    option dontlognull
    option http-server-close
    option forwardfor       # adiciona X-Forwarded-For
    option redispatch       # redirecionar se servidor morre com sessão ativa
    retries 3
    timeout connect 5s
    timeout client  30s
    timeout server  30s

frontend api_frontend
    bind *:443 ssl crt /etc/ssl/certs/empresa.pem
    bind *:80
    http-request redirect scheme https unless { ssl_fc }

    # ACLs para roteamento
    acl is_orders path_beg /api/v1/orders
    acl is_products path_beg /api/v1/products

    use_backend orders_backend if is_orders
    use_backend products_backend if is_products
    default_backend default_backend

backend orders_backend
    balance leastconn
    option httpchk GET /ready
    http-check expect status 200
    server orders-1 orders-1:8080 check inter 5s rise 2 fall 3
    server orders-2 orders-2:8080 check inter 5s rise 2 fall 3
    server orders-3 orders-3:8080 check inter 5s rise 2 fall 3 backup

# HAProxy Stats — monitoramento web
listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 30s

Caddy — TLS automático com Let's Encrypt

Caddy foca em simplicidade e TLS automático — obtém e renova certificados Let's Encrypt automaticamente sem configuração extra. Ideal para projetos menores onde a gestão de certificados é um atrito desnecessário.

# Caddyfile — a configuração mais simples para HTTPS
api.empresa.com {
    reverse_proxy orders-1:8080 orders-2:8080 {
        lb_policy least_conn
        health_uri /ready
        health_interval 5s
        health_status 2xx
    }
    # TLS automático via Let's Encrypt — zero configuração necessária
    # Renovação automática também

    # Headers de segurança
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options nosniff
        -Server  # remover header Server
    }

    # Compressão
    encode gzip

    # Rate limiting (via plugin)
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1s
        }
    }
}

Escolhendo o proxy certo

ProxyMelhor paraEvitar quando
nginx Serving de arquivos estáticos + proxy, configuração bem conhecida, ecossistema maduro, módulos como Lua (OpenResty) Configuração dinâmica sem restart, service mesh, descoberta de serviços dinâmica
Envoy Service mesh sidecar, configuração dinâmica via xDS, observabilidade avançada, controle fino de circuit breaking Simplicidade é prioridade, equipe sem experiência com xDS, recursos como serving de estáticos
HAProxy TCP load balancing puro, alta disponibilidade determinística, banco de dados proxying, performance máxima em L4 Lógica de aplicação complexa, manipulação de conteúdo HTTP, serving de arquivos
Caddy Projetos menores, TLS automático sem operações, configuração mínima, protótipos rápidos Clusters de alta carga, configuração muito granular, ecossistema de plugins limitado vs nginx
Traefik Discovery automático via labels Docker/Kubernetes, configuração zero para microsserviços em containers Ambientes sem container orchestration, configuração muito complexa
dica Reverse proxy vs API Gateway: um reverse proxy encaminha tráfego e aplica políticas de rede (TLS, load balancing, compressão). Um API Gateway adiciona semântica de aplicação: autenticação de API keys, transformação de payload, quota por produto, portal do desenvolvedor, monetização. Na prática, as linhas se borram — nginx com plugins Lua pode fazer autenticação; Kong (baseado em nginx) é um API Gateway completo. O próximo conceito explora API Gateways em profundidade.

Decisões de engenharia

nginx vs Envoy
nginx é a escolha default para edge proxy com configuração estática — maduro, documentação extensa, excelente para serving de estáticos combinado com proxy, e ecossistema de módulos (OpenResty/Lua para lógica customizada). Envoy é a escolha para configuração dinâmica (xDS) sem restart, service mesh sidecar, observabilidade avançada nativa (métricas, tracing, logging por filtro), e controle fino de circuit breaking. Use nginx quando a infraestrutura é estável e a equipe já domina; use Envoy quando você tem Kubernetes e precisa de service mesh, ou quando o plano de controle precisa reconfigurar o proxy em runtime (ex: blue-green dinâmico).
TLS Termination vs mTLS interno
TLS termination no edge (descriptografar no proxy, HTTP simples internamente) é suficiente quando a rede interna é confiável — Kubernetes cluster isolado, VPC privada. O ganho é simplificação: backends não gerenciam certificados, e inspeção de conteúdo (WAF, logging, transformações) é possível. mTLS interno (re-encrypt ou service mesh) é necessário em zero trust networks, quando regulatório exige criptografia fim a fim, ou quando backends comunicam entre data centers. O custo do mTLS é overhead de CPU (~1-5% para TLS 1.3) e complexidade de gerenciamento de certificados internos — amortizado por ferramentas como cert-manager no Kubernetes.
Active vs Passive health checks
Passive health checks (baseados em tráfego real) têm zero overhead mas só detectam falha depois que clientes reais são afetados — ao menos 1 request falha antes do backend ser marcado como down. Active health checks (probes periódicos) detectam falha antes do tráfego ser impactado, mas adicionam conexões extras e podem mascarar falhas intermitentes (o probe passa, requests reais falham). A combinação ideal é: active health checks para detecção rápida de backends completamente down, passive para detecção de degradação sutil (aumento de erros sem queda total). nginx OSS só tem passive; nginx Plus e Envoy têm ambos nativamente.
Sticky sessions vs stateless backends
Sticky sessions (ip_hash, cookie-based) são uma solução paliativa para estado em memória — mantêm o usuário no mesmo backend, mas criam hotspots (uma instância com muitos usuários ativos recebe mais carga), complicam rolling deploys (instância com sessões ativas não pode ser removida facilmente), e falham quando o backend reinicia (sessão perdida). A solução correta é externalizar o estado: Redis para sessões HTTP, WebSocket com state em banco de dados, tokens stateless (JWT). Com backends stateless, qualquer algoritmo de load balancing funciona, rolling deploys são triviais, e scale horizontal é elástico. Reserve sticky sessions para casos onde externalizar estado é genuinamente impossível (ex: proxying de banco de dados com transações longas).

Exercícios práticos

  1. Configure um nginx como reverse proxy para dois backends: um serviço REST na porta 8080 e um serviço gRPC na porta 9090. Implemente TLS termination, least_conn load balancing, active health checks, e rate limiting de 50 req/s por IP. Critério: curl -k https://localhost/api/hello retorna 200; grpcurl -insecure localhost:443 orders.v1.OrderService/List retorna resposta válida; rajada de 60 requests em 1s recebe HTTP 429 nos excedentes; ao desligar um backend, ele é removido do pool em ≤15s sem erros para o cliente.
  2. Reproduza um problema de sticky session: crie dois backends que armazenam um contador em memória (incrementado a cada request). Configure round-robin sem sticky session e verifique que o contador aparece inconsistente entre requisições. Adicione ip_hash e compare. Critério: sem ip_hash, 10 requests consecutivos de um mesmo cliente mostram contadores alternados entre 2 sequências independentes (uma por backend); com ip_hash, todos os 10 requests vão para o mesmo backend e o contador incrementa monotonicamente — verificável nos logs de acesso.
  3. Implemente proxy caching com nginx para um endpoint de produtos. Adicione o header X-Cache-Status e monitore taxas de HIT/MISS. Configure stale serving para que o cache sirva respostas antigas enquanto o backend está em deploy. Critério: segundo request ao mesmo endpoint retorna X-Cache-Status: HIT com latência ≤5ms (vs ≥50ms do backend real); ao desligar o backend completamente, requests continuam sendo servidos como STALE durante a janela configurada; ao religar, próximo request marca REVALIDATED.
  4. Configure o Caddy com TLS automático apontando para Let's Encrypt staging (flag acme_ca). Compare o esforço com nginx + certbot. Documente o que o Caddy faz automaticamente. Critério: servidor acessível via HTTPS com certificado válido (mesmo que staging); o Caddyfile completo com TLS + reverse_proxy + headers de segurança + gzip tem ≤25 linhas; a configuração nginx equivalente tem ≥80 linhas (certbot + nginx.conf + cron de renovação).
  5. Analise um slow HTTP attack: use slowloris para enviar headers HTTP extremamente lentos para um nginx com worker_connections 50 sem timeout de header configurado. Depois adicione client_header_timeout 5s e repita. Critério: sem proteção, 60 conexões slowloris em 30s esgotam as worker connections e requests legítimos recebem timeout (sem resposta); com client_header_timeout 5s, as conexões slowloris são fechadas pelo nginx após 5s e requests legítimos continuam recebendo 200 durante o ataque.

Perguntas de entrevista

Explique TLS termination, SSL passthrough e TLS re-encryption — quando usar cada um?

TLS termination: o proxy descriptografa o TLS na borda e encaminha HTTP simples para o backend. Vantagens: backend não precisa gerenciar certificados, o proxy pode inspecionar, manipular e comprimir o conteúdo, e a carga de CPU do TLS fica centralizada em hardware otimizado (ou offloaded para NICs com suporte a TLS). Use quando a rede interna é confiável e inspeção de conteúdo é necessária (WAF, logging, transformações).

SSL passthrough: o proxy encaminha bytes TLS sem descriptografar, roteando pelo SNI (Server Name Indication) no handshake. O backend recebe e descriptografa. Vantagens: criptografia genuinamente fim a fim, backend mantém controle do certificado. Limitação: o proxy não pode inspecionar ou modificar o conteúdo, e não pode fazer load balancing por camada 7 (só L4 por IP/porta). Use em compliance que exige E2E encryption ou quando o backend precisa do certificado para autenticação de cliente.

TLS re-encryption: o proxy descriptografa, processa, e re-criptografa com um certificado interno antes de encaminhar. Criptografia fim a fim mantida, mas o proxy pode inspecionar. Custo: dois handshakes TLS (cliente-proxy e proxy-backend), gerenciamento de PKI interna. Use em zero trust networks ou quando dados são tão sensíveis que mesmo a rede interna não é confiável.

mTLS: variante onde tanto o proxy quanto o backend apresentam certificados para autenticação mútua. Padrão em service meshes — garante que apenas sidecars autorizados se comunicam, independente de regras de rede. Requer CA interna (cert-manager, Vault) para emitir e renovar certificados de serviço.

Como connection pooling resolve o problema de overhead de TCP/TLS em proxies de alta carga?

Sem connection pooling, cada requisição HTTP cria uma nova conexão TCP (3-way handshake: ~0.5 RTT) e, se TLS, um handshake adicional (~1 RTT em TLS 1.3, ~2 RTTs em TLS 1.2). Para uma API com latência de backend de 10ms e RTT de 5ms, o overhead de conexão representa 50-100% da latência total. Em sistemas de alta carga com centenas de requests/s, o custo acumulado de criação de conexões esgota CPU do proxy e file descriptors.

Connection pooling mantém um conjunto de conexões TCP persistentes (keepalive) entre o proxy e cada backend — quando uma requisição termina, a conexão volta ao pool em vez de ser fechada. Novas requisições pegam uma conexão do pool sem handshake. O nginx configura isso com keepalive N no bloco upstream, onde N é o número máximo de conexões idle por worker.

Dois parâmetros críticos: keepalive_timeout (fechar conexões idle após N segundos — evitar conexões zumbis) e keepalive_requests (reciclar conexão após N requests — evitar memory leaks em proxies como Go que não fecham nunca a conexão). Em sistemas de muito alto throughput (>50k req/s), HTTP/2 multiplexando múltiplas requisições sobre uma conexão é ainda mais eficiente que HTTP/1.1 keepalive.

Por que ip_hash como sticky session é problemático? Quais são as alternativas para estado de sessão?

ip_hash tem três problemas estruturais. Primeiro, desequilíbrio de carga: em ambientes corporativos, todos os usuários de uma empresa saem pelo mesmo IP de NAT — todo o tráfego deles vai para uma única instância, criando hotspot enquanto outras ficam ociosas. Segundo, rolling deploys complicados: ao remover uma instância do pool, todas as sessões nela são perdidas (o cliente começa em um estado zerado em outra instância). Terceiro, falsa sensação de segurança: o IP do cliente pode mudar (usuário móvel, reconnection) quebrando a sessão.

Cookie-based sticky sessions (nginx Plus, HAProxy, Traefik) são melhores: o proxy define um cookie com o ID do backend na primeira resposta, e usa esse cookie para rotear requisições subsequentes. Resolve o problema de NAT e é mais previsível para deploys (você pode drenar um backend esperando cookies expirarem).

A solução arquitetural correta é eliminar a necessidade de sticky sessions externalizando o estado: Redis ou Memcached para sessões HTTP, tokens JWT stateless para autenticação (state no token, não no servidor), ou banco de dados para qualquer estado de longo prazo. Com backends stateless, qualquer algoritmo de load balancing funciona, deploys são triviais, e scale horizontal é automático. Sticky sessions são um cheiro arquitetural — indica estado local que deveria ser externalizado.

Como o modelo xDS do Envoy difere da configuração estática do nginx, e quais problemas operacionais cada abordagem resolve?

nginx usa configuração baseada em arquivos: você edita nginx.conf, testa (nginx -t), e recarrega (nginx -s reload). O reload é "sem downtime" (graceful), mas ainda envolve um ciclo de sinalização ao processo e alguns millisegundos de transição. Em ambientes onde backends mudam frequentemente (Kubernetes com pods efêmeros), recarregar o nginx a cada mudança de endpoint gera overhead e atraso.

O Envoy resolve isso com xDS: um conjunto de APIs gRPC que configuram o Envoy em runtime sem qualquer reload. O plano de controle (Istiod no Istio, Consul agent, ou um servidor xDS customizado) mantém a configuração canônica e empurra atualizações para todos os Envoys. LDS atualiza listeners, RDS atualiza rotas, EDS atualiza endpoints individuais — em millisegundos, sem downtime, sem reload. Isso é fundamental para service meshes onde instâncias sobem e descem constantemente.

A desvantagem do Envoy: xDS é um protocolo complexo e a API de configuração (protobuf verbose) tem curva de aprendizado alta. Debugar uma configuração Envoy gerada pelo Istio (que tem centenas de rotas) requer ferramentas especializadas (istioctl, envoy admin /config_dump). nginx com sua configuração declarativa simples é muito mais fácil de raciocinar para casos simples. A escolha resume-se a: ambientes estáticos → nginx; ambientes altamente dinâmicos com orchestration → Envoy.

O que é um slow HTTP attack e como o reverse proxy pode mitigá-lo?

Slow HTTP attacks (Slowloris, RUDY, Slow POST) exploram servidores que mantêm conexões abertas enquanto aguardam requisições completas. Slowloris abre centenas de conexões e envia headers HTTP com um byte por vez em intervalos longos — o servidor aguarda o header completo, esgotando worker connections sem jamais completar uma requisição. RUDY (R U Dead Yet?) faz o mesmo com o body de POST requests. O ataque é devastador porque usa pouca banda e poucos pacotes, difíceis de distinguir de clientes lentos legítimos.

Mitigações no reverse proxy: client_header_timeout fecha conexões que não completam o header em N segundos (nginx default é 60s — muito alto; 5-10s é suficiente). client_body_timeout faz o mesmo para o body. limit_conn limita conexões simultâneas por IP. limit_req limita requests por segundo. Juntos, esses parâmetros tornam o ataque ineficaz: cada conexão Slowloris expira em 5s em vez de manter um worker indefinidamente.

No nível de rede: rate limiting por IP no load balancer externo (CloudFront, Cloudflare) antes que o tráfego chegue ao nginx. Cloudflare e serviços similares têm detecção automática de Slowloris baseada em comportamento de sessão. Para aplicações críticas, um WAF (Web Application Firewall) na borda é mais robusto que configuração manual de timeouts — ele analisa padrões de tráfego e bloqueia IPs proativamente.

Referências

  1. docs nginx Documentation — nginx Inc. nginx.org/en/docs/ — Referência oficial dos módulos de proxy (ngx_http_proxy_module), upstream (ngx_http_upstream_module), SSL (ngx_http_ssl_module), cache e rate limiting. Inclui exemplos de configuração de conexões keepalive, health checks passivos e buffering.
  2. docs Envoy Proxy Documentation — Envoy / CNCF. envoyproxy.io/docs — Documentação oficial do Envoy incluindo xDS API reference (LDS, RDS, CDS, EDS, SDS), filtros HTTP (JWT, rate limiting, gRPC-JSON transcoding), e o modelo de extensibility via filtros. Essencial para entender o data plane de service meshes modernos.
  3. docs HAProxy Configuration Manual — HAProxy Technologies. docs.haproxy.org — Referência completa de todas as diretivas, ACLs, backends e health checks do HAProxy. Único na classe por suportar proxying L4 (TCP) e L7 (HTTP) na mesma instância, com documentação detalhada de timeouts, stickiness e failover.
  4. docs Caddy Documentation — Matt Holt. caddyserver.com/docs — Documentação do Caddy incluindo Caddyfile (DSL declarativa), JSON API de administração para configuração dinâmica, e o sistema de TLS automático via Let's Encrypt e ZeroSSL. Referência para entender como TLS pode ser zero-config para projetos de menor escala.
  5. docs Traefik Documentation — Traefik Labs. doc.traefik.io/traefik — Documentação do Traefik com foco em discovery automático via labels Docker e annotations Kubernetes, middlewares encadeáveis (auth, rate limit, retry, circuit breaker), e integração nativa com Let's Encrypt. Referência para ambientes de containers sem configuração manual de rotas.
  6. livro High Performance Browser Networking — Ilya Grigorik (O'Reilly, 2013). hpbn.co — Disponível gratuitamente online. Capítulos sobre TLS (handshake, session resumption, OCSP stapling), HTTP/1.1 keepalive vs HTTP/2 multiplexing, e otimização de latência de rede. Base teórica para entender por que as configurações de proxy afetam performance percebida.
  7. standard RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3 — IETF (2018). datatracker.ietf.org/doc/html/rfc8446 — Especificação do TLS 1.3: handshake de 1 RTT (vs 2 RTT do TLS 1.2), 0-RTT session resumption, remoção de cipher suites obsoletos e Perfect Forward Secrecy obrigatória. Referência para entender as implicações de segurança de cada modo TLS configurado no proxy.
  8. standard RFC 9110 — HTTP Semantics — IETF (2022). datatracker.ietf.org/doc/html/rfc9110 — Especificação normativa do protocolo HTTP: semântica de métodos, headers, status codes, caching e content negotiation. Base para entender o comportamento correto de proxies ao encaminhar e transformar requisições e respostas.
  9. standard RFC 7239 — Forwarded HTTP Extension — IETF (2014). datatracker.ietf.org/doc/html/rfc7239 — Especificação do header Forwarded como substituto padronizado dos X-Forwarded-For, X-Forwarded-Proto e X-Forwarded-Host. Explica o formato, semântica de múltiplos proxies em cadeia, e considerações de segurança sobre confiabilidade dos valores.
  10. paper The Power of Two Random Choices — Michael Mitzenmacher (2001). Surveys in Combinatorics — Prova matemática de que selecionar 2 candidatos aleatoriamente e escolher o melhor produz distribuição de carga quase tão boa quanto Least Connections global (O(log log N) vs O(log N) de carga máxima), com custo O(1). Base do algoritmo "Power of Two Choices" implementado no Envoy e em proxies modernos de alta escala.
  11. artigo NGINX Architecture Overview — Igor Sysoev / nginx.com. nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale — Explica o modelo event-driven do nginx (epoll/kqueue), master/worker processes, e por que o design foi escolhido vs Apache's process-per-connection. Contexto histórico para entender as limitações e vantagens do nginx frente a alternativas mais novas.
  12. paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google (2010). research.google/pubs/pub36356 — Paper fundacional de distributed tracing que influenciou OpenTelemetry e o modelo de tracing no Envoy. Relevante para entender como reverse proxies propagam trace context (B3, W3C Trace Context) e contribuem para observabilidade de sistemas distribuídos.