Ir para o conteúdo

Blogue ·

Um segredo que viaja uma única vez

Uma verificação de monitorização precisa de uma palavra-passe do outro lado. Assim chega a uma sonda, para uma tentativa, sem cair nunca num registo.

Por Provibr

Algumas verificações de monitorização precisam de credenciais. Não se pode perguntar a um servidor PostgreSQL se está saudável sem iniciar sessão, e uma instância Redis com palavra-passe não responde nada de útil enquanto não lha enviarmos. Uma regra de monitorização tem portanto de levar um segredo daqui até à sonda ou ao agente que por acaso fizer a tentativa seguinte.

A versão barata disto é uma coluna. Pôr a palavra-passe na regra, lê-la ao enviar, mandá-la junto. Essa versão está errada de uma forma que só se torna visível treze meses depois, e esta é a forma que construímos em vez dela.

Os dois caminhos que já existiam, e porque nenhum serve

Um comando de monitorização chega a uma fonte por dois sítios, e ambos são inadequados.

A tabela de revisões da regra é imutável por desenho, o que é exatamente certo para uma revisão e exatamente errado para uma credencial: ninguém consegue rodar uma palavra-passe que vive numa linha que não pode mudar sem produzir uma revisão nova de cada regra que a usava.

A linha do comando é pior, porque é uma linha de auditoria e atravessa o barramento de mensagens a caminho da saída. As nossas próprias notas de projeto dizem há um ano que uma linha de auditoria de comando contém nomes de campo e nunca valores. Uma palavra-passe que ali aterre uma vez fica durante todo o período de retenção, legível por qualquer pessoa com acesso à base de dados e copiada para a cópia de segurança que correu nessa noite.

Existe ainda um canal lateral transitório para exatamente este tipo de coisa, usado quando alguém encomenda uma máquina e uma palavra-passe de administração tem de chegar ao agente sem ficar guardada. Também não serve aqui. Esse canal existe para segredos que o criador de um comando já conhece, e aqui o criador é um planeador que corre minutos antes e ainda não sabe que fonte ficará com o trabalho.

As quatro mensagens de protocolo acrescentadas nesta ronda não têm por isso campo de utilizador nem campo de palavra-passe. Não é que os campos estejam por usar: não existem.

Selado no momento do envio

O envelope é criado no processo de sessão, mesmo antes de a mensagem ser construída, uma vez por tentativa. Isto não é arrumação. É o único ponto do sistema onde se conhecem ao mesmo tempo as quatro coisas que ele tem de ligar: que fonte está do outro lado desta sessão, que tentativa é esta, a que revisão da regra pertence e quando expira.

A consequência é que não pode estar em mais lado nenhum, e isso verifica-se em vez de se afirmar. Um teste planta uma palavra-passe conhecida, percorre o caminho real e procura-a na carga do comando, nas linhas de eventos, na configuração de verificação guardada, no barramento de mensagens, nos bytes crus do protocolo tanto em UTF-8 como em base64, e no registo da gateway. Exige zero ocorrências nos seis. Uma lista de afirmações sobre onde um segredo não chega não vale nada; uma busca vale alguma coisa.

O texto em claro existe o tempo de uma expressão. Uma função produ-lo, a seguinte consome-o, e depois já não há referência nenhuma. O JavaScript não sabe limpar um buffer, portanto a única opção é nunca guardar um. Do outro lado, em Rust, vai para um tipo que se põe a zero ao ser libertado.

A construção

Um par de chaves X25519 efémero por tentativa, um Diffie-Hellman contra a chave pública de exatamente uma fonte, HKDF-SHA256 para derivar a chave de cifra e AES-256-GCM sobre o texto em claro. Pelo fio vão a chave pública efémera mais um número de uso único, o criptograma e a etiqueta.

As duas chaves públicas entram no sal da HKDF, para que a chave derivada fique ligada a esse par e não a um segredo partilhado que em princípio poderia nascer de outro par. A mesma ideia do plano de chaves de um esquema híbrido comum, com a simplificação que aqui é honesta: há uma chave efémera nova por tentativa, portanto nenhuma chave é usada duas vezes.

Quatro campos entram nos dados autenticados adicionais:

CampoO que impede
Identificador da tentativaQue a mesma fonte reutilize um envelope para uma medição posterior
Identificador de revisãoQue um envelope seja colado debaixo de outra verificação
PrazoQue um envelope tirado de uma captura ou de um despejo sirva mais tarde
FinalidadeQue uma credencial de PostgreSQL seja dada a uma verificação de Redis

A identidade da fonte não consta dessa lista porque não precisa: o selo é feito contra uma única chave pública, portanto outra fonte não o abre de todo.

Prefixos de comprimento, porque um separador é ambíguo

Os dados adicionais são construídos com o comprimento de cada campo à frente em vez de um separador entre campos. Isto lê-se como preciosismo até se escrever a alternativa.

Com a:b como formato, o par ("x", "y:z") e o par ("x:y", "z") produzem bytes idênticos. A ligação que estes dados devem estabelecer deixa então de distinguir duas situações diferentes, e quem controle parte de um campo pode deslocar a fronteira. Quatro campos com quatro bytes de comprimento à frente não têm esse problema. Ambos os lados os constroem byte a byte da mesma maneira, com um vetor de teste partilhado, de modo que uma divergência é um teste vermelho e não um envelope que já ninguém abre.

