Pular para o conteúdo

ADR-0017 Reverse proxy e TLS

Norma da empresa para o edge — o Traefik em cada host termina o TLS, define o IP de cliente de confiança, filtra por /readyz e limita floods; sem CDN por agora, com o risco de flood volumétrico aceite.

Decisão
Traefik 3.7, sem CDN por agoraEntregar e operar
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

Os ADRs anteriores empurraram várias tarefas para o edge:

  • Terminação de TLS — nossa, uma vez que fazemos self-host (ADR-0003)
  • Um IP de cliente fiável — o rate limiter usa-o como chave do tráfego anónimo, pelo que tem de vir de um header que o cliente não possa forjar (ADR-0010)
  • Proteção contra floods — um limiter ao nível da app continua a aceitar todas as ligações; os floods têm de parar antes do servidor da app (ADR-0010)
  • Encaminhar apenas para instâncias prontas — via /readyz, para que os deploys não percam pedidos (ADR-0015, ADR-0016)
  • Uma origem pública conhecida — para a verificação CSRF do Start e para o Better Auth (ADR-0009, ADR-0013)

O deploy é trabalho do Nodus (ADR-0016), e o Nodus pode já fornecer parte disto. Por isso este ADR fixa o que o edge tem de fazer e indica uma implementação por omissão; se o Nodus fornecer um edge, este tem de cumprir os mesmos requisitos.

Dois factos condicionam o desenho:

  • Nenhum proxy no nosso próprio host consegue absorver um flood volumétrico — a ligação de rede satura antes de o proxy ver um pacote. Só uma rede com mais capacidade do que a do atacante o consegue, o que significa um CDN ou um serviço de scrubbing à frente
  • Os prazos de validade dos certificados estão a encurtar: o perfil tlsserver opcional do Let’s Encrypt emite certificados de 45 dias desde 2026-05-13; o perfil classic por omissão passa a 64 dias em 2027-02-10 e a 45 dias em 2028-02-16. A renovação tem de ser totalmente automática
Prazo de validade dos certificados Let's Encrypt, em dias
  • classic (por omissão)
  • tlsserver (opcional)
Ver como tabela
PerfilValidade
classic (por omissão)90 dias, até 2027-02-10
classic (por omissão)64 dias, a partir de 2027-02-10
classic (por omissão)45 dias, a partir de 2028-02-16
tlsserver (opcional)45 dias, desde 2026-05-13

Prós: absorve floods L3–L7 numa rede construída para isso; regras de WAF e de bots; cache no edge. Contras: termina o TLS, pelo que vê texto simples — cookies de sessão, dados de formulários, dados pessoais. A Cloudflare é uma empresa norte-americana (as transferências assentam no EU-U.S. Data Privacy Framework, com cláusulas contratuais-tipo como alternativa) e um ponto único de falha para todas as apps com proxy — teve falhas globais de cerca de seis horas em 2025-11-18 e 2026-02-20. A Bunny é sediada na UE (Eslovénia), o que elimina a questão da jurisdição, mas não a do texto simples. Em qualquer dos casos, um custo mensal e um fornecedor em cada pedido.

2. Apenas a proteção DDoS de rede do fornecedor de alojamento

Seção intitulada “2. Apenas a proteção DDoS de rede do fornecedor de alojamento”

Prós: nenhum terceiro vê texto simples; normalmente incluída no alojamento. Contras: só scrubbing L3/L4, e a sua robustez varia consoante o fornecedor; os floods L7 chegam ao nosso proxy.

Prós: o mais simples; sem fornecedor. Contras: um flood a sério deita o host abaixo.

Prós: MIT; rateLimit integrado (por IP de cliente, IPv6 agrupado por subnet) e inFlightReq; health checks ativos; ACME automático; HTTP/3; descoberta por labels Docker; traces e métricas OTLP (parâmetros de query ocultados nos traces por omissão). Como primeiro salto, descarta os headers X-Forwarded-* e X-Real-Ip enviados pelo cliente e define X-Real-Ip a partir da ligação TCP. Contras: a descoberta Docker precisa de acesso à API do Docker — equivalente a root, a menos que passe por um proxy; a configuração fica espalhada pelas labels dos contentores.

Prós: a configuração mais simples; HTTPS automático; health checks ativos; ignora por omissão X-Forwarded-* forjados. Contras: o rate limiting é um módulo de terceiros (caddy-ratelimit) que exige um build xcaddy personalizado.

