Tynnaretrato de 22/09, 21:52
voltar à Biblioteca

Runbook — backup e restore do JeP

docs/runbooks/backup-restore-jep.md

Runbook — backup e restore do JeP

Estado: em operação contra a PRODUÇÃO do ARMELS desde 2026-09-03 (autorizado pelo board, gatilho "dado de cliente real"), e contra staging desde a missão E0. Os dois ambientes rodam o mesmo mecanismo com alvo, credencial B2, destinatários age e prefixo próprios.

Produção é READ-ONLY neste runbook: pg_dump + SELECT de contagem. Nenhuma escrita, nenhum DDL, e nenhum restore aponta para lá — o drill só restaura em Postgres efêmero, e guarda_alvo_efemero recusa qualquer host gerenciado (*.supabase.co, *.supabase.com, o ref de produção).

O que existe

Peçastagingprodução (ARMELS)
Scripts (fonte da verdade)scripts/backup/ neste repoidem
Wrapper de cronscripts/backup/wrapper-cron.shneo:/usr/local/bin/tynna-backup-jep.shidem
Deploy em execuçãoneo:/opt/tynna-backup-jep/idem
Credencial do alvoneo:/etc/tynna-backup-jep.env (600)neo:/etc/tynna-backup-jep-prod.env (600)
Credencial do B2neo:/etc/tynna-backup-jep-b2.env — app-key backup-jep-staging, prefixo jep-staging/neo:/etc/tynna-backup-jep-b2-prod.env — app-key backup-jep-producao, prefixo jep-producao/
Identidade age do drillneo:/etc/tynna-backup-jep.d/drill-identity.key (root-only)a mesma
Chave do fundadorKeychain do Mac, JEP_BACKUP_AGE_KEY_FOUNDER_STAGINGKeychain do Mac, JEP_BACKUP_AGE_KEY (conta tynna) + cópia offline
Canal de alarmeneo:/etc/paperclip-alerts.env (Telegram)idem
ArtefatosB2 tynna-paperclip-backups/jep-staging/B2 tynna-paperclip-backups/jep-producao/
Logneo:/var/log/tynna-backup-jep.logidem

Menor privilégio no B2 — a fronteira, provada

Cada ambiente tem app-key própria, restrita a um namePrefix, com as 5 capabilities mínimas (listBuckets, listFiles, readFiles, writeFiles, deleteFiles). Medido por mutação real em 2026-09-03 com a key de produção: escrita/list/delete dentro de jep-producao/ passam; list da raiz, jep-staging/ e daily/ devolvem 0 nomes; cat de um objeto real de jep-staging/ devolve 0 bytes; escrita e delete cruzados são recusados.

⚠️ Armadilha medida: rclone lsf sai com rc=0 e listagem vazia quando a key não alcança o prefixo. O código de saída não é sinal de autorização — conte os nomes devolvidos, não o rc.

Agendamento

0  2 * * *  (usuário paperclip) offsite-backup.sh            # off-site do Paperclip
30 3 * * *  /usr/local/bin/tynna-backup-jep.sh dump           # staging, diário
0  4 * * *  /usr/local/bin/tynna-backup-jep.sh dump  production   # PRODUÇÃO, diário
0  5 1 * *  /usr/local/bin/tynna-backup-jep.sh drill           # drill staging, dia 1
0  6 1 * *  /usr/local/bin/tynna-backup-jep.sh drill production   # drill PRODUÇÃO, dia 1

Quatro horários distintos, nenhum colide. O prune de cada ambiente é escopado no próprio prefixo — daily/ (Paperclip) nunca é tocado, e a credencial de produção sequer o enxerga.

O 2º argumento do wrapper é opcional e default staging, de propósito: as linhas de cron antigas seguem valendo sem edição, e apontar para produção é um ato explícito, escrito na linha de comando. O wrapper ainda confere que o ambiente pedido bate com o JEP_BACKUP_ENV declarado dentro do env file — um arquivo trocado por engano aborta em vez de rodar contra o alvo errado.

O que o artefato contém

Dois arquivos por execução, ambos cifrados com age:

  • jep-<env>-<STAMP>.sql.gz.agepg_dump dos schemas public + auth.
  • jep-<env>-<STAMP>.manifest.json.age — contagem de linhas por tabela lida na origem no momento do dump. É contra ele que o drill confere integridade, sem nunca reconectar na origem (requisito duro quando a origem for produção).

Por que auth entra junto

public tem 6 foreign keys para auth (ex.: public.auth_audit_log.user_idauth.users.id). Um dump só de public não restaura — morre em violação de FK. Isso foi medido, não suposto: foi o erro real do drill antes da correção.

Lacunas — o que o backup NÃO cobre

Fora do artefatoTamanho medido em produção (2026-09-03)Consequência num restore real
storage.*8 tabelas de metadado; 10 objetos / 625 kB de arquivo real, todos no bucket trabalhos-pdf (comprovantes, balaustres-pdf, loja-logos, potencia-docs estão vazios)Os arquivos em si vivem no S3 do Supabase e não entram em pg_dump nenhum. Hoje não têm backup nenhum.
realtime.*2 tabelasConfiguração de replicação; recriada pelo provedor.
cron.*2 tabelasJobs agendados somem. Os alertas de tesouraria (pg_cron) precisam ser recriados à mão.
vault.*1 tabelaSegredos do projeto; recriados à mão.
Config do projetoChaves de API, URLs de redirect, provedores de auth, Edge Functions, políticas de storage — nada disso é banco.

