# Handoff 2026-07-08 — PIX sidecar mTLS-HSM + SPB, e a fronteira de autorização no BACEN

## TL;DR

As duas cabines (PIX e SPB) estão **tecnicamente prontas e provadas ponta a ponta**
até o BACEN de produção. O único bloqueio restante nas DUAS é **do lado do BACEN**:
o participante/certificados da Monetarie (ISPB 46026562) **ainda não estão
liberados/registrados para operar em produção** (provavelmente os certs não foram
submetidos/registrados no BACEN via STA — hipótese do dono no fim da sessão, que
bate 100% com a evidência).

- **PIX**: a pibr.001 (EchoRequest) é **construída, assinada no HSM (PIA), enviada
  por mTLS com a chave PIC 100% no HSM** e chega no BACEN. O BACEN responde
  `403 Acesso nao permitido`.
- **SPB**: a GEN0001 (eco) sai pelo MQ (canal RUNNING, sequenciamento resetado) e o
  BACEN responde `EGEN9910 Serial do certificado invalido` / `EGEN9920 Certificado
  invalido` (cert não registrado/ativado no cadastro do BACEN).

Nada do nosso lado precisa mudar para o tráfego funcionar; falta a **liberação do
participante no BACEN de produção**.

## O que está deployado (vivo)

- `pix-api:5` (cluster `monetarie-greenfield-prod`), 2 containers: `pix-api` +
  `pix-mtls-sidecar`. Imagem pix-api = `monetarie/pix-api:prod-pixmtls-20260708`
  (digest `sha256:d81e235c…`, buildada com o override de URL do runtime.exs).
  Sidecar = `monetarie/pix-mtls-sidecar:prod-log-20260708`.
- `spb-api:2`, com o HSM ligado (kp_B602EB3E) + `BACEN_CONTROL_PREFIX=MON` +
  `WEBHOOK_ALLOW_UNSIGNED_INTERNAL=true` + `bacen_env=production`.
- Rollback: `pix-api` volta removendo `pix_mtls_sidecar_enabled` (task-def sem
  sidecar) e/ou `service_image_tags`. `spb-api` volta pra `:1`.

## Commits / push

- `origin/main bb561eb4` — sidecar mTLS-HSM (Go) em `pix/services/bacen_mtls_sidecar/`
  + override de URL em `pix/backend/config/runtime.exs` (DICT_BASE_URL /
  ICOM_PRIMARY_URL / ICOM_SECONDARY_URL, default inalterado = mTLS direto).
- Branch `origin/infra-prod` — wiring do sidecar no `ecs.tf` (gateado por
  `pix_mtls_sidecar_enabled`), `variables.tf`, `envs/prod.tfvars`.
- **PENDENTE de commit** (feito em disco, não commitado): no `main.go` do sidecar o
  `dumpTransport` (dump RAW de request/response, gateado por `SIDECAR_DUMP_RAW`); no
  `ecs.tf` a env `SIDECAR_DUMP_RAW`. A imagem `prod-rawdump` NÃO foi buildada (Docker/
  OrbStack caiu no fim da sessão) — a tfvars foi revertida pra `prod-log-20260708`
  (imagem existente) pra não quebrar o apply.

## PIX — arquitetura do sidecar mTLS-HSM (a entrega grande)

**Problema**: a chave PIC (mTLS ICP-Brasil) mora no HSM e NUNCA sai de lá. O
`:ssl`/OTP do Erlang só faz mTLS com keyfile em disco ou engine PKCS#11 local — não
consegue chamar a API REST do HSM da RTM no meio do handshake.

