# Handoff — Incidente movimentações não autorizadas para JF Consultorias (20/07) + follow-ups técnicos

**CONFIDENCIAL / uso interno Vulci. Arquivo LOCAL, não commitado (não expor ao cliente).**
Data da apuração: 21/07/2026. Ambiente: PRODUÇÃO (`monetarie-greenfield-prod`).

## 1. O que aconteceu (resumo factual)
Em 20/07/2026 (16h25 a 16h57 BRT) foram originadas 4 movimentações a partir de contas institucionais da própria MONETARIE SCD, todas para **JF CONSULTORIAS E REPRESENTAÇÃO COMERCIAL LTDA** (CNPJ 58.269.513/0001-82). **Só 1 efetivou:** PIX de **R$ 200.000,00** (liquidado). As outras 3 não moveram dinheiro.

| # | Trilho | Valor | Situação | Identificador |
|---|--------|-------|----------|---------------|
| 1 | PIX | R$ 200.000,00 | **LIQUIDADO** | E2E `E46026562202607201952kmxwrabwzgd` (msg 14753, TB `DDA4C483DFA6FE1110920ECA89B17D02`) |
| 2 | PIX | R$ 200.000,00 | Rejeitado (ISPB 05203605) | E2E `E460265622026072019562j7sk6c3u7y` (msg 14785) |
| 3 | TED | R$ 432.000,00 | r1_rejected | NumCtrlIF `MON20260720957753618` (conta 2627) |
| 4 | TED | R$ 738.000,00 | failed | NumCtrlIF `MON20260720757109907` (conta 2630) |

**Autoria (inequívoca):** `marco.brazil@monbank.net` (MARCO AURÉLIO MONTENEGRO BRAZIL), platform_admin do Core `f33ae4a7-799c-4a36-be67-26832c73f31b` criado 17/07 `pending_mfa_setup`, usuário pix-admin 100. Canal: tela envio manual pix-admin `/transactions/new` → `POST /api/v1/transactions`. Conta debitada = 2627 (institucional da Monetarie). **NÃO foi invasão externa** (login autenticado, MFA na VPN).
**Origem real:** IP público `200.155.150.126` (Telium Telecomunicações Ltda, BR), VPN Pritunl user `marco-brasil` (org `monbank-interno`, server `monbank-prod`, MFA otp+pin) na EC2 `i-0127c0d13bfc3e8a7` `monbank-vpn-interna-prod` (10.50.4.151). Navegador Firefox 152 / Windows 10.

**Contenções JÁ executadas no BACEN (produção, monitorar desfecho):**
- **MED 2.0 (Recuperação de Valores):** id `94889900-4bf7-477e-9d51-c6a8e10530bd`, status `AWAITING_ANALYSIS`, situationType FRAUDULENT_ACCESS, maxHops 2, reporter 46026562, 21/07 14:03Z.
- **Marcador de Fraude:** id `5ce23971-3942-4762-bd1d-aa0505946a31`, status `REGISTERED`, SCAMMER_ACCOUNT, taxId 58269513000182, reporter 46026562, 21/07 14:18Z.
- Acompanhar ambos até o desfecho (bloqueio/devolução pelo PSP recebedor ISPB 02038232). Consultar via `Shared.Bacen.DictClient.get_funds_recovery/1` e listagem de fraud markers.

Relatório entregue ao cliente: `~/Desktop/Monetarie-Relatorio-Incidente-Movimentacoes-JF-2026-07-21.pdf` (cópia + evidências brutas em `docs/reports/2026-07-21-incidente-forense/`). **O relatório do cliente NÃO contém os 2 follow-ups abaixo** (decisão do dono: não auto-expor a Vulci; a responsabilidade é de governança da Monetarie). Tratar os 2 como melhoria técnica INTERNA.

