From 675806314d4547bb47c84b80b57170a946be8db3 Mon Sep 17 00:00:00 2001 From: B2BCall Bootstrap Date: Thu, 27 Aug 2026 21:33:32 -0300 Subject: [PATCH] =?UTF-8?q?docs:=20registrar=20libera=C3=A7=C3=A3o=20de=20?= =?UTF-8?q?SIP/RTP=20no=20firewall=20e=20confus=C3=A3o=20de=20senhas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atualiza TODO.md/SECURITY.md para refletir a regra de nftables real (SIP/ RTP liberados para RFC1918, AMI/ARI seguem bloqueados incondicionalmente) e documenta em TROUBLESHOOTING.md o caso completo: timeout de registro de softphone causado pelo firewall + confusão entre senha de login web e senha SIP do ramal (duas credenciais diferentes). --- TODO.md | 11 +++++++++-- docs/SECURITY.md | 21 +++++++++++++++------ docs/TROUBLESHOOTING.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/TODO.md b/TODO.md index 8c5a7d8..f3da4e3 100644 --- a/TODO.md +++ b/TODO.md @@ -349,8 +349,15 @@ banco. Todos os fixtures de teste foram removidos/desativados ao final. `docs/SECURITY.md`, revisitar se o frontend algum dia sair da mesma origem da API). - [x] nftables final revisado — regras conferidas contra o estado atual - (Nginx na porta 80, AMI/ARI/SIP ainda bloqueados da interface LAN - `ens18`, SSH nunca bloqueado). Nenhuma alteração necessária. + (Nginx na porta 80, SSH nunca bloqueado). **Atualizado após a Fase + 10**: o bloqueio incondicional de SIP/RTP na interface LAN + inviabilizava o uso real de softphones (um agente de verdade nunca + conseguiria registrar, já que o OpenSIPS não foi implantado — + `docs/OPENSIPS.md`). Corrigido liberando SIP (5060/udp) e RTP + (10000-20000/udp) para toda a faixa RFC1918, mantendo AMI/ARI + bloqueados incondicionalmente mesmo de origem privada. Achado em + produção real: usuário tentou registrar um softphone e o contador de + drop da regra confirmou 109 pacotes descartados. - [x] Logs estruturados JSON (sem segredos) — confirmado: `redact.paths` no `nestjs-pino` cobre `authorization`, `cookie`, `password`, `currentPassword`, `newPassword`, `set-cookie`; varredura nos logs diff --git a/docs/SECURITY.md b/docs/SECURITY.md index ff97594..e0e0013 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -53,12 +53,20 @@ ## Rede -- Único ponto exposto à LAN: Nginx, porta 80 (443 quando HTTPS configurado - — `docs/OPERATIONS.md`). +- Único ponto exposto à LAN sem restrição de origem: Nginx, porta 80 (443 + quando HTTPS configurado — `docs/OPERATIONS.md`). - Postgres/Redis nunca publicados fora de `127.0.0.1`/rede Docker interna. -- AMI (5038), ARI (8088/8089), SIP (5060/5061), RTP (10000-20000) bloqueados - da interface LAN por nftables (`infrastructure/nftables/b2bcall.nft`), - mesmo o Asterisk rodando em `network_mode: host`. SSH nunca bloqueado. +- AMI (5038) e ARI (8088/8089) bloqueados incondicionalmente da interface + LAN por nftables (`infrastructure/nftables/b2bcall.nft`), mesmo o + Asterisk rodando em `network_mode: host` — nenhum softphone ou host + externo deve alcançá-los, só a própria aplicação via rede interna. +- SIP (5060/udp) e RTP (10000-20000/udp) são liberados na interface LAN, + mas **só para origem dentro do RFC1918** (10.0.0.0/8, 172.16.0.0/12, + 192.168.0.0/16) — necessário para softphones/ramais reais de agentes + registrarem, já que o OpenSIPS que ficaria na frente (`docs/OPENSIPS.md`) + não foi implantado nesta fase. Origem fora do RFC1918 (internet pública) + continua bloqueada — o Asterisk nunca deve receber SIP de fora da rede + privada (agente.md seção 5). SSH nunca bloqueado. - CORS: `origin` restrito a uma allowlist configurável (`ALLOWED_ORIGINS`), `credentials: true` só para essas origens. - Helmet (`@fastify/helmet`) ativo: `X-Content-Type-Options`, @@ -88,7 +96,8 @@ | Agente não eleva a própria permissão | ✅ | | Segredos de tronco/ramal cifrados em repouso | ✅ | | Postgres/Redis não expostos à LAN | ✅ | -| AMI/ARI/SIP não expostos à LAN | ✅ (nftables) | +| AMI/ARI não expostos à LAN | ✅ (nftables, incondicional) | +| SIP/RTP restritos a origem RFC1918 na LAN | ✅ (nftables — liberado deliberadamente para softphones reais) | | Rate limiting de login | ✅ | | Auditoria de ações sensíveis, sem vazar segredos | ✅ | | Logs estruturados sem credenciais | ✅ | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 519e430..2aee503 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -3,6 +3,38 @@ Problemas reais encontrados durante o desenvolvimento deste projeto e como foram diagnosticados/resolvidos — mantido como referência para o futuro. +## Softphone não registra ("timeout") + +**Sintoma**: usuário criou um ramal, colocou usuário/senha num softphone +real e o registro dava timeout (sem resposta nenhuma do servidor). + +**Causa raiz nº 1 — firewall**: nftables bloqueava incondicionalmente SIP +(5060/udp) e RTP (10000-20000) vindos da interface LAN (`ens18`) — regra +criada na Fase 2 como proteção, mas que também bloqueia qualquer uso real, +já que o OpenSIPS que ficaria na frente (`docs/OPENSIPS.md`) nunca foi +implantado. Diagnosticado direto pelo contador da regra: +`nft list table inet b2bcall_fw` mostrando `udp dport 5060 ... drop` com +contador subindo a cada tentativa. **Fix**: liberar SIP/RTP para origem +RFC1918 (rede privada), mantendo AMI/ARI sempre bloqueados — ver +`docs/SECURITY.md`. + +**Causa raiz nº 2 — depois de liberar o firewall, "Failed to authenticate"**: +o usuário estava digitando a **senha de login web** (a do `admin@b2bcall. +local`) no campo de senha do softphone, em vez da **senha SIP** do próprio +ramal (que é uma credencial completamente diferente, gerada por ramal). +Confirmado ativando `pjsip set logger on` e vendo o REGISTER chegando e +falhando na autenticação. **Lição**: deixar claro na interface (ou pelo +menos na documentação) que são dois sistemas de credenciais distintos — +fácil de confundir porque os dois são "a senha que o sistema gerou". + +**Melhoria feita por causa disso**: antes, a única forma de recuperar uma +senha SIP esquecida era resetá-la (invalidando a anterior). Agora dá para +ver e editar a senha SIP diretamente no formulário de edição do ramal +(`GET /extensions/:id/password`, decifra sob demanda) — diferente da +senha de login (hash argon2id, irreversível por design), a senha SIP é +cifrada (reversível) porque o próprio Asterisk precisa dela em texto puro +para autenticar. + ## "Body cannot be empty when content-type is set to 'application/json'" em quase todo botão de ação **Sintoma**: usuário real testando no navegador (não `curl`) relatou erro