Prós: os controlos mais fortes de ligações e de taxa de pedidos (stick tables) e de balanceamento de carga; LTS até 2031. Contras: o suporte ACME é experimental e só para uma instância; a configuração mais exigente.

Prós: omnipresente; limit_req e limit_conn integrados. Contras: os health checks ativos são só comerciais (NGINX Plus), pelo que o /readyz não pode condicionar o tráfego; o ACME precisa do jovem módulo nginx-acme (0.4) ou do certbot.

O Traefik 3.7 em cada host é o edge: termina o TLS, define o IP de cliente, encaminha apenas para instâncias prontas e limita floods. Sem CDN por agora — aceitamos que um flood volumétrico possa deitar um host abaixo. Qualquer filtragem DDoS que o fornecedor de alojamento inclua é bem-vinda, mas não contamos com ela. Qualquer edge — incluindo um fornecido pelo Nodus — tem de cumprir os requisitos abaixo. Esta é a norma da empresa para todas as novas apps.

  1. TLS em todos os pedidos públicos, com certificados renovados automaticamente. TLS 1.2 no mínimo, 1.3 preferido; o HTTP simples apenas redireciona para HTTPS e serve desafios ACME
  2. Um único header de IP de cliente de confiança, definido pelo edge a partir da ligação TCP e a substituir tudo o que o cliente tenha enviado
  3. Limites de flood antes do servidor da app: taxa de pedidos por IP, pedidos concorrentes por IP, tamanho do corpo do pedido e timeouts para clientes lentos
  4. Tráfego apenas para instâncias prontas, avaliadas por GET /readyz
  5. A porta da app é inacessível a partir do exterior; só o edge fala com ela
  6. Os endpoints de saúde não são públicos: /healthz e /readyz respondem ao edge e ao Docker, não à internet
  • Traefik 3.7, fixado, um por host, o único contentor a publicar portas (80/443, mais UDP 443 para HTTP/3). A app, o Postgres, o Valkey e o Collector não publicam nada; o Traefik chega às apps pela rede Docker interna

  • Certificados do Let’s Encrypt via HTTP-01, renovados automaticamente, pelo que prazos de validade mais curtos não exigem qualquer ação. A porta 80 serve apenas desafios ACME e redirecionamentos. DNS-01 apenas quando um produto precisar de certificados wildcard, com um token de API de DNS limitado às edições de DNS dessa zona

  • O IP de cliente é X-Real-Ip. Como primeiro salto, o Traefik apaga os X-Forwarded-* e o X-Real-Ip enviados pelo cliente e define X-Real-Ip a partir da ligação TCP. Isto só se mantém enquanto o ponto de entrada público não tiver forwardedHeaders.trustedIPs e nunca tiver forwardedHeaders.insecure — qualquer um deles permitiria aos clientes forjar o seu IP. As apps definem CLIENT_IP_HEADER=x-real-ip, usado pelo rate limiter (ADR-0010) e pelo Better Auth (advanced.ipAddress.ipAddressHeaders). Um teste de guarda contra staging envia um X-Real-Ip forjado através do Traefik e verifica que é ignorado (ADR-0014)

  • Descoberta: labels Docker através de um proxy de socket Docker só de leitura que permite apenas listar contentores — nunca o socket em bruto, que é root no host. Se o Nodus escrever a configuração de routing, o file provider do Traefik substitui as labels

  • Balanceamento de carga com health checks: o loadBalancer.healthCheck de cada serviço sonda /readyz a cada 5 s com um timeout de 2 s; as instâncias que falham saem do pool. Espera-se que os rolling deploys do Nodus arranquem a nova instância, esperem que passe e só então parem a antiga

  • Os routers públicos não servem /healthz nem /readyz — os health checks do Traefik vão diretamente ao contentor

  • Limites de flood (valores iniciais, ajustar por produto):

    Controlo Middleware / definição Valor inicial
    Pedidos por IP rateLimit, IP de cliente a partir da ligação, ipv6Subnet: 64 50/s em média, burst de 100
    Concorrentes por IP inFlightReq, mesma chave 20
    Tamanho do corpo do pedido buffering.maxRequestBodyBytes 10 MB
    Clientes lentos respondingTimeouts do entry point leitura 30 s, escrita 60 s, inatividade 90 s

    Estão bem acima dos limites por utilizador e por endpoint da app (ADR-0010) — travam floods, não a utilização intensiva normal. Os contadores ficam na memória do Traefik: o edge nunca tem acesso ao Valkey, que guarda tokens de sessão

    Pedidos permitidos por minuto, por chave (escala logarítmica)
    • Rate limiter da app (ADR-0010)
    • Edge, Traefik (este ADR)
    Ver como tabela
    LimiteCamadaPermitido
    Credenciais, por emailRate limiter da app (ADR-0010)10 por 15 min
    Credenciais, por IPRate limiter da app (ADR-0010)3 por 10 s
    Escritas, por IPRate limiter da app (ADR-0010)100 por min
    Escritas, por utilizadorRate limiter da app (ADR-0010)300 por min
    Qualquer pedido, por IPEdge, Traefik (este ADR)50/s em média
  • Hardening do host abaixo do Traefik, uma vez que nada filtra à sua frente: SYN cookies do kernel ativos (net.ipv4.tcp_syncookies=1), tabela de connection tracking dimensionada para o host, e um limite nftables por IP de origem em novas ligações para 80/443, para que um único IP não possa esgotar as ligações do Traefik. A firewall do host abre apenas 80/443 (e SSH a partir da rede de gestão)

  • Headers: X-Forwarded-Proto e Host passam; o HSTS (max-age=63072000; includeSubDomains) é definido no edge. Os restantes headers de segurança (CSP e afins) pertencem à app

  • A origem pública é configuração, não inferência: cada app define APP_ORIGIN e passa-o a createCsrfMiddleware({ origin }) (ADR-0013) e ao baseURL do Better Auth. Nada depende do URL do pedido reconstruído pelo proxy

  • Observabilidade: o Traefik exporta traces e métricas por OTLP para o endpoint que a plataforma fornece (ADR-0015); os access logs são JSON em stdout. A exportação de logs OTLP fica desligada enquanto o Traefik a marcar como experimental. Alertar sobre taxas sustentadas de 429 e sobre contagens de ligações — o primeiro sinal de um flood