Restaurar num projeto Supabase NOVO não é "rodar este arquivo". Lá o GoTrue já é dono do schema auth, e a carga vira "dados de auth para dentro do schema existente". Este artefato prova integridade (tudo que a origem tinha volta, linha a linha); não é um restore turnkey de projeto Supabase.

Restore manual (o fundador, sozinho)

A metade privada da chave do fundador vive no Keychain do Mac dele (produção: serviço JEP_BACKUP_AGE_KEY, conta tynna, com cópia offline; staging: JEP_BACKUP_AGE_KEY_FOUNDER_STAGING, conta jep-staging). Ela não existe em lugar nenhum da frota. Com ela:

# 1. identidade a partir do Keychain (nunca em arquivo permanente)
security find-generic-password -s JEP_BACKUP_AGE_KEY -a tynna -w > /tmp/id.key

# 2. decifrar o artefato baixado do B2
age --decrypt -i /tmp/id.key < jep-production-<STAMP>.sql.gz.age | gunzip > dump.sql

# 3. restaurar num Postgres limpo (NUNCA por cima de um banco com conteúdo)
createdb restore_teste
psql -d restore_teste -v ON_ERROR_STOP=1 -f dump.sql

rm -f /tmp/id.key

Alarme

Alarme por transição de estado (Telegram), nunca diário: um backup que falha 30 dias seguidos manda 1 alarme, não 30. Alarme que toca todo dia vira ruído e para de ser lido — que é como falha silenciosa nasce.

Estado em neo:/var/lib/tynna-backup-jep/<componente>.state.

O drill também recusa artefato obsoleto (JEP_BACKUP_RPO_MAX_SEG, 48h por padrão): se o cron do dump parar, o drill fica vermelho mesmo com o arquivo íntegro. Cobertura parcial e declarada — se dump e drill pararem juntos, só um monitor externo pega.

Produção — números medidos (2026-09-03)

MétricaProdução (ARMELS)Staging (referência)
Tabelas no artefato101 (public 78 + auth 23) + 3 views101
Linhas conferidas no restore3.218 (60 tabelas com linhas)459
Banco na origem22 MB18 MB
Artefato cifrado448.126 B (≈438 KiB) + manifesto 5.343 B≈84 KiB
SQL decifrado1.602.220 B528.128 B
Dump ponta-a-ponta25 s (pg_dump 6 s)11 s
RTO32 s ponta-a-ponta (recorte só-restore 10 s)18 s
RPO53 s no drill imediato; ≤ 24 h em regime21 s

Método: date +%s no início do drill até a verificação de integridade passar (inclui subir o Postgres efêmero, baixar do B2, decifrar, restaurar e conferir); RPO derivado do stamp no nome do artefato, nunca inferido.

O que estes números NÃO capturam: o tempo humano de decidir restaurar, e o provisionamento de um projeto Supabase novo. Restaurar num projeto Supabase novo não é "rodar este arquivo" — ver a seção de lacunas.

Apontar um ambiente novo para produção (exige [BOARD])

Não é sessão de construção, é troca de alvo. Num env file próprio (não edite o de staging), p. ex. /etc/tynna-backup-jep-prod.env:

JEP_BACKUP_ENV=production
JEP_DATABASE_URL='<conexão do projeto de produção, senha percent-encoded>'
JEP_BACKUP_ALLOW_PRODUCTION=1     # a guarda recusa produção sem isto
JEP_BACKUP_B2_PREFIX=jep-producao # prefixo próprio, retenção própria

Nunca escreva arquivo de credencial com sed ou interpolação de shell. Chave B2 e senha contêm /, + e $; gere o arquivo e entregue-o por stdin (ssh host 'umask 077; cat > /etc/...'), conferindo o sha256sum dos dois lados. Isso já quebrou um arquivo e vazou um segredo em 2026-09-03.

A guarda é fail-closed nos dois eixos e foi exercitada: sem JEP_BACKUP_ALLOW_PRODUCTION=1 ou sem JEP_BACKUP_ENV=production, o script aborta ao ver o ref etoaddrlxbehtelapygj no alvo.

Diagnóstico rápido

ssh neo-root 'tail -30 /var/log/tynna-backup-jep.log'
ssh neo-root 'grep -H "" /var/lib/tynna-backup-jep/*.state'
ssh neo-root '/usr/local/bin/tynna-backup-jep.sh drill'              # drill de staging agora
ssh neo-root '/usr/local/bin/tynna-backup-jep.sh drill production'   # drill de PRODUÇÃO agora

São quatro componentes de alarme independentes: backup-jep-staging, drill-jep-staging, backup-jep-production, drill-jep-production.

Só leitura. Toda mudança nestes documentos é pull request.