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
Ligações
Apoia-se em
Contexto
Seção intitulada “Contexto”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
tlsserveropcional do Let’s Encrypt emite certificados de 45 dias desde 2026-05-13; o perfilclassicpor 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
- classic (por omissão)
- tlsserver (opcional)
Ver como tabela
| Perfil | Validade |
|---|---|
| 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 |
Opções consideradas
Seção intitulada “Opções consideradas”Absorção de floods a montante
Seção intitulada “Absorção de floods a montante”1. Proxy CDN à frente (Cloudflare ou Bunny)
Seção intitulada “1. Proxy CDN à frente (Cloudflare ou Bunny)”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.
3. Nada a montante
Seção intitulada “3. Nada a montante”Prós: o mais simples; sem fornecedor. Contras: um flood a sério deita o host abaixo.
Reverse proxy
Seção intitulada “Reverse proxy”A. Traefik 3.7
Seção intitulada “A. Traefik 3.7”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.
B. Caddy 2.11
Seção intitulada “B. Caddy 2.11”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.
C. HAProxy 3.4 LTS
Seção intitulada “C. HAProxy 3.4 LTS”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.
D. nginx
Seção intitulada “D. nginx”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.
Decisão
Seção intitulada “Decisão”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.
Requisitos para qualquer edge
Seção intitulada “Requisitos para qualquer edge”- 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
- 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
- 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
- Tráfego apenas para instâncias prontas, avaliadas por
GET /readyz - A porta da app é inacessível a partir do exterior; só o edge fala com ela
- Os endpoints de saúde não são públicos:
/healthze/readyzrespondem ao edge e ao Docker, não à internet
Traefik
Seção intitulada “Traefik”-
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 osX-Forwarded-*e oX-Real-Ipenviados pelo cliente e defineX-Real-Ipa partir da ligação TCP. Isto só se mantém enquanto o ponto de entrada público não tiverforwardedHeaders.trustedIPse nunca tiverforwardedHeaders.insecure— qualquer um deles permitiria aos clientes forjar o seu IP. As apps definemCLIENT_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 umX-Real-Ipforjado 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.healthCheckde cada serviço sonda/readyza 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
/healthznem/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: 6450/s em média, burst de 100 Concorrentes por IP inFlightReq, mesma chave20 Tamanho do corpo do pedido buffering.maxRequestBodyBytes10 MB Clientes lentos respondingTimeoutsdo entry pointleitura 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)
Credenciais, por email10 por 15 minCredenciais, por IP3 por 10 sEscritas, por IP100 por minEscritas, por utilizador300 por minQualquer pedido, por IP50/s em médiaVer como tabela
Limite Camada Permitido Credenciais, por email Rate limiter da app (ADR-0010) 10 por 15 min Credenciais, por IP Rate limiter da app (ADR-0010) 3 por 10 s Escritas, por IP Rate limiter da app (ADR-0010) 100 por min Escritas, por utilizador Rate limiter da app (ADR-0010) 300 por min Qualquer pedido, por IP Edge, 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-ProtoeHostpassam; 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_ORIGINe passa-o acreateCsrfMiddleware({ origin })(ADR-0013) e aobaseURLdo 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
Quando adicionar um CDN
Seção intitulada “Quando adicionar um CDN”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.
Consequências
Seção intitulada “Consequências”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
/readyzpasse - 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
trustedIPsouinsecureao 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.

