Forward proxy vs Reverse proxy
A distinção entre os dois tipos de proxy é sobre quem o proxy representa:
- Forward proxy: o cliente o configura explicitamente. O proxy faz requisições em nome do cliente para a internet — usado para controle de acesso corporativo, anonimização (Tor), caching de rede corporativa. O servidor de destino vê o IP do proxy, não do cliente real.
- Reverse proxy: o cliente não sabe de sua existência. O proxy fica na borda da infraestrutura do servidor — o cliente acessa
api.empresa.come o DNS aponta para o proxy. O proxy encaminha para o backend real. O cliente vê o IP do proxy como se fosse o servidor.
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: descriptografa HTTPS na borda, mantendo HTTP simples internamente
- Load balancing: distribui carga entre múltiplas instâncias do backend
- Health checking: remove instâncias não-saudáveis do pool automaticamente
- Caching: armazena respostas e as serve sem chegar ao backend
- Compressão: comprime respostas (gzip, brotli) sem que o backend precise
- Rate limiting: limita requests por cliente antes de chegarem ao backend
- Autenticação/autorização: valida tokens antes de encaminhar
- SSL passthrough: em alternativa à termination, encaminha TLS criptografado para o backend
- Rewriting de URL: modifica paths antes de encaminhar (ex:
/api/v1/orders→/orders) - Logging e observabilidade: ponto único para registrar todas as requisições
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
| Modo | Como funciona | Quando 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:
- OCSP Stapling: o proxy inclui a resposta OCSP (verificação de revogação do certificado) diretamente no handshake TLS, eliminando um round-trip adicional que o cliente faria ao CA. Reduz latência do primeiro request em ~100-300ms.
- TLS Session Tickets / Session IDs: permite que clientes retomem sessões TLS anteriores sem renegociar o handshake completo. Reduz latência de reconexão de ~250ms (full handshake) para ~0ms (resumption).
- TLS 1.3: reduz o handshake de 2 round-trips (TLS 1.2) para 1, e suporta 0-RTT para clientes que já estabeleceram sessão — com cuidados sobre replay attacks.
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:
- Readiness (
/ready): o serviço está pronto para receber tráfego. Falha indica que o serviço existe mas não deve receber carga (warmup, carregando cache, esperando dependência). O proxy remove do pool mas não reinicia. - Liveness (
/health): o serviço está vivo. Falha indica processo travado, deadlock, ou estado corrompido — o orquestrador reinicia o container. - Startup (
/startup): o serviço terminou de inicializar. Usado quando a inicialização é lenta e o liveness probe daria falso positivo.
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;
}
}
/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;
}
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
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:
- LDS (Listener Discovery Service): configura listeners (portas e filtros)
- RDS (Route Discovery Service): configura regras de roteamento
- CDS (Cluster Discovery Service): configura upstreams (clusters)
- EDS (Endpoint Discovery Service): configura instâncias de cada cluster
- SDS (Secret Discovery Service): distribui certificados TLS dinamicamente
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
| Proxy | Melhor para | Evitar 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 |
Decisões de engenharia
Exercícios práticos
- 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/helloretorna 200;grpcurl -insecure localhost:443 orders.v1.OrderService/Listretorna 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. - 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.
- Implemente proxy caching com nginx para um endpoint de produtos. Adicione o header
X-Cache-Statuse 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 retornaX-Cache-Status: HITcom latência ≤5ms (vs ≥50ms do backend real); ao desligar o backend completamente, requests continuam sendo servidos comoSTALEdurante a janela configurada; ao religar, próximo request marcaREVALIDATED. - 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). - Analise um slow HTTP attack: use
slowlorispara enviar headers HTTP extremamente lentos para um nginx comworker_connections 50sem timeout de header configurado. Depois adicioneclient_header_timeout 5se repita. Critério: sem proteção, 60 conexões slowloris em 30s esgotam as worker connections e requests legítimos recebem timeout (sem resposta); comclient_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
- docs nginx Documentation — nginx Inc.
- docs Envoy Proxy Documentation — Envoy / CNCF.
- docs HAProxy Configuration Manual — HAProxy Technologies.
- docs Caddy Documentation — Matt Holt.
- docs Traefik Documentation — Traefik Labs.
- livro High Performance Browser Networking — Ilya Grigorik (O'Reilly, 2013).
- standard RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3 — IETF (2018).
- standard RFC 9110 — HTTP Semantics — IETF (2022).
- standard RFC 7239 — Forwarded HTTP Extension — IETF (2014).
- paper The Power of Two Random Choices — Michael Mitzenmacher (2001).
- artigo NGINX Architecture Overview — Igor Sysoev / nginx.com.
- paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google (2010).