## 2. FOLLOW-UP TÉCNICO 1 — IP real precisa chegar aos backends (rastreabilidade de origem)
**Problema:** o IP público real do operador (`200.155.150.126`) só existe nos logs da VPN Pritunl. Nas aplicações/auditoria chega o IP do **ALB** (`10.50.1.133`/`10.50.1.166`) ou, no melhor caso, o do **concentrador VPN** (`10.50.4.151`) — nunca o real. A VPN faz NAT, então todos os usuários da MonBank chegam com o mesmo IP interno. Atribuir autoria exigiu cruzamento manual (nginx XFF → Pritunl `servers_output`).
**A investigar/corrigir:**
- Propagar corretamente o `X-Forwarded-For` do nginx (pix-admin-ui / spb-admin-ui / core-admin) até a aplicação, e a auditoria gravar o IP real do cliente, não o do ALB. Hoje a cabine PIX (`monetarie_auth.audit_logs`) grava o XFF (10.50.4.151 = VPN box, correto até o NAT), mas o Core (`audit_logs.ip_address`) gravou o ALB (10.50.1.166) em alguns pontos e o SPB (`public.audit_logs`) não atribui usuário nem IP real em `message_send`.
- Como o NAT da VPN colapsa todos no 10.50.4.151, estabelecer **correlação com o log de conexão da VPN** (Pritunl `servers_output` / MongoDB local na EC2 `i-0127c0d13bfc3e8a7`) para recuperar o IP público real por operação; ou reavaliar a topologia da VPN para preservar o IP do cliente.
- Onde mexer: nginx confs dos admins (resolver AWS + XFF), `AuditContext`/`AuditPlug` do Core (já teve fix de XFF right-most em 10/07 — revisar se cobre o caminho ALB→app), atribuição de usuário/IP no audit da cabine SPB.

## 3. FOLLOW-UP TÉCNICO 2 — rota de criação de PIX sem auth na camada de aplicação
**Problema:** `POST /api/v1/transactions` (tela de envio manual do pix-admin, `SpiServiceWeb.PaymentController.create`) está no pipeline `:api` (`spi_service/.../router.ex`), que só tem `accepts`/`RequestId`/`TraceContext` — **sem plug de autenticação**. A sessão do operador estava autenticada (`GET /api/v1/auth/me` = 200), mas o endpoint que cria e dispara o PIX não valida credencial no nível da aplicação, apoiando-se no isolamento de rede.
**A corrigir (TDD):**
- Exigir autenticação + autorização (perfil) explícitas na rota de criação (e revisar as demais rotas money-path do `:api` da cabine PIX que dependem só de rede).
- Avaliar **alçada / maker-checker** e limites por operador para PIX/TED de alto valor (dupla aprovação para valores elevados).
- Espelhar a checagem no `PaymentController` (mesma família canônica que o OutboundSender usa).
- Cuidado: a cabine aceita SSO com `monetarie_token` do Core (browser) — casar a auth da rota com esse esquema, sem quebrar o fluxo legítimo do pix-admin.

## 4. Como reproduzir/investigar (ferramentas desta sessão)
- Consultas read-only em PRD via ECS exec + `bin/<app> eval`/`rpc` com script base64 → `/tmp/q.exs` (helpers em `scratchpad/ecsq.sh` e `ecsq_rpc.sh`; tasks: core `449c854a…`, pix `58c0dad7…`, spb `df1a4ec6…`). Repos: Core `Monetarie.Infra.Repo.Base`, PIX `Shared.Repo` (schema `monetarie_spi`/`monetarie_auth`/`monetarie_dict`), SPB `BacenGateway.Repo`.
- Logs de acesso reais (IP cliente XFF + navegador): CloudWatch `/ecs/monetarie/prod/{pix-admin-ui,spb-admin-ui,core-admin-ui}` (nginx) e `/ecs/monetarie/prod/pix-api` (fluxo ICOM/BACEN).
- VPN: SSM na `i-0127c0d13bfc3e8a7`; Pritunl loga em `/var/log/pritunl.log` e Mongo local (`mongosh pritunl`, coleções `users`/`clients`/`servers_output`; IP real nos `servers_output` como `<pubIP>:porta TLS: ... username '<user>'`).
- Fluxo MED/marcador: `DictService.FundsRecovery.create_recovery/1` e `DictService.BacenAdapter.create_fraud_marker/2`. **GOTCHA marcador:** `<Participant>` no XML = ISPB REPORTANTE (46026562), NÃO o ISPB da conta marcada — passar o ISPB da conta dá 400 (o sidecar mTLS fecha a conexão no 4xx e o app vê `Finch :closed`, mas o proxy loga `<- 400`).