Rever esta decisão — e adicionar a Bunny ou a Cloudflare à frente do Traefik — quando acontecer qualquer um destes casos:

  • um ataque sustentado deitar um host abaixo, ou os alertas de 429 e de ligações do Traefik dispararem repetidamente
  • um produto passar a ter um compromisso de disponibilidade (SLA) ou a tratar pagamentos
  • um lançamento público ou uma campanha esperar tráfego bem acima do que um só host suporta

Os requisitos acima já descrevem uma configuração com CDN: o header de IP de cliente passa a ser o do CDN, a firewall abre 80/443 apenas aos intervalos do CDN, e o TLS da origem é verificado pelo CDN.

Os fatores decisivos: nenhum terceiro a ver tráfego em texto simples e nenhum fornecedor em cada pedido, um edge com controlos de flood, encaminhamento com health checks e TLS automático integrados, e um IP de cliente que nenhum cliente consegue forjar. Aceitamos que os floods volumétricos fiquem fora do nosso alcance até se adicionar um CDN.

Mais fácil:

  • O TLS termina nos nossos próprios hosts; nenhum terceiro vê cookies de sessão nem dados pessoais
  • O rate limiting, o Better Auth e os logs veem todos o mesmo IP de cliente real, vindo da ligação TCP
  • Os deploys não perdem pedidos: o tráfego só passa para instâncias cujo /readyz passe
  • Os certificados renovam-se sozinhos, por mais curtos que os prazos de validade fiquem
  • Sem fatura de CDN, contrato nem acordo de tratamento de dados

Mais difícil:

  • Um flood volumétrico ou distribuído de grande escala deita o host abaixo, e todas as apps nele. Os limites do Traefik e do nftables travam abuso de origem única e L7 moderado, não uma botnet que se mantém abaixo dos limites por IP nem um flood que sature a ligação
  • O Traefik está diretamente na internet: aplicar patches de imediato e manter as suas definições de TLS atualizadas
  • Uma única má configuração — adicionar trustedIPs ou insecure ao ponto de entrada público — torna todos os IPs de cliente forjáveis; o teste de guarda tem de se manter
  • Sem WAF nem proteção contra bots; o tratamento de abuso é nosso, na app e no Traefik
  • O Traefik, o seu proxy de socket, as regras nftables e qualquer token de API de DNS são nossos para operar e rodar
  • Os limites iniciais são julgamento, não medição; ajustá-los por produto a partir das métricas do Traefik e da app

Do que abdicamos: da absorção de floods numa rede maior do que a nossa, e do WAF, das regras de bots e da cache de edge de um CDN. Aceite por agora — manter o tráfego em texto simples nos nossos hosts e evitar um fornecedor em cada pedido pesa mais do que o risco de flood na nossa exposição atual; os critérios acima dizem quando isso muda.