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).
6.3 KiB
6.3 KiB
B2BCall — Segurança
Autenticação
- Senhas com Argon2id (
argon2lib), nunca bcrypt/md5/sha. - JWT de acesso (curta duração,
JWT_ACCESS_TTL) + refresh token opaco (hash SHA-256 armazenado, nunca o token em texto puro) com rotação: a cada uso, o refresh antigo é revogado e um novo par é emitido. - Cookies
HttpOnly,SameSite=Lax,Securequando HTTPS estiver ativo.SameSite=Laxé a mitigação primária de CSRF (o navegador não envia o cookie em requisições cross-site que não sejam navegação de topo) — não há token CSRF de double-submit adicional. Suficiente para o modelo de ameaça atual (rede privada, sem terceiros hospedando conteúdo que chame a API); reavaliar se o frontend algum dia rodar em uma origem publicamente diferente da API. - Rate limiting progressivo de login por IP via Redis
(
LoginThrottleService): backoff exponencial a cada bloco de tentativas falhas, nunca bloqueio permanente sem reset. - Resposta de "esqueci minha senha" sempre genérica, independente de o e-mail existir (evita enumeração de usuários).
- Troca de senha revoga todas as sessões ativas do usuário.
Autorização (RBAC)
User→UserRole→Role→RolePermission→Permission.- Catálogo de permissões centralizado (
packages/shared/src/permissions.ts) — nenhuma checagem de permissão usa string solta espalhada pelo código. PermissionsGuardglobal, aplicado via@RequirePermissions(...)em cada rota que precisa (rotas sem decorator exigem apenas autenticação).- Ninguém pode alterar os próprios perfis de acesso
(
users.service.ts#update), mesmo tendo permissão de gerenciar usuários — bloqueia auto-elevação mesmo por um super_admin comprometido/mal configurado.
Criptografia de segredos
Trunk.secretEncrypted/Extension.sipPasswordEncrypted: AES-256-GCM (packages/shared/src/secret-crypto.ts), chave mestraSECRETS_MASTER_KEY(.env, nunca no banco, nunca versionada).- Nenhum segredo (senha de tronco, senha de ramal, tokens) é retornado em
GET/PATCH— só uma vez, no momento da criação, quando aplicável.
Auditoria
AuditServiceregistra toda ação sensível (login, criação/edição/exclusão de entidades, disposição de chamada, mudanças de compliance, etc.) comuserId,ipAddress,userAgent, estado antes/depois.- Redação automática de campos sensíveis (
SENSITIVE_KEYS) antes de persistirbefore/after— senha, secret, token nunca aparecem em texto puro no log de auditoria. - Logs estruturados JSON (
nestjs-pino/pino) comrequest_idde correlação — mesma política de redação se aplica.
Rede
- Ú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) e ARI (8088/8089) bloqueados incondicionalmente da interface
LAN por nftables (
infrastructure/nftables/b2bcall.nft), mesmo o Asterisk rodando emnetwork_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:
originrestrito a uma allowlist configurável (ALLOWED_ORIGINS),credentials: truesó para essas origens. - Helmet (
@fastify/helmet) ativo:X-Content-Type-Options,X-Frame-Options,Strict-Transport-Security,Cross-Origin-Opener-Policy, etc.
Validação de entrada
- Todo DTO usa
class-validator(ValidationPipeglobal,whitelist: true, forbidNonWhitelisted: true) — payload com campo não esperado é rejeitado, não ignorado silenciosamente. - Toda query ao banco via Prisma (parametrizado) — as únicas duas exceções
(
$queryRawemlead-repository.tseagent-call-binding.ts) usam apenas parâmetros tipados interpolados pelo próprio Prisma ($queryRaw\...${valor}...``), nunca concatenação de string. - Comandos de diagnóstico do Asterisk expostos via API usam allowlist
explícita de comandos (
GET /api/asterisk/diagnostic/allowed-commands) — nunca shell livre nemasterisk -rx "<input do usuário>"sem filtro.
Checklist de aceite de segurança (agente.md seção 92)
| Item | Status |
|---|---|
| Senhas com hash forte (Argon2id) | ✅ |
| Nenhuma senha padrão (bootstrap sempre aleatório) | ✅ |
| RBAC aplicado no backend (não só escondido na UI) | ✅ |
| Agente não eleva a própria permissão | ✅ |
| Segredos de tronco/ramal cifrados em repouso | ✅ |
| Postgres/Redis não expostos à LAN | ✅ |
| 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 | ✅ |
.env/segredos nunca versionados no git |
✅ |
| Validação de entrada em todos os DTOs | ✅ |
| Sem SQL injection (Prisma parametrizado em toda parte) | ✅ |
| Sem shell injection (allowlist de comandos Asterisk) | ✅ |
| CSRF | ⚠️ Mitigado via SameSite=Lax, sem token dedicado (ver acima) |
| HTTPS | ⚠️ Não configurado nesta fase (rede privada) — ver docs/OPERATIONS.md |
Como reportar/tratar um incidente
- Revogar sessões:
UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL;(ou endpoint de logout em massa, se necessário adicionar). - Rotacionar segredos: gerar novos valores com
scripts/generate-secrets.sh, atualizar.env, reiniciarapi. RotacionarSECRETS_MASTER_KEYexige redescriptografar e recriptografar todos osTrunk.secretEncrypted/Extension.sipPasswordEncrypted(não automatizado nesta fase — script futuro se necessário). - Consultar
AuditLogfiltrando poruserId/entityType/janela de tempo para reconstituir o que aconteceu.