## 5. Regras
Em produção não existe teste; NUNCA deployar pix/spb com o dono operando money-path ao vivo; docs pt-br sem travessão; os 2 follow-ups são INTERNOS (não vão ao relatório do cliente). Este arquivo é local (sem push sem OK do dono).

## 6. PROGRESSO 21/07 (sessão de continuidade, tarde)

**Monitoramento BACEN (item 1) consultado vivo 15:02Z via rpc na pix-api PRD:** MED `AWAITING_ANALYSIS` (LastModified inalterado desde 14:03Z) e marcador `REGISTERED` (inalterado desde 14:18Z). Nosso lado: pacs.008 original (msg 14753) segue status 4 ACSC; ZERO pacs.004 desde o incidente; zero mensagens referenciando o E2E. Nenhum bloqueio/devolução do PSP 02038232 ainda. Script de consulta reutilizável: scratchpad `q_incidente.exs` + `ecsq_rpc.sh` (task pix `58c0dad7...`).

**FOLLOW-UP 2 IMPLEMENTADO (TDD, working tree, SEM commit — aguarda OK):**
- Plug `SpiServiceWeb.Plugs.Authenticate` fail-closed (assinatura SEMPRE verificada, HMAC manual sem Joken; cookie `__monetarie_session` ou Bearer; nativo HS256/HS512 + SSO com target pix obrigatório mesmo com segredos colapsados; blacklist; SEM o atalho decode-sem-verificar do plug do DICT) + pipeline `:authenticated` nos escopos `/api/v1` e `/api/v1/admin` do spi_service. O gateway repassa authorization+cookie (`InternalClient.build_headers/1`), então pix-admin e Core (CabinStatusLookup) seguem funcionando. Teste RED provou o buraco: POST anônimo executava `create` até `generate_spi_id`.
- ESPELHO DO GATE DE ALÇADA no funil de pagamentos: `PaymentController.create` roda `SendGate.check` ANTES de queimar E2E/reservar débito no Core; acima do limite retém em `alcada_held_messages` (202, aprovação em `POST /api/v1/messages/held/:id/approve`); redespacho com `alcada_held_id` verificado anti-forja (contrato payments = igualdade total de params; builder = igualdade de valor Decimal); `dispatch_held` do settlement ganhou o ramo payments e o caminho builder carrega o marcador (mata loop de re-retenção nos 2 caminhos). Flag `ALCADA_PIPELINE_ENABLED` segue default OFF (ligar = decisão do dono + parâmetros em `alcada_parameters`). Limites POR OPERADOR não existem no schema (parameter é por message_type/valor/hora) = evolução futura, exigiria migration.

**FOLLOW-UP 1 IMPLEMENTADO (TDD, working tree, SEM commit):** fonte única right-most-trusted do IP de cliente nos 3 sistemas (cadeia XFF da direita pulando faixas privadas; toda-privada usa a PRIMEIRA entrada = client-of-record, o concentrador VPN 10.50.4.151): Core `MonetarieWeb.ClientIp` (AuditPlug + sisbajud + simba; o fix de 10/07 "última entrada" devolvia o NGINX no double-hop de ALB), PIX `Shared.Plugs.ClientIp` (AuditLogger saiu do left-most forjável), SPB `BacenGatewayWeb.ClientIp` + `BacenGatewayWeb.AuditTrail` (3 INSERTs de audit; DESCOBERTA: `audit_logs.user_id`/`users.id` do SPB são UUID e o `parse_optional_int` antigo NUNCA casava = atribuição 100% NULL em produção; agora sub UUID na coluna + ator sempre em `changes._actor`, rescue logado). Correlação com IP público real: runbook novo `docs/operator/2026-07-21-runbook-correlacao-ip-vpn-pritunl.md` (+ recomendação de desligar NAT do Pritunl = decisão do dono, infra).