**Solução (provada viva)**: um sidecar Go (`pix/services/bacen_mtls_sidecar/`) que:
- Termina o request PLAIN do pix-api (http://localhost:9101..9104) e origina o mTLS
  com o BACEN (DICT/DICT-NP/ICOM/ICOM-sec).
- Assina o `CertificateVerify` do TLS 1.2 via `crypto.Signer` que chama
  `POST /v1/kmip/{vhsm}/sign-rsa` (RSAHASH = manda o SHA-256, o HSM faz DigestInfo +
  PKCS#1 v1.5 + RSA — exatamente o que TLS 1.2 rsa_pkcs1_sha256 exige). **Zero chave
  privada** no sidecar: só o cert PIC público + o UID KMIP; crypto_user/token via
  secrets do task.
- Reescreve o `Host` para o hostname do BACEN sem porta (Manual Canal Secundário).
- Verifica o cert servidor do BACEN pela cadeia ICP-Brasil prod baked
  (`icp_brasil_ca_chain_prod.pem`).
- `SIDECAR_SELFTEST=true`: no boot, faz o handshake mTLS real contra cada endpoint e
  loga se o BACEN aceitou nosso cert.

**Provas vivas** (logs do sidecar em prod):
```
[selftest] dict     -> dict.pi.rsfn.net.br:16422      mTLS OK (BACEN accepted our cert) TLS1.2 peerCN="*.pi.rsfn.net.br"
[selftest] dict_np  -> dict-np.pi.rsfn.net.br:16432   mTLS OK
[selftest] icom     -> icom.pi.rsfn.net.br:16422      mTLS OK
[selftest] icom_sec -> icom-sec.pi.rsfn.net.br:17422  mTLS OK
```

## PIX — certs e assinatura (tudo no HSM, provado)

- HSM: `sign-rsa` provado com PIA (`=PIA_P003= HSM_sign_OK verify_contra_cert=true`) e
  PIC (`=PIC_P002= HSM_sign_OK verify_contra_cert=true`). vHSM 60042.
- Certs subidos **pelo sistema** (`CertificateManager.import_certificate` /
  `import_peer_certificate`, tabela `monetarie_spi_msg.crypto_keys`):
  - **CPIA** = MONETARIE P003 (serial 192494077696259819) — ativo, cert de assinatura.
  - **CPIC** = MONETARIE P002 — ativo, cert de conexão.
  - **PEERPIA** = BANCO CENTRAL DO BRASIL PI P515 (ISPB 00038166) — ativo, verificação
    da assinatura de entrada do BACEN.
  - Slots válidos: `@cert_types = CPIC CPIA CERTQRC PEERPIA`. O cert servidor do BACEN
    (PIC-P514, `*.pi.rsfn.net.br`) NÃO tem slot: é confiado pela cadeia ICP-Brasil no
    mTLS (não precisa importar).

## PIX — o bug que travava a assinatura (RESOLVIDO)

`hsm_key_not_configured: 46026562` na assinatura da pibr.001. Causa raiz: o
`envs/prod.tfvars` tinha `pix_rtm_hsm_private_key_uid = ""` e **tfvars (-var-file)
tem precedência sobre TF_VAR**, então o `""` sobrescrevia a UID que eu passava por
TF_VAR — o env `RTM_HSM_PRIVATE_KEY_UID` chegava VAZIO no task-def, e o runtime usa
`keys: %{default => {private_key_uid: env}}`. **Fix**: comentei as linhas `""` no
tfvars; agora o TF_VAR entra. `pix-api:5` tem `RTM_HSM_PRIVATE_KEY_UID=KQII1vQRpKP0Pj3l+RAl`
(PIA) e `RTM_HSM_PUBLIC_KEY_UID=1CgmpkFRZ/dZlFEOAjXd`.

> GOTCHA #1: **tfvars com `""` sobrescreve TF_VAR.** Não deixar UIDs `= ""` no tfvars
> quando a intenção é passar por TF_VAR.
> GOTCHA #2: query jmespath aninhado `environment[?name=='X'].value|[0]` em task-def
> pode retornar null falso — conferir com `environment[]|[?name=='X']`.

## PIX — a prova do 403 (é do BACEN)

pibr.001 real, gerada pelo sistema (`Shared.Bacen.SpiClient.echo(channel: :primary)`),
**assinada no HSM e enviada**. Log do sidecar:
```
POST /api/v1/in/46026562/msgs  ->  icom.pi.rsfn.net.br:16422
<- 403  application/problem+xml
   <problem xmlns="urn:ietf:rfc:7807">
     <type>https://icom.pi.rsfn.net.br/api/v1/error/forbidden</type>
     <title>Acesso nao permitido</title>
   </problem>
```
Re-testado **com o PEERPIA (cert do BACEN) carregado** → continua 403. Logo, o 403
NÃO é falta de cert; é o BACEN negando o **acesso do participante** ao recurso em
produção. O long-poll (GET `/api/v1/out/46026562/stream/start`) dá o mesmo 403 (e
`/api/v2/cids/events` no DICT dá `Participant is not allowed to access this resource`).
NÃO é 429 (rate-limit); os `count=NN` dos workers ICOM são contador de falhas.

## SPB — estado (deixado como está, por decisão do dono)

Tecnicamente pronto e provado: MQ 2035 resolvido (spb_server com connect/put/get/
setall + admin/CHSTATUS + `MCAUSER(spb_server)` no `APP.SVRCONN`, via SSH herberth+
sudo no `172.31.1.50`); **sequenciamento** do canal sender `C46026562.00038166.1`
resetado para `SEQNUM(15232)` (o BACEN esperava 15232, nosso QM refeito voltou a 1) →
canal RUNNING, xmitq drenou, GEN0001 entregue ao BACEN; certs subidos pelo sistema
(OurCertStore B602EB3E como SPB01 + BacenCertStore P060/P061). Bloqueio BACEN:
`EGEN9910/EGEN9920` (cert P001 não registrado/ativado no cadastro do BACEN de prod).
Detalhe: [[monetarie-infra-prod-fase3]] [[monetarie-mq-prod-channels-0707]].

## Pendências

1. **BACEN (dono/instituição, o desbloqueio real):** registrar/ativar os certificados
   do participante em produção (provavelmente via STA) — PIX (liberar o participante
   nos recursos ICOM/DICT) e SPB (ativar o cert P001, GEN0006). Sem isso: 403 (PIX) e
   EGEN9910 (SPB).
2. **Tabelas faltando no banco PIX prod** (migrations não aplicadas): `monetarie_dict.dict_sync_cursors`
   (DictInboundPollWorker crasha), `monetarie_spi.operational_alerts` (AlertEngine).
   Rodar as migrations pendentes do PIX em prod.
3. **Sidecar RAW dump**: commitar o `dumpTransport` + `SIDECAR_DUMP_RAW` no ecs.tf,
   buildar a imagem `prod-rawdump` (Docker estava fora), aplicar, e capturar o corpo
   completo da pibr.001 assinada + resposta crua do BACEN pra anexar no chamado.
4. **Desligar depois de debugar**: `SIDECAR_LOG_REQUESTS`/`SIDECAR_DUMP_RAW` (verbosos).
5. **STA/NPC/CLST**: adiados (dono). No plan prod eles sobem `desired 0->1` num apply
   full — usar `-target` no pix/spb pra não subi-los.

## Secrets gravados nesta sessão (Secrets Manager, regra #9/#10)

- `monetarie/prod/hsm/{pix,spb}/crypto_user` = `APP-MONETARIE-PRD`
- `monetarie/prod/hsm/{pix,spb}/token` = token do app (28ch, com o `%` no fim)
- `monetarie/prod/hsm/vco/{name,token}` = `MONETARIE-PRD` / token VCO admin
- UIDs (já existiam, provados): `monetarie/prod/hsm/pix/{private_key_uid=PIA,pic_private_key_uid=PIC}`,
  `monetarie/prod/hsm/spb/kp_B602EB3E_{priv,pub}_uid`.

## Comandos de retomada

```bash
awsmon(){ env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN AWS_PROFILE=vulcimonetarie AWS_REGION=sa-east-1 aws "$@"; }

# rpc no pix-api (Repo/HSM vivos):
PT=$(awsmon ecs list-tasks --cluster monetarie-greenfield-prod --service-name pix-api --query 'taskArns[0]' --output text)
awsmon ecs execute-command --cluster monetarie-greenfield-prod --task "$PT" --container pix-api --interactive \
  --command "bin/monetarie_pix rpc 'IO.inspect(Shared.Bacen.SpiClient.echo(channel: :primary))'"

# ver o RAW no sidecar (SIDECAR_LOG_REQUESTS ja on):
TID=$(echo "$PT"|sed 's#.*/##')
awsmon logs get-log-events --log-group-name /ecs/monetarie/prod/pix-api \
  --log-stream-name "pix-mtls-sidecar/pix-mtls-sidecar/$TID" --limit 50 --query 'events[].message' --output text

# terraform prod (worktree monetarie-infra-prod):
cd /Users/luizpenha/monetarie-infra-prod/infra/aws/greenfield
terraform init -reconfigure -backend-config=backend.prod.hcl
# TF_VARs de UID/cert por Secrets Manager no apply (ver histórico); -target pix-api/spb-api.
```

Regra de ouro do stack compartilhado: defaults reproduzem HML byte a byte; prod só
via `envs/prod.tfvars` + TF_VAR. Apply em prod sempre com `-target` no serviço, nunca
full (sobe STA/NPC/CLST). SPB de lado.
