Skip to content

mTLS Traefik ↔ backend: o apiServer exige certificado de cliente da CA do RPC (#90) - #113

Merged
hbgit merged 12 commits into
developfrom
feat/issue-90-mtls-traefik-backend
Oct 27, 2026
Merged

hbgit merged 12 commits into
developfrom
feat/issue-90-mtls-traefik-backend

Conversation

@hbgit

@hbgit hbgit commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Fecha a #90 (continuação da #78 / PR #89): o trecho Traefik → backend passa a TLS 1.3 mútuo. O backend exige, no handshake da 8080 interna, um certificado de cliente assinado pela CA de desenvolvimento do RPC; o Traefik apresenta o seu pelo serversTransport.certificates; qualquer processo na rede do Compose sem o certificado é recusado antes de qualquer byte HTTP.

Plano executado: docs/superpowers/plans/2026-10-27-issue-90-mtls-traefik-backend.md (decisões do dono registradas em 2026-10-27).

Antes / depois (comando-evidência, outro container na rede sinalacs_default)

rede="$(docker inspect sinalacs-serverpod -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')"
docker run --rm --network "$rede" -v "$PWD/infra/docker/traefik/runtime/backend/ca.crt:/ca.crt:ro" \
  curlimages/curl:8.10.1 -sS --tlsv1.3 --cacert /ca.crt -X POST -H 'Content-Type: application/json' -d '{}' \
  -w '\nHTTP %{http_code}\n' https://sinalacs-serverpod:8080/health/check
  • antes: {"__className__":"ServiceHealth","status":"ok",…} / HTTP 200 (o servidor nunca enviava Request CERT)
  • depois: curl: (56) … SSL_ERROR_SYSCALL / HTTP 000 — nenhuma linha HTTP

Decisão de arquitetura (ADR em spec/stack.md)

O Serverpod é consumido por um fork pinado por SHA (hbgit/serverpod@2a0b67e, dependency_overrides em backend/pubspec.yaml, patch versionado em backend/patches/). Motivo medido: Server.start chama RelicApp.serve fixo no 3.4.13, 4.0.4 e 4.1.0-rc.1; o relic_io não expõe requestClientCertificate; e o HttpServer.bindSecure do SDK só pede o certificado — com requestClientCertificate: true o handshake completa sem certificado (seria 401 na aplicação, não recusa). A recusa exige SecureServerSocket.bind(requireClientCertificate: true) + HttpServer.listenOn, só possível se a aplicação ligar o socket. O patch é um gancho genérico (apiServerAdapterFactory), proposto upstream contra o main: serverpod/serverpod#5923. Critério de saída e custo de upgrade em backend/patches/README.md.

O que muda

  • ApiTlsConfig: API_TLS_CLIENT_CA_PATH obrigatória junto com cert e chave; subconjunto (um .env anterior à mTLS Traefik ↔ backend bloqueado pelo Serverpod (requestClientCertificate) #90) → mensagem com as três no stderr e exit 78, nunca TLS de mão única em silêncio.
  • lib/src/runtime/mtls_api_server.dart: bind com requireClientCertificate: true, embrulho ServerSocket com handleError (recusas contadas, log só do tipo), adapter cujo close fecha o socket (o listenOn do SDK não fecha).
  • traefik-init: duas folhas clientAuth distintas — traefik-client (chave 600, só o Traefik) e healthcheck (CN próprio, 90 dias, 644).
  • dynamic/tls.yaml: certificates no backend-tls (serverName/rootCAs intocados). Compose: API_TLS_CLIENT_CA_PATH, healthcheck com a folha dele.
  • Dockerfile copia o pubspec.yaml real para o override sobreviver ao build (clone anônimo do fork público).

Provas

  • Backend: dart test -j 1 1573/1573; novos api_tls_config_test (11), mtls_api_server_test (9), server_wiring_test, serverpod_fork_test.
  • scripts/qa/tls_invariants.sh 20/20; Grupo E reescrito para sondar de fora da rede: E5 sem certificado e E6 outra CA (mesmo CN) recusados; E2 com certificado 200; E3 TLS 1.2 recusado; E8 20 recusas seguidas sem derrubar o listener (RestartCount=0); E4 cobra certificates + API_TLS_CLIENT_CA_PATH; E7 EKU clientAuth + openssl verify; E9 folha do healthcheck > 7 dias e distinta.
  • Mutações: (a) sem certificates → 502 e E4 acusa; (b) sem API_TLS_CLIENT_CA_PATH → exit 78 com a mensagem exata, Grupo A acusa; (c) requireClientCertificate: false → E5/E6 acusam; (d) wrapper relançando como StateError → segue verde (o SDK/relic protegem; o handleError é defesa em profundidade).
  • mtls_invariants.sh 4/4, dev_key_permissions_test.sh ok, cobertura_prd.py --final ok.
  • Emulador (emulator-5554, API 36): e2e.sh --emulator --keep verde (live_check do paciente e do ACS + smoke dos dois apps pelo Traefik). acs_full_e2e.sh: 5/6 em duas execuções — falha só o 1º teste, num tap local em sync_visits coberto pela SnackBar "visita salva", antes de qualquer rede; apps/ é idêntico ao develop; o último verde desse script é o da [P1] Telemetria estruturada dos eventos do §1.3 (North Star e hipóteses) #83 (2026-10-25) e depois dele apps/acs mudou em ACK de alerta já concluído devolve "acknowledged" e o botão de concluir reaparece (RF05) #86 (e70e0c3, perto do showSnackBar) e ACK repetido reescreve o ACS do alerta e audita "acknowledged" de novo #88 (6b8c8dc) — regressão pré-existente do e2e, fora desta issue (ver PROGRESS.md; issue própria a abrir).
  • Revisão independente do núcleo (checkpoint depois da Task 5): 2 achados importantes corrigidos antes de tocar Traefik/Compose.

Critérios da issue

Critério Prova
Traefik apresenta certificado de cliente assinado pela CA do RPC (serversTransport.certificates) E4, E7, Grupo A (200 só possível com a folha), mutação (a)
Sem o certificado, conexão à 8080 interna recusada no handshake unitário + E5 + comando-evidência (HTTP 000), mutação (c)
Grupo E ganha o caso negativo e o positivo E5/E6 e E2

Fora do escopo e intocados: Postgres, Gorush, 8082/8083, 8081 — RNF04 segue parcial por eles. Abertos: produção (no Render o TLS termina na borda), rebase do fork a cada upgrade, o backend não loga o CN do cliente, rede interna como defesa em profundidade.

🤖 Generated with Claude Code

hbgit added 12 commits October 27, 2026 16:53
…o TLS sai com 78 e nomeia a variável (#90)

Achados do checkpoint de revisão: HttpServer.listenOn cria o servidor com
_closeServer=false, então o shutdown deixava a 8080 ocupada e aceitando
handshakes; e o StateError/TlsException do boot escapava do run() async
void para o runZonedGuarded da #56, que loga só o tipo (e classifica a
TlsException como transitória, saindo com código 0).
…GRESS corrige os suspeitos da regressão do acs_full_e2e (#90)
@hbgit
hbgit merged commit 5706fee into develop Oct 27, 2026
18 of 19 checks passed
@hbgit
hbgit deleted the feat/issue-90-mtls-traefik-backend branch October 27, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant