# Runbook interno: correlação de operação com IP público real via VPN Pritunl

**CONFIDENCIAL / uso interno Vulci. Follow-up técnico 1 do incidente 20/07 (movimentações não autorizadas).**

## Contexto e limite físico

Todo acesso de operador aos admins de produção entra pela VPN Pritunl da MonBank
(EC2 `i-0127c0d13bfc3e8a7`, `monbank-vpn-interna-prod`, IP interno `10.50.4.151`).
O concentrador faz NAT: TODO usuário chega às aplicações com origem `10.50.4.151`.
O IP público real do operador (ex.: `200.155.150.126` no incidente) existe SOMENTE
nos logs do Pritunl. Nenhum header HTTP consegue carregá-lo até o app, porque o NAT
acontece na camada de rede, antes do primeiro proxy HTTP.

Com o fix de auditoria de 21/07 (fonte única right-most-trusted nos 3 sistemas:
`MonetarieWeb.ClientIp`, `Shared.Plugs.ClientIp` na cabine PIX e
`BacenGatewayWeb.ClientIp` + `BacenGatewayWeb.AuditTrail` no SPB), a auditoria passa
a gravar de forma consistente o **client-of-record** = `10.50.4.151` (o concentrador),
em vez do ALB (`10.50.1.x`) ou do nginx interno. A recuperação do IP público real e
do usuário da VPN por operação é feita pela correlação abaixo.

## Receita de correlação (dado um evento de auditoria)

Entrada: timestamp do evento (UTC) + IP gravado (`10.50.4.151`).

1. SSM na EC2 da VPN:
   `awsmon ssm start-session --target i-0127c0d13bfc3e8a7`
2. As conexões ficam no MongoDB local do Pritunl (e em `/var/log/pritunl.log`):
   ```
   sudo mongosh pritunl
   ```
3. Sessões ativas/recentes com IP público real, usuário e IP virtual:
   ```js
   // servers_output guarda as linhas do daemon OpenVPN; o IP público real
   // aparece como "<pubIP>:porta TLS: ... username '<user>'"
   db.servers_output.find({ output: /username/ }).sort({ timestamp: -1 }).limit(50)
   // clientes conectados (usuário -> virt_address -> real_address)
   db.clients.find({}, { user_id: 1, real_address: 1, virt_address: 1, connected_since: 1 })
   db.users.find({}, { name: 1, org_id: 1 })
   ```
4. Filtrar pela janela de tempo do evento de auditoria: o usuário conectado no
   momento da operação (com MFA otp+pin no login da VPN) é o autor da sessão de
   rede. Com poucos usuários simultâneos a atribuição é direta; com muitos,
   cruzar também o horário de login/logout (`/var/log/pritunl.log`) e o
   navegador/user-agent do nginx (CloudWatch `/ecs/monetarie/prod/*-ui`).

No incidente 20/07 essa receita atribuiu a autoria de forma inequívoca
(`marco-brasil`, org `monbank-interno`, IP público `200.155.150.126` Telium).

## Limitação estrutural e recomendação (decisão do dono)

Enquanto o Pritunl fizer NAT, a correlação é por JANELA DE TEMPO, não por conexão.
Para atribuição determinística por request há duas opções, ambas mudanças de
infra/topologia (NÃO executar sem decisão explícita):

1. **Desligar o NAT do server Pritunl** e rotear a subnet virtual da VPN no route
   table da VPC (retorno para o concentrador). Cada operador chega ao ALB com seu
   IP VIRTUAL único da VPN; a auditoria (que já lê o XFF right-most-trusted) passa
   a gravar o IP virtual por usuário, e o Pritunl mapeia IP virtual -> usuário ->
   IP público. Custo: rota nova na VPC + SGs; risco baixo, reversível.
2. **Um usuário VPN = um IP virtual fixo** (Pritunl static IP por usuário) como
   complemento da opção 1, tornando o mapeamento estável no tempo.

Regra viva: drift/alteração de Terraform e topologia só com OK do dono
([[monetarie-terraform-drift-regra]]).

## Onde cada sistema grava o IP hoje (pós-fix 21/07)

| Sistema | Escritor | Fonte |
|---|---|---|
| Core | `AuditPlug`/`AuditContext` -> `audit_logs.ip_address` (+ `Repo.do_log`) | `MonetarieWeb.ClientIp` (XFF right-most-trusted) |
| Core | `sisbajud_controller` / `simba_controller` audits | idem |
| PIX | `SettlementServiceWeb.Plugs.AuditLogger` -> `monetarie_auth.audit_logs` | `Shared.Plugs.ClientIp` |
| SPB | `BacenGatewayWeb.AuditTrail` -> `public.audit_logs` (message_send, state_change, retry) | `BacenGatewayWeb.ClientIp` + ator em `changes._actor` |

Follow-ups NÃO cobertos (mesma classe, fora do escopo do incidente): rate limiters
do Core (`RateLimiter`/`DistributedRateLimiter` usam `List.last` do XFF — no caminho
dos admins todos os operadores caem no MESMO bucket do nginx); registros legais de
IB/merchant (`lgpd_controller` consentimento, `onboarding` terms_ip,
`approvals_controller`) ainda gravam `conn.remote_ip` (= ALB).