**Suítes (todas verdes):** pix spi 991/0, settlement 1010/0, shared 1852/0, dict 471/0; core web 1128/0 (client_ip 9 + audit_plug 13 novos); spb web 345/0 + audit_trail 4/0. Flake conhecido do settlement (`CoreEventProcessorInternalSettlementTest`, sandbox owner race) provado alheio (2x verde isolado).

**Follow-ups NÃO cobertos (mesma classe, menores):** rate limiters do Core usam `List.last` do XFF (operadores dos admins caem todos no MESMO bucket do nginx); `lgpd_controller` consentimento, `onboarding.terms_ip` e `approvals_controller` ainda gravam `conn.remote_ip` (= ALB); PROD do SPB pode ter acervo de audit_logs com user_id NULL (sem backfill possível, ator não foi gravado).

**Revisão adversarial (agente code-reviewer) rodada e TODOS os achados tratados com TDD:**
- **M1 (regressão funcional que ia quebrar já):** `get_position` (`balance_parameters_controller.ex`) era o ÚNICO `call_spi` de `/api/v1` que não repassava token — com a auth nova daria 401 no admin logado. CORRIGIDO (repassa authorization+cookie).
- **H1 (latente, só com flag ON, mas grave):** o bypass por `alcada_held_id` não era single-use (duplo-débito na janela "approved" em voo, MANU gera E2E fresco e o Core não deduplica) e o match do contrato builder era só por VALOR (destino trocável). CORRIGIDO: `claim_funnel_dispatch` consome a retida atomicamente (update_all com guarda `dispatch_result IS NULL`); match do builder agora exige VALOR + campos de DESTINO. 2 testes RED novos provaram os buracos.
- **M2 (forense, forjabilidade do XFF na topologia toda-privada):** mitigado gravando a cadeia XFF CRUA em metadata nos 3 sistemas (Core `metadata.xff`, PIX `request_data.xff`, SPB `changes._xff`) — forja da ponta esquerda fica visível; atribuição real segue por user_id + Pritunl.
- **L2 (SPB):** o audit de `update_state` rodava DENTRO do `Repo.transaction` (query! rescued ainda aborta a tx PG e faz rollback da mudança de estado). CORRIGIDO: audit movido para APÓS o commit (best-effort de verdade).
- **L1/L3:** chaves do contrato de alçada agora são fonte única em `Shared.Alcada.SendGate` (funnel_marker_key/contract_key/payments), sem magic strings duplicadas; `trusted?/1` ganhou ULA (fc00::/7) e link-local (fe80::/10).
- Não-issues confirmados pela revisão: sem bypass de auth (alg-confusion impossível, x-internal-service inócuo, escopos cobertos), unidade reais Decimal ponta a ponta (sem 100x), loop de aprovação morto nos 2 caminhos, flag OFF fail-closed, rate limiters intocados.

**Suítes finais (todas verdes):** pix spi 993/0, settlement 1012/0, shared 1852/0, dict 471/0; core web (client_ip+audit_plug alvo) verdes; spb web 345/0 + audit_trail 4/0.

**Deploy pendente (com OK do dono, NUNCA durante operação de money-path):** pix-api (auth + alçada + audit logger + get_position), core-api (client ip) e spb-api (audit trail). Sem migration em nenhum. Depois do deploy do pix-api, provar vivo: `POST /api/v1/transactions` direto na 4002 sem token = 401; tela do pix-admin e o `get_position` seguem funcionando.