Porque não a cifra que já usamos

O cofre de credenciais do agente usa XChaCha20-Poly1305, e a mesma construção aqui daria uma só resposta à pergunta de como ciframos um segredo em vez de duas. Medimos, e não dá:

text
node v24.20.0   crypto.getCiphers() contém "chacha20-poly1305"
bun  1.4.0      crypto.getCiphers() devolve 28 nomes, nenhum chacha
                createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"

A produção corre sobre Node, portanto ali funcionaria. Mas a bancada de testes deste repositório é o bun, e uma cifra autenticada que a bancada não consegue executar é uma cifra cuja ida e volta nunca é demonstrada. O único controlo sobre o envelope viveria então num teste saltado, e esse é precisamente o sabor de medição verde em vazio que fazemos muito esforço por evitar.

A alternativa era escrever à mão a peça em falta. Foi mesmo feito, funcionou contra o vetor de teste publicado, e depois foi retirado. Código criptográfico que só existe para contornar uma bancada de testes não pertence a um repositório de produto.

Portanto AES-256-GCM, a mesma cifra autenticada que já protege este segredo em repouso, presente em ambos os ambientes de execução e já na árvore de dependências do agente. O número de uso único de 96 bits por causa do qual se costuma escolher XChaCha não é aqui um problema, porque a razão de ser do XChaCha é poder tirar esses números ao acaso sob uma chave de vida longa. Não há nenhuma chave de vida longa. Cada envelope tem uma efémera nova, portanto cada chave serve exatamente uma vez e uma repetição não seria uma segunda mensagem sob a mesma chave.

Onde a fonte guarda a sua metade

A chave privada fica ao lado da identidade do agente ou da sonda, num ficheiro de trinta e dois bytes crus com modo 0600, junto à chave privada do certificado de cliente. Não no cofre de credenciais, e as três razões merecem ser escritas porque foram a decisão verdadeira.

Revogar apaga-a de graça: a revogação já elimina essa pasta, e uma chave guardada noutro sítio precisaria de uma regra de limpeza própria, que a primeira ronda a esquecê-la deixaria numa máquina revogada. Pertence a uma inscrição, e a plataforma guarda a metade pública ao lado do certificado, portanto uma inscrição nova deve produzir um par novo, o que acontece porque a pasta está então vazia. E o cofre nem sempre está disponível: abre-se com material derivado do ficheiro de licença, e uma sonda de monitorização não tem licença e portanto não tem cofre. Pôr ali a chave obrigaria a um segundo mecanismo para sondas, e então haveria duas respostas para uma pergunta.

Também não é mantida em memória, e é propositado, por mais tentador que seja para uma sonda que nada armazena. A plataforma guarda a metade pública, portanto depois de um reinício deixaria de corresponder e todos os envelopes ficariam fechados até alguém voltar a inscrever a sonda. Um ponto de monitorização que depois de um reinício deixa em silêncio de fazer verificações autenticadas é exatamente o tipo de avaria que ninguém repara.

Nunca um recurso alternativo

Se uma fonte não tem chave, a verificação não corre. Comunica um erro de configuração que nomeia o intermediário como indisponível, e esse é todo o comportamento. Não há recurso a texto em claro e, mais importante, não há um estado de queda enganador para o alvo, porque uma verificação que não se consegue medir nunca deve parecer um serviço avariado. Isso é um alerta que manda alguém à máquina errada às três da manhã.

O que registaríamos

  1. Uma linha de auditoria não é um canal de entrega. O que lá se escreve fica todo o período de retenção, em cada cópia de segurança.
  2. Não acrescente o campo. Uma mensagem de protocolo sem campo de palavra-passe não pode deixar escapar nenhuma por um caminho de código que alguém se esqueça de rever.
  3. Sele onde as ligações são conhecidas. Se quem cria um trabalho ainda não sabe quem o vai executar, o segredo não pode nascer com o trabalho.
  4. Demonstre a ausência procurando, não afirmando. Plante um valor e cace-o em cada serialização, base64 incluída.
  5. Prefixe o comprimento nos seus dados autenticados. Um separador faz com que dois pares diferentes produzam os mesmos bytes.
  6. Escolha a primitiva que a sua bancada de testes consegue executar. Uma construção mais forte que nunca é exercitada é na prática mais fraca do que uma boa que o é.
  7. Uma chave que é uma identidade pertence à identidade. Assim a revogação limpa-a e uma nova inscrição substitui-a, sem uma regra de que alguém se tenha de lembrar.

Para ser claro sobre o que isto é: a monitorização está atrás de um interruptor de lançamento desligado por omissão, portanto nada disto está hoje à venda. Duas coisas não estão demonstradas. A troca de autenticação baseada em palavra-passe só foi medida contra a implementação de referência da biblioteca do protocolo e não contra um servidor real. E o binário da sonda escrito em Rust não foi exercitado pelo fio nesta ronda.

Do trabalho de plataforma da Provibr. Porque é que uma sonda trata como não fidedigna cada mensagem que recebe está em uma sonda que não se deixa convencer de nada, e porque é que as credenciais dos painéis do próprio cliente nunca chegam até nós está em onde devem viver as credenciais de painel.

Comece com um host e um agente

Crie uma conta, instale o agente com um único comando e ligue o seu primeiro host Proxmox. O período experimental dura 14 dias e não pede quaisquer dados de pagamento.