Blogue ·
Contra o encaminhamento de portas do painel
Um agente que liga para fora precisa de duas regras de saída e de nenhuma porta de entrada. O que isso dá, o que custa e os quatro problemas que só desloca.
Por Provibr
Se uma plataforma de gestão vai gerir os seus hipervisores, alguma coisa tem de atravessar a fronteira entre a rede deles e a sua. Há duas direções por onde escolher, e a escolha determina quase tudo o resto no modelo de segurança.
A direção tradicional é para dentro. A plataforma guarda credenciais do seu cluster Proxmox, da sua máquina DirectAdmin e do seu painel de jogos, e liga-se a cada um deles pela internet. Isso significa uma API a responder à internet, uma regra de firewall ou uma VPN, e um plano de controlo alcançável por quem o conseguir encontrar.
A outra direção é para fora. Corre um pequeno agente dentro da sua própria rede. Ele abre uma ligação à plataforma e mantém-na aberta, os comandos descem por essa ligação e os resultados sobem por ela. Do seu lado, nada fica à escuta.
Este texto é sobre o que a segunda opção custa e o que lhe dá, escrito por quem construiu uma.
A alteração no firewall são duas regras, e ambas de saída
Todo o requisito de rede de um agente de saída cabe numa tabela:
| Direção | Regra | Quando |
|---|---|---|
| Saída | TCP 50051 para o gateway da plataforma | Uma vez, na inscrição |
| Saída | TCP 50052 para o mesmo anfitrião | Sempre, é esta a sessão |
| Entrada | Nenhuma |
Duas portas em vez de uma é uma separação deliberada. A inscrição acontece antes de o agente ter um certificado de cliente, pelo que essa porta só pode oferecer TLS do lado do servidor. A porta da sessão exige TLS mútuo e recusa tudo o que não traga um certificado de cliente válido. Um único ouvinte para as duas significaria um ouvinte onde o certificado de cliente é opcional e a decisão de autorização é condicional. Dois ouvintes tornam a fronteira estrutural: na 50052 não há caminho de código em que um par não autenticado passe do aperto de mão.
Se a sua filtragem de saída é estrita, essas duas regras são tudo o que se pede. Não há regra de entrada para escrever, nem entrada de NAT, nem concentrador de VPN, nem nada para renovar quando o seu endereço público mudar.
O que «para fora» significa quando a rede é hostil
Uma ligação TCP de saída através de NAT não é gratuita: o estado que a faz funcionar vive na tabela de outra pessoa, e essa tabela tem um tempo-limite. O NAT do operador, um firewall empresarial e um balanceador de carga na nuvem esquecem todos, em silêncio, uma ligação inativa, e o modo de falha é o pior de todos, porque ambas as pontas continuam a acreditar que o socket está bom.
Dois mecanismos tratam disso, e precisa dos dois.
O primeiro é o keepalive do HTTP/2, a intervalos de 30 segundos com um tempo-limite de 10 segundos. É essa a camada que vence os tempos-limite do NAT e do balanceador de carga, e a única que dá pela ligação TCP que morreu em silêncio por baixo do fluxo.
O segundo é uma pulsação da aplicação a cada 20 segundos, com um vigia que termina a sessão se não chegar confirmação dentro de 45 segundos. O keepalive prova que o socket está vivo; a pulsação prova que a outra ponta continua a processar.
Quando a sessão termina mesmo, o agente volta a ligar-se com recuo exponencial e jitter completo: a espera é sorteada uniformemente entre zero e o teto atual, que duplica de um segundo até sessenta. O jitter não é enfeite. Sem ele, todos os agentes de todos os clientes voltam a ligar-se no mesmo instante depois de o gateway reiniciar, e a sua recuperação transforma-se numa debandada provocada por si próprio.
Um pormenor que só aparece em produção: o contador de recuo só se repõe depois de uma sessão ter estado saudável durante um minuto inteiro. Sem essa regra, um agente com uma ligação meio partida liga-se, morre, repõe o contador a um segundo e martela o gateway uma vez por segundo para sempre.
Identidade: um certificado, não uma palavra-passe
Um modelo de entrada autentica a plataforma perante o seu painel, normalmente com uma chave de API que se cola num formulário. Um modelo de saída tem de autenticar o agente perante a plataforma, e pode fazê-lo com algo melhor do que um segredo partilhado.
O agente inscreve-se uma vez com um token de utilização única: 36 bytes aleatórios, 48 caracteres base64url, guardado do lado do servidor apenas como um resumo SHA-256 e válido durante 72 horas. É consumido dentro de uma transação que volta a verificar as condições pelas quais foi selecionado, porque é entre a leitura e a escrita que se infiltra uma inscrição duplicada.
O que volta é um certificado de cliente. A partir daí a sessão é mutuamente autenticada, e o gateway fixa o par exato: pega na impressão digital SHA-256 do certificado apresentado e procura o agente por essa impressão, antes de o agente ter dito quem julga ser. A identidade fica fixada pelo certificado; a primeira mensagem no fluxo só a pode confirmar.
A rotação corre no relógio do próprio agente: os certificados vivem 90 dias e a renovação começa a meio, ao dia 45. Isso deixa 45 dias de novas tentativas antes de alguma coisa estar em risco, e o painel só avisa uma pessoa quando faltam 21 dias, ou seja, a renovação tem de ter falhado durante três semanas antes de alguém ser incomodado. Cada rotação gera um novo par de chaves: renovar sobre a mesma chave prolonga a vida de algo que pode já ter fugido.
A revogação merece uma frase própria, porque a versão honesta é menos elegante do que o diagrama. Não há CRL nem OCSP. Revogar um agente é uma atualização na base de dados, e uma sessão ligada é confrontada com ela uma vez por minuto. Essa verificação falha fechada, com um orçamento de três falhas seguidas, para que um soluço da base de dados não possa manter vivo em silêncio um agente revogado, e o pior caso é limitado e calculável: sessenta segundos vezes três.
Exatamente uma vez, numa ligação que vai cair
Assim que se aceita que a ligação é um fluxo de longa duração sobre um caminho pouco fiável, a semântica de entrega deixa de ser uma preocupação teórica. A plataforma vai voltar a entregar um comando depois de uma reconexão, porque do lado dela a diferença entre «o agente não o recebeu» e «o resultado não voltou» é invisível.
Por isso o agente lembra-se. Os comandos concluídos são guardados em disco com os seus resultados, indexados pelo identificador do comando, numa LRU limitada. A repetição de um comando terminado devolve o resultado em cache e não executa nada. A repetição de um comando ainda em curso é ignorada, porque a resposta já vem a caminho.
Em disco, sublinhe-se, e é esse o ponto: uma cache em memória está vazia exatamente quando a plataforma volta a oferecer tudo, que é logo a seguir a um reinício.
A direção inversa é igual. Os eventos saem por uma caixa de saída sequenciada; ao reconectar, a plataforma indica o número de sequência que tem e o agente repete a partir daí. A caixa de saída é limitada e, quando alguma coisa cai fora dela, o agente di-lo na ligação seguinte, declarando a sequência mais baixa que ainda guarda menos um, para que a plataforma deixe de esperar por eventos que já não existem.
Há uma fenda honesta na promessa, e mais vale dizê-la do que escondê-la: se a escrita do registo de desduplicação em disco falhar, o resultado é enviado à mesma. A alternativa seria deitar fora o resultado de um comando concluído para proteger uma garantia, e, na pior das hipóteses, uma reentrega posterior executa-o uma segunda vez.
As credenciais ficam do seu lado
É esta a parte que muda a forma de uma violação.
No modelo de entrada, a plataforma guarda credenciais que dão controlo sobre a sua infraestrutura, e esse depósito é o prémio. No modelo de saída, as credenciais dos seus hipervisores e painéis vivem num cofre cifrado no seu próprio anfitrião, enviadas para lá uma única vez quando adiciona uma integração.
O nosso cofre usa XChaCha20-Poly1305 com uma chave derivada por HKDF-SHA256 a partir de um ficheiro de chave local e do identificador da máquina, com o identificador do agente misturado na informação de derivação. Uma consequência que vale a pena explicitar: copiar o vault.enc para outra máquina dá-lhe um ficheiro que não vai decifrar, porque a máquina a que estava ligado faz parte da chave.
E o limite, que escrevemos no código ao lado da implementação, é que isto não protege contra o root no anfitrião. Nem precisa. O cliente é dono do anfitrião e dono das credenciais. O cofre protege contra um ficheiro fugido, não contra o dono.
O rasto de auditoria segue a mesma regra. Cada comando é uma linha que nunca é apagada em definitivo, e o conteúdo de um envio de credenciais fica guardado com os valores ocultados. Uma tabela de auditoria que contém os segredos que audita é uma segunda cópia do problema.
Quatro coisas que isto não resolve
Um texto de arquitetura que só enumera vantagens é publicidade. Eis o que se desloca em vez de desaparecer.
Software no seu anfitrião. A troca é real: deixa de expor uma porta e, em contrapartida, corre um binário que tem de ser instalado, atualizado e merecer confiança. Isso passa a pergunta de «quem consegue chegar ao meu painel» para «quem assina o código que eu corro», que é uma pergunta melhor mas não é gratuita.
Privilégios amplos. Um agente que gere um hipervisor detém credenciais que gerem um hipervisor. A direção da ligação não muda nada quanto ao que essas credenciais podem fazer, só onde vivem e quem mais lhes chega.
A saída continua a ser uma saída. Uma ligação que sai da sua rede pode levar dados para fora dela. O destino é fixo, o transporte é mutuamente autenticado e o protocolo é estreito, mas «duas regras de saída» é uma afirmação mais pequena do que «não sai nada».
Respostas em tempo real. Esta aparece logo na API. Como a plataforma não pode chamar o agente, uma escrita não pode responder com o seu resultado: a resposta correta a «cria esta máquina» é uma confirmação e algo para consultar. As leituras não são afetadas, porque vêm da base de dados da própria plataforma. Quem desenhar uma API por cima de um agente de saída tem de aceitar essa assimetria em vez de a disfarçar.
Existe também exatamente um caso em que o agente fica à escuta, e fingir o contrário seria desonesto. Instalar bare metal pela rede significa responder a DHCP e servir ficheiros, o que não se faz a partir de um socket de saída. Por isso o agente vincula esses serviços, numa interface nomeada, apenas para máquinas com um trabalho em curso, e apenas enquanto houver trabalho a fazer. «Ativado» quer dizer «permitido aqui», não «à escuta agora».
Quando a entrada continua a ser a resposta certa
Se explora um único hipervisor e é você que o gere, nada disto se aplica: um agente não lhe traz nada que uma regra de firewall e uma palavra-passe forte não tragam.
O caso agudiza-se com a distância e com a quantidade. Vários locais, redes que não são suas, endereços que mudam, clientes cujos firewalls não pode editar, e um plano de controlo que preferia não ter de todo na internet pública. Nesse ponto, o agente não é um componente a mais; é o componente que lhe permite apagar os restantes.
É assim que o Provibr funciona: uma ligação de saída por agente, nenhuma porta de entrada, e credenciais que nunca saem da rede do cliente. A documentação do agente cobre a instalação, o dimensionamento e a revogação, o guia de arranque percorre a primeira ligação de ponta a ponta, e a API documenta a forma de confirmar e consultar que o desenho de saída impõe.