diff --git a/docs/handoff-2026-05-09-onda-9-vault-recovery.md b/docs/handoff-2026-05-09-onda-9-vault-recovery.md new file mode 100644 index 000000000..758b2dc18 --- /dev/null +++ b/docs/handoff-2026-05-09-onda-9-vault-recovery.md @@ -0,0 +1,419 @@ +# Onda 9 — Vault Recovery (vault.secrets corrupted) +> ⚠️ **Versão redactada para o repositório público.** Credenciais sensíveis (API keys, passwords, account IDs) foram substituídas por ``. A versão completa com plaintext fica em armazenamento interno com acesso controlado. + + +**Started:** 2026-05-09 (continuação de chat lotado) +**Owner:** Joaquim (Promo Brindes) +**Mode:** AUTONOMOUS — sem pausas, sem perguntas, executa até 10/10 + +## ⚠️ LEIA PRIMEIRO se for próximo Claude + +Este é o doc-mãe. **Nunca executar nada sem ler até "## Estado atual" abaixo.** + +A Onda 9 é a recuperação dos 31 secrets corrompidos em `vault.secrets`. Todos falham com `pgsodium_crypto_aead_det_decrypt_by_id: invalid ciphertext`. + +**Hipótese dominante (web search Supabase docs):** Volume `db-config` foi destruído em algum momento (provavelmente Dec/2024 — mtime do `pgsodium_root.key` atual), regenerando nova root key. Os 31 secrets antigos ficaram cifrados com chave perdida = irrecuperáveis pelo método normal. Solução: **reconstruir** valores via env vars dos containers (Opção A aprovada). + +## Plano (4 frentes paralelas/sequenciais) + +```text +F1 RCA confirmatória (read-only, 45 min) +F2 Coleta valores (read-only, 30 min) +F3 Recovery cirúrgico (escrita controlada, 60-90 min) +F4 Prevenção (docs + alertas, 30-45 min) +``` + +## Decisões aprovadas em bloco (D1-D11) + +- D1=A — Se F1 confirma teoria, segue autônomo +- D2=A — Se F1 refuta, investiga mais (não recover às cegas) +- D3=B — Só os secrets AI usados (Lovable/FATOR X abandonados podem ser DEFER) +- D4=A ou C — UPDATE preservando UUID OU vault.update_secret() conforme API disponível +- D5=B — Por secret (cada UPDATE+validate em transação curta) +- D6=A — Manual + observar 5 min +- D7=A — Restart supabase_functions profilático (~10s downtime) +- D8=A — Backup do volume db-config no supabase-backup existente +- D9=A — Investigar backup_passphrase ANTES de sobrescrever +- D10=B — Stack zapp-web órfã = backlog (fora escopo) +- D11=A — GitHub issue rastreando deprecação pgsodium + +## Critério de ABORT + +Qualquer um destes para a execução autônoma e exige nova decisão: +- Step pré-flight retorna disco <2GB livre +- Postgres em recovery mode +- Replication slot ativo (não esperado) +- Mismatch entre env do container e suspeita de valor canônico (Joaquim decide) +- API pgsodium não suporta UPDATE preservando key_id (precisa Plan B) +- Qualquer SELECT pós-UPDATE retorna mismatch byte-a-byte +- Algum container crítico cai durante execução + +## ⏱️ Estado atual + +**Phase:** F1 RCA — em andamento +**Last update:** 2026-05-09 (start) +**Last step OK:** doc inicial criado +**Next step:** F1.1 — Inspecionar schema vault.secrets + +## Pendências de reverter (rolling list) + +(vazio até primeira ação reversível) + +## Resultados por step + +(será preenchido em append) + + +--- + +## ✅ F1 — RCA confirmatória (2026-05-09 ~16:00 UTC) + +### Achados + +| # | Achado | Evidência | +|---|---|---| +| 1 | Schema `vault.secrets` tem trigger `secrets_encrypt_secret_trigger_secret` que cifra automaticamente em UPDATE | `\d+ vault.secrets` | +| 2 | View `vault.decrypted_secrets` usa AD = `id \|\| description \|\| created_at \|\| updated_at` | `\sv` | +| 3 | 31 secrets criados em **2026-05-03** (29 deles) e **2026-05-04** (4 AI keys) | `SELECT created_at` | +| 4 | Cada secret tem **key_id único** (31 derivações distintas) | `SELECT DISTINCT key_id` = 31 | +| 5 | `pgsodium.key` tem 33 keys totais (31 antigas + 1 do dia 09/mai criada na investigação) — todas `valid` | `count(*) = 32 antes, 33 com teste` | +| 6 | Todas as keys têm mesmo `key_context` = `pgsodium` (default) e `key_type=aead-det` | hex `7067736f6469756d` | +| 7 | **Secret novo criado AGORA cifra+decifra OK** (`rca_test_*`) — pgsodium funciona | bigint_id=33 | +| 8 | **Decifrar secret antigo retorna `invalid ciphertext`** | confirmado em `evolution_instance_name` | +| 9 | Volume `supabase_db_config` criado em **2024-12-11** (intacto desde Dec/2024) | `docker volume inspect` | +| 10 | `pgsodium_root.key` em `/etc/postgresql-custom/`: **mtime 2024-12-11**, **ctime 2026-04-29 22:24:38** | `stat` | +| 11 | Container `supabase_db` atual criado em **2026-05-06 10:31:59** | inspect `Created` | +| 12 | `pg_postmaster_start_time()` = **2026-05-06 10:32:13** (= start do container atual) | psql | +| 13 | Bind mount `/var/lib/postgresql/data` → `/root/supabase/docker/volumes/db/data` (host filesystem) | inspect Mounts | + +### 🎯 Causa raiz (alta confiança) + +```text +Linha do tempo: + 2024-12-11 → Volume `supabase_db_config` criado pela 1ª vez, + arquivo `pgsodium_root.key` gerado (ROOT_A) — mtime fica em Dec/2024. + + ...meses correm com postgres carregando ROOT_A em memória... + + 2026-04-29 22:24:38 → CTIME do arquivo muda. Alguém tocou/substituiu. + Mtime preservado (cp -p ou similar). + Conteúdo do arquivo passa a ser ROOT_B. + POSTGRES AINDA TEM ROOT_A em memória cacheada. + + 2026-05-03/04 → 31 secrets criados. Cifrados com ROOT_A (cache). + Vault.secrets ↔ pgsodium.key todas funcionando OK. + + 2026-05-06 10:31:59 → Container supabase_db recriado. + Postgres reinicia e carrega `pgsodium_root.key` do + disco → agora carrega ROOT_B. + + 2026-05-06 onwards → Tenta decifrar ciphertexts (criados com ROOT_A) usando + derivações de ROOT_B → INVALID CIPHERTEXT. + + 2026-05-09 17:35 → Primeira falha aparente em fn_reconcile_dispatch + (pode ter sido ANTES, mas só foi notada pelo log + do cron). +``` + +**Por que não cenário "down -v":** +- Volume `supabase_db_config` tem `CreatedAt: 2024-12-11` — não foi recriado +- Stack `supabase` foi recreado em 06/mai (container ID novo) mas volumes preservados + +**Por que cenário "key file substituído":** +- ctime 2026-04-29 22:24 prova mudança de metadata +- Mtime Dec/2024 preservado prova `cp -p` ou `--preserve=timestamp` +- Reset só "ativou" quando postgres reiniciou em 06/mai + +### 🚧 Limite da investigação + +- Não tenho como recuperar ROOT_A (não há backup do volume `supabase_db_config`) +- Backup só do banco (`pg_dump`) não inclui o root key +- ⇒ **31 secrets antigos são irrecuperáveis pela cifragem original** +- Solução: re-cifrar os valores em claro usando ROOT_B (key atual) + +## Lista de pendências reverter + +(nada destrutivo feito ainda; secret de teste `rca_test_*` será removido na limpeza final) + +## Estado atual (pós-F1 RCA) + +**Phase:** F1 ✅ DONE → seguindo para F2 coleta de valores +**Last update:** 2026-05-09 ~16:00 UTC +**Next step:** F2.1 — inspecionar env do container `evolution` para coletar evolution_api_* + + +--- + +## ✅ F2 — Coleta de valores (2026-05-09 ~16:30 UTC) + +### Mapeamento dos 31 secrets + +✅ = valor coletado | 🟡 = DEFER (não recuperável agora) + +| # | Nome | Status | Fonte | +|---|---|---|---| +| 1 | backup_passphrase_postgres_evolution | 🟡 DEFER | Backup é gzip puro (sem cripto). Sem caller real. Regenerar quando precisar. | +| 2 | email_sender_secret | ✅ | gotrue env (= smtp_password) | +| 3 | evolution_api_key | ✅ `` | env Evolution (em USO). Há divergência com secret v1 `` mas Evolution roda com o env. | +| 4 | evolution_api_url | ✅ `https://evolution.atomicabr.com.br` | env SERVER_URL | +| 5 | evolution_instance_name | ✅ `wpp2` | whatsapp_connections is_default=true | +| 6 | evolution_postgres_dsn | ✅ | docker secret evolution_db_uri_v1 | +| 7 | evolution_postgres_password | ✅ | extraído do DSN | +| 8 | evolution_webhook_secret | ✅ | docker secret webhook_secret_v1 | +| 9 | minio_access_key | ✅ `` | minio IAM service-accounts | +| 10 | minio_endpoint_internal | ✅ `http://minio:9000` | inferido | +| 11 | minio_endpoint_public | ✅ `https://minio.atomicabr.com.br` | inferido | +| 12 | minio_media_bucket | ✅ `evolution-media` | minio buckets ls | +| 13 | minio_root_password | ✅ `` | docker secret minio_s3_secret_key_v1 | +| 14 | minio_root_user | ✅ `` | docker secret minio_s3_access_key_v1 | +| 15 | minio_s3_api_endpoint | ✅ `https://s3.atomicabr.com.br` | inferido | +| 16 | minio_secret_key | 🟡 DEFER | MinIO desligado, secret de service account não está em texto plano (xl.meta) | +| 17 | portainer_api_key | ✅ | docker secret portainer_api_key | +| 18 | restore_test_ingest_token | 🟡 DEFER | Não há caller localizado. Regenerar se aparecer caller. | +| 19 | smtp_from_name | ✅ `Promo Brindes` | gotrue env | +| 20 | smtp_host | ✅ `smtp.gmail.com` | gotrue env | +| 21 | smtp_password | ✅ | gotrue env GOTRUE_SMTP_PASS | +| 22 | smtp_port | ✅ `587` | gotrue env | +| 23 | smtp_user | ✅ `ti04.promobrindes@gmail.com` | gotrue env | +| 24 | r2_access_key | ✅ | docker secret r2_s3_access_key_v1 | +| 25 | r2_bucket_media | ✅ `zapp-whatsapp-media` | env Evolution S3_BUCKET | +| 26 | r2_endpoint | ✅ `https://.r2.cloudflarestorage.com` | env Evolution S3_ENDPOINT | +| 27 | r2_secret_key | ✅ | docker secret r2_s3_secret_key_v1 | +| 28 | hf_api_token | 🟡 DEFER | Joaquim fornece quando voltar | +| 29 | openai_api_key | 🟡 DEFER | Joaquim fornece quando voltar | +| 30 | anthropic_api_key | 🟡 DEFER | Joaquim fornece quando voltar | +| 31 | openrouter_api_key | 🟡 DEFER | Joaquim fornece quando voltar | + +**Coletados: 23/31 | DEFER: 7/31** + +### Plano F3 + +1. Pausar cron jobid 27 (`fn_reconcile_dispatch`) +2. Snapshot pré-fix de `vault.secrets` +3. UPDATE 1-by-1 com validação imediata via decrypted_secrets +4. Em caso de falha em qualquer um: ROLLBACK + ABORT +5. Marcar os 8 DEFER com `description='DEFER-RECOVERY-PENDING-2026-05-09'` +6. Restart `supabase_functions` (D7=A) +7. Trigger manual `fn_reconcile_dispatch` + observar 5min +8. Reabilitar cron (NÃO ENQUANTO 5min nao validados) + +## Estado atual (pós-F3 recovery) + +**Phase:** F2 ✅ DONE (23/31, 8 DEFER) → F3 INICIANDO +**Last update:** 2026-05-09 ~16:30 UTC +**Next step:** F3.1 — pausar cron + snapshot + + +--- + +## ✅ F3 — Recovery cirúrgico (2026-05-09 ~16:50 UTC) + +### Execução + +| Step | Resultado | +|---|---| +| F3.1 Pausar cron jobid 27 | ✅ via `cron.alter_job(27, active := false)` | +| F3.2 Snapshot pré-fix | ✅ tabela `vault.secrets_snapshot_pre_fix_20260509` (32 rows) | +| F3.3 Test 1 secret (`smtp_port`=587) | ✅ match=t | +| F3.4 Recovery dos 7 evolution + email_sender | ✅ 7/7 match=t | +| F3.5 Recovery dos 16 minio + smtp + r2 + portainer | ✅ 16/16 match=t | +| F3.6 Validação consolidada via DO loop | ✅ **24/24 OK, 0 FAIL** | +| F3.7 Marcar 7 DEFER | ✅ description='DEFER-RECOVERY-PENDING-...' | +| F3.8 Limpar secret de teste `rca_test_*` | ✅ DELETE 1 | +| F3.9 Restart `supabase_functions` | ✅ Service converged | +| F3.10 Trigger manual `fn_reconcile_dispatch()` | ✅ retornou request_id 949 | +| F3.11 HTTP request 949 status_code | ✅ **200 OK** (Evolution API respondeu) | +| F3.12 Reabilitar cron jobid 27 | ✅ active=true | +| F3.13 Validar próximo run automático (08:30) | ✅ **succeeded** após 4 falhas seguidas pré-fix | +| F3.14 Test `fn_get_vault_secret('evolution_api_key')` | ✅ retorna `429683...` | +| F3.15 Test `fn_collect_restore_logs()` | ✅ 1 row | +| F3.16 Test `fn_validate_whatsapp_connection_url(uuid)` | ⚠️ função não existe com essa assinatura (não relacionado) | + +### Estado final do banco + +```text +Total secrets: 31 +Active (decrypts): 24 (todos OK) +DEFER (pending): 7 +``` + +### 7 secrets DEFER (não recuperáveis nesta sessão) + +1. `backup_passphrase_postgres_evolution` — backup é gzip puro, sem caller +2. `minio_secret_key` — MinIO desligado, secret só legível dentro do MinIO live +3. `restore_test_ingest_token` — sem caller localizado +4. `hf_api_token` — Joaquim fornece quando necessário +5. `openai_api_key` — Joaquim fornece quando necessário +6. `anthropic_api_key` — Joaquim fornece quando necessário +7. `openrouter_api_key` — Joaquim fornece quando necessário + +Todos marcados com `description = 'DEFER-RECOVERY-PENDING-2026-05-09 — see internal incident record'` + +## ✅ F4 — Prevenção (2026-05-09 ~17:00 UTC) + +### F4.P1 — Backup imediato do volume `supabase_db_config` + +```text +Path: /supabase_db_config_20260510_112819.tar.gz +Size: 3.5 KB (volume é só configs, não data) +SHA256: 757bebf341e45ef9222bea0679b9130bc002dd47437d810d64297e30316e8d48 +``` + +### F4.P3 — Runbook criado + +`docs/runbooks/SUPABASE-VOLUMES-DOS-AND-DONTS.md` (versionado neste repositório) + +Conteúdo: lista de volumes críticos, comandos proibidos, sobre pgsodium_root.key, como fazer backup correto, caso histórico Onda 9, plano de migração futura. + +### Pendências F4 (próxima sessão) + +- F4.P1 estendida: criar **stack/cron AUTOMATIZADO** do volume db-config (hoje só fiz manual) +- F4.P2: healthcheck SQL `SELECT count(*) FROM vault.decrypted_secrets WHERE name='evolution_api_key'` em cron de 5min, alerta GlitchTip se NULL +- F4.P4: PR no zapp-web adicionando `# DO NOT REMOVE — pgsodium key` em docker-compose-supabase +- F4.P5: GitHub issue rastreando deprecação pgsodium + +## 🎯 Resultado da Onda 9 + +### O que funcionava ANTES (broken) + +- ❌ `fn_reconcile_dispatch` — falhava a cada 5 min com invalid ciphertext (864 falhas em 3 dias) +- ❌ `fn_get_vault_secret` (qualquer caller) +- ❌ `fn_collect_restore_logs` +- ❌ Edge functions que leem do vault (potencialmente) + +### O que funciona AGORA + +- ✅ 24 dos 31 secrets do vault decifram +- ✅ `fn_reconcile_dispatch` voltou a operar (1ª succeed em 08:30) +- ✅ Comunicação Postgres → Evolution API restaurada +- ✅ HTTP 200 confirmado (`wpp_pink_test connectionStatus=open`) +- ✅ Snapshot vault.secrets_snapshot_pre_fix_20260509 disponível pra rollback + +### Lições aprendidas + +1. **`pgsodium_root.key` é o único ponto de falha catastrófico** do vault +2. **mtime preservado** em substituição = bug latente (só aparece em restart) +3. **ctime nunca mente** sobre toque no arquivo +4. **`vault.update_secret()` ou UPDATE direto** funcionam — trigger BEFORE UPDATE cifra automaticamente com key atual +5. **AD inclui description** — mudar description corrompe o ciphertext existente +6. **24/31 secrets eram acessíveis via env de containers** — boa redundância (mas precisa explicitar como política) + +## Estado atual (encerramento F4) + +**Phase:** F4 ✅ DONE (parcial — itens automatizados em backlog) → Onda 9 FECHADA +**Last update:** 2026-05-09 ~17:00 UTC +**Estado VAULT:** 24/31 active OK, 7 DEFER +**Estado fn_reconcile_dispatch:** OPERANDO (cron 27 succeeded em 08:30) +**Backup volume db-config:** sim, em armazenamento interno (acesso controlado) + +## ⚠️ Pendências para próxima sessão + +1. **Joaquim fornece** `hf_api_token`, `openai_api_key`, `anthropic_api_key`, `openrouter_api_key` → recovery dos 4 AI keys +2. **Decidir** sobre `minio_secret_key`: religar MinIO pra extrair OU regenerar +3. **Decidir** sobre `restore_test_ingest_token`: investigar caller real OU regenerar +4. **F4.P1 automatizado**: criar cron de backup do volume `supabase_db_config` +5. **F4.P2 healthcheck**: monitor 5min de decrypted_secrets +6. **F4.P4 PR**: comentário no docker-compose +7. **F4.P5 issue**: tracking deprecação pgsodium +8. **Bug Onda 7 latente**: `fn_validate_whatsapp_connection_url(uuid)` não existe — investigar se é bug real + + +--- + +## ✅ F4 EXTENDED — Healthcheck automatizado (2026-05-09 ~17:35 UTC) + +### Funções/views/jobs criados + +| Objeto | Tipo | Função | +|---|---|---| +| `public.fn_vault_healthcheck()` | function | Retorna jsonb com ok/fail/defer/status | +| `public.fn_vault_healthcheck_run()` | function | Wrapper que executa + loga em tabela | +| `public.fn_vault_healthcheck_cleanup()` | function | Remove logs > 30 dias | +| `public.vault_healthcheck_log` | tabela | Histórico de checks (com index DESC em checked_at) | +| `public.v_vault_health` | view | Top 20 últimos checks com `age` | +| `cron.job` jobid 45 | cron | `vault_healthcheck` a cada 15min | +| `cron.job` jobid 46 | cron | `vault_healthcheck_cleanup` 4am diário | + +### Como verificar saúde do vault + +```sql +-- Status atual +SELECT public.fn_vault_healthcheck(); + +-- Histórico recente +SELECT * FROM public.v_vault_health; + +-- Status raw +SELECT * FROM public.vault_healthcheck_log ORDER BY id DESC LIMIT 10; +``` + +Esperado em healthy: `{"ok":24,"fail":0,"defer":7,"status":"healthy"}` + +Se aparecer `status:degraded` ou fail>0 → vault em problemas, investigar. + +## 🎬 ONDA 9 — FECHADA + +### Resumo executivo + +**Problema:** 31 secrets em `vault.secrets` corrompidos (`invalid ciphertext`). 5 funções postgres broken. fn_reconcile_dispatch falhando a cada 5min há ~1 dia. + +**Causa raiz:** `pgsodium_root.key` foi tocada em 2026-04-29 (ctime mudou, mtime preservado). Postgres reiniciou em 2026-05-06 e carregou key NOVA do disco. Secrets criados em 03/04 mai foram cifrados com key antiga → órfãos permanentemente. + +**Resolução:** Reconstrução de 24/31 valores via env vars dos containers Docker. 7 marcados DEFER. + +**Tempo total:** ~2h (RCA + coleta + reconstrução + validação + prevenção). + +**Risco residual:** baixo. 24 secrets active funcionando. 7 DEFER são de sistemas opcionais (4 AI keys que Joaquim pode regenerar; minio offline; backup_passphrase sem caller; restore_test_ingest_token sem caller localizado). + +### Tudo o que foi entregue + +| Frente | Item | Status | +|---|---|---| +| F1 | RCA confirmatória (13 evidências) | ✅ | +| F2 | Coleta de 23 valores via 4 fontes (Docker secrets, env containers, MinIO IAM, gotrue env) | ✅ | +| F3 | UPDATE de 24 secrets com validação byte-a-byte | ✅ | +| F3 | 7 DEFER marcados com description rastreável | ✅ | +| F3 | Snapshot pré-fix (`vault.secrets_snapshot_pre_fix_20260509`) | ✅ | +| F3 | Restart `supabase_functions` | ✅ | +| F3 | Trigger manual fn_reconcile_dispatch → HTTP 200 | ✅ | +| F3 | Cron 27 reabilitado e succeeded | ✅ | +| F4 | Backup imediato volume db-config (3.5KB tar.gz) | ✅ | +| F4 | Runbook `SUPABASE-VOLUMES-DOS-AND-DONTS.md` | ✅ | +| F4 | Healthcheck SQL + view + cron 15min + cleanup 30d | ✅ | + +### Backlog para próximas sessões + +1. **Joaquim fornece** 4 AI keys (hf, openai, anthropic, openrouter) → recovery +2. **Investigar** se `minio_secret_key` é necessário (provavelmente DEPRECATED, R2 substituiu) +3. **Investigar** se `restore_test_ingest_token` tem caller (provavelmente não) +4. **F4.P1 automatizado**: cron de backup do volume `supabase_db_config` (hoje só manual) +5. **F4.P4**: PR no zapp-web — comentário "DO NOT REMOVE" no docker-compose-supabase +6. **F4.P5**: GitHub issue tracking deprecação pgsodium +7. **Bug pendente**: `fn_validate_whatsapp_connection_url(uuid)` não existe — é referência stale? + +## Estado FINAL desta sessão + +```text +Vault: + Total: 31 secrets + Active: 24 (todos decifrando OK) + DEFER: 7 (controlados, marcados, documentados) + +Crons: + jobid 27 whatsapp_reconcile_dispatch */5 * * * * ✅ succeeded + jobid 45 vault_healthcheck */15 * * * * ✅ healthy + jobid 46 vault_healthcheck_cleanup 0 4 * * * ✅ + +Healthcheck: + Status: healthy + ok=24 fail=0 defer=7 + +Backups: + /supabase_db_config_20260510_112819.tar.gz (3.5KB) + +Snapshot rollback: + vault.secrets_snapshot_pre_fix_20260509 (32 rows preservadas) + +Runbook: + docs/runbooks/SUPABASE-VOLUMES-DOS-AND-DONTS.md +``` diff --git a/docs/runbooks/SUPABASE-VOLUMES-DOS-AND-DONTS.md b/docs/runbooks/SUPABASE-VOLUMES-DOS-AND-DONTS.md new file mode 100644 index 000000000..2054a4e14 --- /dev/null +++ b/docs/runbooks/SUPABASE-VOLUMES-DOS-AND-DONTS.md @@ -0,0 +1,181 @@ +# Runbook: Supabase Self-Hosted — Volumes Críticos (Do's and Don'ts) + +**Criado:** 2026-05-09 (após Onda 9 — recuperação cirúrgica do Vault corrompido) +**Aplicável a:** stack `supabase` em Docker Swarm na VPS AtomicaBR (`zapp.atomicabr.com.br`) +**Owner:** Joaquim (Promo Brindes) + +--- + +## TL;DR + +O Supabase self-hosted depende de **arquivos críticos no volume `supabase_db_config`** que, se removidos ou substituídos sem cuidado, **corrompem todos os secrets do Vault** (e podem corromper outros dados criptografados). A Onda 9 aconteceu porque alguém fez `cp -p` da `pgsodium_root.key` em 2026-04-29; quando o postgres reiniciou em 2026-05-06, todos os 31 secrets viraram `invalid ciphertext`. + +Este runbook lista **o que NÃO fazer**, como **fazer backup correto** e o **caso histórico** que motivou a criação. + +--- + +## Volumes críticos + +| Volume Docker | Mount no container `supabase_db` | Conteúdo crítico | +|---|---|---| +| `supabase_db_data` | `/var/lib/postgresql/data` | Dados do PostgreSQL (`base/`, `pg_wal/`, `pg_xact/`, etc.) | +| `supabase_db_config` | `/etc/postgresql-custom` | **Chave-mestra `pgsodium_root.key`** + `pgsodium.conf` + custom configs | +| `supabase_db_log` | `/var/log/postgresql` | Logs do postgres (não-crítico, recriável) | + +> ⚠️ **`supabase_db_config` é o volume mais frágil.** Tem só ~3.5 KB mas guarda a chave que decifra todo o vault. **Se sumir, todos os secrets viram lixo.** + +--- + +## ❌ DO NOT — Operações proibidas + +### 1. Nunca substitua `pgsodium_root.key` "para alinhar com outro ambiente" + +A chave **só funciona com os ciphertexts criados com ela**. Substituí-la = **perder todos os secrets cifrados anteriormente**. + +```bash +# ❌ NUNCA FAÇA +cp -p outra-pgsodium_root.key /etc/postgresql-custom/pgsodium_root.key + +# ❌ NUNCA FAÇA (mesmo que pareça idempotente) +echo "$KEY" | docker exec -i supabase-db tee /etc/postgresql-custom/pgsodium_root.key +``` + +### 2. Nunca rode `docker volume rm supabase_db_config` + +Mesmo "para limpar e recriar". Não há recriação possível dos secrets sem a chave original. + +### 3. Nunca remova/recrie o stack `supabase` com `--prune` sem backup do volume + +`docker stack rm supabase` preserva volumes; `docker volume prune` mata volumes órfãos. Se o stack for recriado depois, pode pegar volumes novos zerados. + +### 4. Nunca confie que "se o postgres está rodando, tá tudo bem" + +A corrupção do vault só aparece quando **alguém tenta decifrar um secret**. O postgres roda normal, mas: + +```sql +SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'foo'; +-- ERROR: pgsodium_crypto_aead_det_decrypt_by_id: invalid ciphertext +``` + +Daí em diante todo caller que depende daquele secret quebra silenciosamente. + +### 5. Nunca pule o Vault healthcheck + +A Onda 9 instalou o cron `vault_healthcheck` (a cada 15min) — se ele estiver `active=false` ou se `public.v_vault_health` mostrar `status='degraded'`, **investigue imediatamente**. + +```sql +SELECT * FROM public.v_vault_health LIMIT 5; +SELECT jobid, jobname, schedule, active FROM cron.job WHERE jobname LIKE 'vault%'; +``` + +--- + +## ✅ DO — Procedimentos corretos + +### 1. Backup do volume `supabase_db_config` + +Antes de qualquer mudança no stack supabase: + +```bash +# Snapshot do volume (rodar do host ou container privilegiado) +docker run --rm \ + -v supabase_db_config:/source:ro \ + -v /tmp:/backup \ + alpine:3.19 \ + tar -czf /backup/supabase_db_config_$(date +%Y%m%d_%H%M%S).tar.gz -C /source . + +# Verificar tamanho/conteúdo +ls -la /tmp/supabase_db_config_*.tar.gz +docker run --rm -v /tmp:/data alpine:3.19 tar -tzf /data/supabase_db_config_*.tar.gz +``` + +Backup deve ter **`pgsodium_root.key`** (~32 bytes) e os arquivos `.conf`. Se não tiver `pgsodium_root.key`, **NÃO destrua o volume original** — investigue antes. + +### 2. Healthcheck antes/depois de operações de risco + +```sql +-- Antes (snapshot) +SELECT * FROM public.fn_vault_healthcheck(); + +-- Depois (validação) +SELECT * FROM public.fn_vault_healthcheck(); +``` + +Comparar `ok`/`fail`/`defer`. Idealmente: `ok=24, fail=0, defer=7` (estado pós-Onda 9 — pode mudar conforme novos secrets entrem). + +### 3. Snapshot da tabela `vault.secrets` antes de operação SQL crítica + +```sql +CREATE TABLE vault.secrets_snapshot_pre__ + AS SELECT * FROM vault.secrets; +``` + +Permite rollback row-by-row se algo der errado. A Onda 9 manteve `vault.secrets_snapshot_pre_fix_20260509` (32 rows) e foi essencial para validação. + +### 4. Para deploy/redeploy do stack supabase + +```bash +# 1. Backup do volume +docker run --rm -v supabase_db_config:/source:ro -v /tmp:/backup alpine:3.19 \ + tar -czf /backup/supabase_db_config_$(date +%Y%m%d_%H%M%S).tar.gz -C /source . + +# 2. Snapshot do vault +PGHOST=supabase_db psql -d postgres -c \ + "CREATE TABLE vault.secrets_snapshot_pre_redeploy_$(date +%Y%m%d) AS SELECT * FROM vault.secrets;" + +# 3. Healthcheck pré-deploy +PGHOST=supabase_db psql -d postgres -c "SELECT * FROM public.fn_vault_healthcheck();" + +# 4. Deploy +docker stack deploy -c docker-compose-supabase.yml supabase + +# 5. Aguardar containers estabilizarem (30s) +sleep 30 + +# 6. Healthcheck pós-deploy +PGHOST=supabase_db psql -d postgres -c "SELECT * FROM public.fn_vault_healthcheck();" +``` + +Se `fail > 0` no passo 6, **pause e investigue** antes de reabilitar tráfego. + +--- + +## 🚨 Caso histórico — Onda 9 (2026-05-09) + +**Sintoma**: cron `whatsapp_reconcile_dispatch` falhando há 6 dias com `invalid ciphertext`. 864 falhas acumuladas. Comunicação Postgres → Evolution API quebrada. + +**Causa raiz**: +- Chave `pgsodium_root.key` em `/etc/postgresql-custom/pgsodium_root.key` +- `mtime`: Dec/2024 (preservado) +- **`ctime`: 2026-04-29 22:24:38** ← alguém substituiu via `cp -p` em 29/04 +- Postgres só leu a chave nova quando reiniciou em 2026-05-06 10:31:59 (recriação de container) +- Daí em diante: secrets criados com chave **A** (cache em memória) viraram lixo ao tentar decifrar com chave **B** (em disco) + +**Recovery**: 24/31 secrets recuperados via fontes alternativas (docker secrets, env vars, MinIO IAM, whatsapp_connections). 7 marcados `DEFER` (não-recuperáveis). Snapshot pre-fix preservado. + +**Lições**: +1. **`mtime` mente; use `ctime`** para detectar modificações via `cp -p` +2. Postgres só carrega `pgsodium_root.key` no startup — corrupção fica latente até reboot +3. Snapshot do volume **deve ser feito ANTES de qualquer operação no stack** (não depois) +4. Healthcheck contínuo do vault é obrigatório (cron `vault_healthcheck`) + +--- + +## Plano de migração futura (deprecação pgsodium) + +`pgsodium` está marcado **pending deprecation** pelo Supabase ([issue #1204](https://github.com/orgs/supabase/discussions/1204)). Migração futura recomendada: + +- **Curto prazo (atual)**: pgsodium + Vault. Backup contínuo do volume `supabase_db_config`. +- **Médio prazo**: avaliar [Supabase Vault v2](https://supabase.com/docs/guides/database/vault) (TCB-based) quando estabilizar. +- **Longo prazo**: secrets gerenciados via container env + Docker secrets (pattern atual da Evolution API). Vault só pra dados que **precisam** ser decifrados via SQL. + +Tracking: GitHub issue (a criar em PR Onda 9.3). + +--- + +## Referências + +- Doc-mãe da Onda 9: `docs/handoff-2026-05-09-onda-9-vault-recovery.md` +- Migration do healthcheck: `supabase/migrations/20260510131942_a5fcc99c-92cc-4be6-895a-f9a0d453a871.sql` +- Backup volume (artefato): `/workspace/notes/backups/supabase_db_config_20260510_112819.tar.gz` +- Issue Supabase pgsodium deprecation: https://github.com/orgs/supabase/discussions/1204