Vitor Bellini
Todos os posts

Como uns tokens de sobra viraram o Rosen

7 min de leitura #meus-projetos #inteligencia-artificial

1.00

A reunião era sobre outra coisa, mas uma hora caiu na pergunta: como a gente acessa o banco de stage? O Postgres fica num servidor do Forge, fechado para a internet, e não queríamos adicionar complexidade para algo simples e "urgente". A conclusão foi a mais sensata: conectar por túnel SSH.

ssh -N -L 5433:127.0.0.1:5432 user@meu-servidor

Foi aí que eu soltei: "já sabe o que vou fazer com os tokens de sobra, né...".

Não era só piada. O túnel resolve o acesso, mas cobra um preço pequeno toda vez: um terminal aberto só para segurar a conexão, e um comando que some da memória. Stage não é um ambiente que eu acesso toda hora. Na próxima vez que eu precisar, vou ter que lembrar a porta local, o usuário, o caminho da chave e o IP do servidor. Eu queria guardar isso uma vez, com um nome que eu reconheça, e depois só ligar.

O banco continua fechado

Antes de qualquer app, vale dizer por que a gente não simplesmente abre a porta do banco. O Postgres está configurado para somente escuta só em localhost, e o firewall bloqueia a 5432. A única porta aberta é a 22, do SSH, e só entra quem tem a chave. Se quiser conferir no seu servidor:

nc -z -w 4 meu-servidor 22     # aberta
nc -z -w 4 meu-servidor 5432   # fechada

Abrir daria trabalho e traria problema: o banco ficaria exposto a qualquer varredura da internet, protegido só pela senha. Sem SSL e cloudflare, os dados das consultas passariam abertos. E liberar só para o meu IP quebra toda vez que o IP muda. O túnel junta as duas trancas: a chave SSH abre o servidor, e o usuário e a senha do Postgres abrem o banco.

O túnel nunca foi o problema. O problema era o trabalho de montar e manter ele de pé.

Início

Abri o Claude, criei uma pasta e escrevi o pedido mais ou menos assim: um app nativo de Mac, distribuído pelo Homebrew, para gerenciar túneis como aquele. Vários túneis, cada um com a sua chave ou senha, tudo criptografado. E a UX como prioridade: tinha que ser fluido de usar.

O jeito de trabalhar foi esse a noite toda. Eu descrevia o que queria, o agente construía, eu abria o app, testava e corrigia o rumo.

O primeiro tropeço não foi no código. O swift build não funcionava no meu Mac, nem num projeto vazio: as Command Line Tools tinham vindo com uma biblioteca do SwiftPM fora de sincronia com o resto. Logo depois, o SDK do macOS 27 reclamou de um plugin de macros do SwiftUI que não estava lá. A saída foi compilar com o swiftc direto, por um script, e usar o SDK 26, que também estava instalado. O app roda igual no macOS 27, e eu não precisei instalar o Xcode.

A primeira versão já fazia o essencial. Eu colava o comando ssh e o formulário se preenchia sozinho: servidor, usuário, portas, e até o -i da chave virava uma credencial. A porta local era sugerida sozinha (5432 vira 5433) e conferida antes, para não bater com nada que já estivesse rodando.

O túnel que dá para ver

Pedi um polimento visual, e o que saiu foi o desenho do túnel: três portais, o meu Mac, o servidor e o banco, ligados por um tubo. Com a conexão de pé, pacotes passam pelo tubo nos dois sentidos. Conectando, um brilho laranja percorre o tubo procurando caminho. Com erro, o tubo racha.

Gostei tanto que ele virou o centro do app. Aparece grande na tela de cada túnel, em miniatura no editor enquanto você digita as portas, e animado na tela inicial, antes de qualquer configuração. Parece enfeite, mas responde de longe a pergunta que importa: "isso está ligado?".

A senha que não foi pedida

Com o app funcionando, pedi Touch ID. Uma credencial com senha ou passphrase guardada passou a exigir a digital antes de ser usada e antes de mostrar o segredo na tela. Sem sensor disponível, o macOS pede a senha do Mac. O desbloqueio vale alguns minutos e cai na hora em que o Mac bloqueia.

Aí fiz um teste de usuário desconfiado: deixei a senha de uma credencial em branco para ver se o app pediria na hora de conectar. Não pediu. O túnel simplesmente falhava, porque a credencial sem senha era tratada como quebrada antes mesmo de chamar o ssh.

O ssh não aceita senha por argumento, e argumento nenhum é lugar para segredo: qualquer processo do usuário enxerga com um ps. O caminho é o SSH_ASKPASS, um programa que o ssh chama quando precisa de uma senha. O do Rosen é um script de poucas linhas que conversa com o app por dois FIFOs privados, um para a pergunta e outro para a resposta:

printf '%s\n' "$prompt" > "$ROSEN_ASKPASS_REQ"
IFS= read -r line < "$ROSEN_ASKPASS_RESP" || exit 1
case "$line" in
  OK:*) printf '%s\n' "${line#OK:}"; exit 0 ;;
esac
exit 1

Quando o servidor pede senha, passphrase ou um código 2FA, o Rosen abre um painel na hora, mesmo com a janela fechada. Ele oferece salvar no cofre e proteger com Touch ID, mas só salva depois que o servidor aceita, então uma senha errada nunca fica guardada. Testamos com o OpenSSH de verdade, errando a passphrase de propósito: ele pediu de novo, e a certa passou.

Tudo que fica salvo vai para um cofre único, criptografado com AES-256-GCM, cuja chave mora no Keychain e não sai do Mac.

Burrow, não. Rosen.

O app nasceu com o nome de Burrow, "toca" em inglês: o buraco que os bichos cavam. Fazia sentido, mas eu não gostei. Fomos atrás de outros nomes, e eu perguntei se "wormhole" fazia sentido. Fazia, e muito: um buraco de minhoca liga dois pontos distantes como se estivessem lado a lado, que é o que o túnel faz com o banco. Mas o nome já estava ocupado no Homebrew.

Ficamos com Rosen, da ponte de Einstein-Rosen, o nome científico do buraco de minhoca. Renomear não foi só trocar texto. Eu já tinha túneis salvos no cofre do Burrow, então o Rosen ganhou uma migração: na primeira abertura, ele lê o cofre antigo, regrava com uma chave nova, confere a cópia e só então apaga o original. Os túneis vieram junto, sem eu redigitar nada.

O ícone virou um buraco de minhoca de verdade: anéis afunilando até um núcleo de luz, num céu estrelado.

Publicado

No fim da noite, o Rosen foi para um repositório público no GitHub como 0.1.0-beta, com um zip que roda em Apple Silicon e Intel. A instalação ficou num comando, por um tap próprio do Homebrew:

brew install --cask bellinivitor/rosen/rosen

Uma coisa que eu não sabia: o repositório oficial do Homebrew é grátis, mas na prática exige que o app seja assinado e notarizado pela Apple, o que pede uma conta de desenvolvedor paga. Por enquanto, o tap próprio resolve.

O que ainda falta

É uma beta, e tem arestas que eu conheço:

  • O app não é assinado pela Apple. Na primeira abertura, o macOS bloqueia e é preciso liberar com um comando.
  • O Touch ID é uma trava do app. O cofre é criptografado, mas a chave dele no Keychain não depende da digital. Amarrar as duas também exige a assinatura da Apple.
  • A interface é só em português.

O túnel de stage, que começou a conversa na reunião, hoje é um interruptor com nome na minha barra de menus. Os tokens de sobra foram bem gastos.

O Rosen é gratuito e o código está no GitHub.

0 curtidas