Blogue ·
Um segredo errado parece silêncio
Três protocolos de monitorização escritos à mão em vez de importados, e uma falha que o próprio protocolo impede de distinguir de um servidor morto.
Por Provibr
Acrescentar uma verificação de monitorização para um protocolo novo parece uma questão de dependências. Existe um pacote para MongoDB, outro para BSON, outro que fala o protocolo de rede do SQL Server. Três linhas num manifesto, quarenta linhas de cola, pronto.
Os três protocolos foram escritos à mão, e vale a pena registar as razões, porque nenhuma delas é «não foi inventado aqui». Duas são medições, e a terceira é uma promessa já feita aos clientes sobre aquilo que este agente sabe fazer.
O que uma dependência traz de facto
O pacote BSON arrasta quarenta e uma dependências. Isso mede-se e foi medido, mas não foi o número que decidiu.
A verificação lê quatro campos de uma resposta do servidor. Um tipo de documento genérico guarda a resposta inteira em memória: a lista de anfitriões, as etiquetas, o instante da última escrita, a hora do cluster, tudo o que o servidor tenha querido enviar. Nessa altura, «só lemos quatro campos» passa a ser um acordo entre programadores em vez de uma propriedade do código, e os acordos são quebrados pela ronda seguinte que precisa de um quinto campo por uma boa razão.
Existe portanto um leitor que percorre os elementos, devolve os quatro pedidos e salta o resto sem o pousar em lado nenhum. Não consegue reter a resposta, logo a promessa é uma forma e não um hábito. A mesma decisão que a lista de permissões no comando INFO do Redis, e a mesma que manteve um controlador de base de dados completo fora da verificação do PostgreSQL: um controlador traz um construtor de consultas, e então existe, dentro do binário que corre na rede de um cliente, código capaz de fazer uma pergunta arbitrária à sua base de dados.
O pacote para SQL Server é a versão mais afiada. Compila para o nosso alvo de publicação, que era justamente o que se esperava que o travasse. O que ele traz é uma segunda pilha TLS: uma linha fora de manutenção ao lado da atual que já distribuímos. O fornecedor criptográfico de todo o processo é instalado para a linha atual e nada faz pela antiga, pelo que o TLS de uma verificação usaria um fornecedor diferente de qualquer outra ligação deste agente; uma vulnerabilidade nessa linha é uma que distribuímos e não podemos corrigir sem esperar pelo projeto a montante; e a política de certificados, que vive num único módulo precisamente para não divergir, receberia uma segunda implementação contra outra interface.
Saltar é onde um leitor escrito à mão falha
Quem escreve o seu próprio leitor para um formato binário pode cometer um erro que pesa mais do que os outros, e ele não está na análise. Está no salto.
Cada valor BSON tem um byte de tipo e um comprimento que dele decorre. Um leitor que conhece os tipos com que se cruzou por acaso, e que salta um tipo desconhecido em zero bytes, não falha: continua a ler o resto do documento a partir do sítio errado. Tudo o que vem depois fica deslocado. Obtêm-se valores plausíveis que não são os que foram enviados.
A tabela de comprimentos está por isso completa, com uma recusa fechada para tudo o resto. Um byte de tipo desconhecido é um erro de protocolo e nunca um salto.
A mesma família de erros está um nível acima, na leitura dos números. Um servidor MongoDB responde ok: 1 como vírgula flutuante e um code de erro como inteiro de 32 bits, no mesmo documento. Um leitor que só conhece 32 bits perde o primeiro em silêncio, e então cada início de sessão falhado lê-se como bem-sucedido. O leitor aceita um número BSON nas suas três larguras e devolve um inteiro.
O passo de autenticação que tem de poder falhar
A verificação autentica-se com SCRAM-SHA-256 e não com SCRAM-SHA-1, por duas razões que apontam no mesmo sentido.
O mecanismo antigo não introduz a palavra-passe no SCRAM. Introduz um MD5 hexadecimal do nome de utilizador, de uma cadeia fixa e da palavra-passe. Suportá-lo implicaria escrever código que fabrica um MD5 de credenciais, para um mecanismo que nenhum servidor atual precisa de oferecer.
A razão silenciosa é pior. Um servidor que ofereça ambos acabaria, ao recuar, no mais fraco sem que nada ficasse vermelho, e então há uma medição verde por cima de um início de sessão mais fraco do que o cliente julga ter. Se o servidor não oferecer SHA-256 para esse utilizador, a verificação recusa com um código próprio e di-lo.
Escrever SCRAM à mão foi recusado numa ronda anterior para a verificação do PostgreSQL, e a objeção era real: a troca tem uma assinatura do servidor que o cliente deve verificar, e uma implementação que salta esse passo fica verde perante um servidor autêntico e igualmente verde perante um falsificado. Aqui a objeção não foi contornada mas respondida. A verificação da assinatura é uma função própria com um código de erro próprio, para que «o passo falta» não seja o mesmo que «o passo correu bem», e existe um teste que apresenta uma assinatura falsificada e exige que o início de sessão falhe. Esse teste foi provado a vermelho retirando a verificação.
O pacote que a verificação do PostgreSQL usa não servia aqui, e isso está medido e não suposto. A sua implementação de SCRAM escreve a primeira mensagem do cliente com o nome de utilizador vazio, porque o PostgreSQL envia o nome no seu pacote de arranque. O MongoDB não tem esse pacote e lê o nome precisamente desse campo. Introduzi-lo depois mudaria os próprios bytes sobre os quais a assinatura já foi feita, e o servidor rejeitaria a nossa prova. Não há interface para o passar.
Uma falha que o protocolo não deixa nomear
Agora a parte que nenhum simulacro teria encontrado.
O RADIUS é verificado com um pedido Status-Server, que pergunta ao servidor se está vivo e mais nada. A alternativa óbvia, um pedido de acesso com uma conta de teste, foi posta de lado: é uma tentativa real de início de sessão na rede do cliente, com uma conta real que alguém poderia usar, e escreve uma linha no seu registo de autenticação no meio dos seus utilizadores reais. Com um intervalo de um minuto são mil quatrocentas e quarenta linhas por dia que nada dizem sobre o seu serviço, e em muitas instalações uma palavra-passe errada é motivo para bloquear uma conta, pelo que a monitorização provoca exatamente o incidente que devia ter visto chegar.
Depois mediu-se, contra um servidor real, o que faz um segredo partilhado errado.
Nada. Um servidor que não consegue verificar o autenticador de mensagem de um pedido descarta o pacote em silêncio, sem resposta de erro. Está na especificação, e significa que o quadro de falha de um segredo errado não se distingue de uma regra de firewall, de uma porta errada ou de uma máquina morta.
Isso não se corrige num adaptador, porque é uma propriedade do protocolo. O que se pode fazer é deixar de fingir. A mensagem de «sem resposta» nomeia um segredo partilhado errado como causa possível, no texto que o cliente lê, para que um administrador não passe uma tarde na firewall à procura de algo que está num ficheiro de configuração. E se o servidor responder mesmo assim, o autenticador de resposta é verificado, e uma divergência aí é comunicada como facto de autenticação em vez de erro de protocolo, porque nesse arranjo é isso mesmo.
| Situação | O que o cliente vê | Porquê |
|---|---|---|
| Segredo errado, sem resposta | Sem resposta, com o segredo como causa possível | O servidor descartou o pacote; nada voltou para examinar |
| Segredo errado, chega uma resposta | Um segredo errado, como resultado de autenticação | O autenticador de resposta não batia certo |
| Segredo certo, sem resposta | Sem resposta | O mesmo silêncio, e merece as mesmas palavras |
Essa linha do meio é o único caso em que a diferença se vê, e não é o caso comum.
Um servidor que não se pode medir, dito em voz alta
A última medição é a que menos agradou.
O SQL Server em Linux gera um certificado no primeiro arranque. Esse certificado é X.509 versão 1, uma forma sem extensões que há muito deixou de se destinar a TLS, e o analisador de certificados por baixo recusa-a. O modo cifrado falha portanto com uma versão de certificado não suportada.
Vale a pena ser preciso quanto ao motivo por que esse modo ainda verifica alguma coisa. A identidade não é pesada aí: o nome do certificado não é conferido. A assinatura é, porque sem ela não resta um aperto de mão mas uma troca de bytes com quem quer que tenha respondido. Para pesar uma assinatura é preciso ler a chave pública do certificado, e é precisamente esse o passo que recusa.
O resultado é portanto: um SQL Server com o seu próprio certificado gerado não é mensurável com esta verificação. O cliente lê o verdadeiro erro do analisador como detalhe, citado em vez de parafraseado, e a solução é um certificado a sério na configuração do servidor, coisa que um administrador que torna o seu servidor monitorizável faz de qualquer maneira.
Duas coisas ficaram deliberadamente por fazer. A verificação da assinatura não é saltada nesse modo, porque quebraria a promessa exatamente na definição que a maioria escolhe e transformaria uma recusa visível numa fraqueza invisível. E ainda não há um código de erro próprio para este caso, porque a única forma de o reconhecer seria comparar com o texto legível de uma biblioteca, e isso é uma suposição sobre código alheio que se pode deslocar em silêncio. Isso merece uma ronda própria, com prova própria.
Também não há um modo totalmente sem cifra, por uma razão própria deste protocolo: a palavra-passe no pacote de início de sessão é ofuscada com um ou exclusivo fixo e uma troca de quartetos. Isso é uma codificação, não uma cifra. Quem vê os bytes lê a palavra-passe, portanto um modo sem cifra enviaria a palavra-passe de um cliente em texto simples pela sua rede, sob um nome que sugere outra coisa.
O que o canário encontrou
Tudo correu contra servidores reais e não simulacros: catorze verificações contra MongoDB nu e com TLS, um servidor RADIUS real e um SQL Server real. O resultado mais útil é aborrecido. Uma palavra-passe errada é em toda a parte uma falha de autenticação e nunca uma falha de protocolo. É essa a distinção que diz a um cliente se deve olhar para as suas credenciais ou para a sua rede, e uma ronda anterior tinha-a errada na verificação do PostgreSQL: um erro durante o passo de autenticação era traduzido por «isto não é um servidor PostgreSQL».
O que registaríamos
- Conte o que uma dependência retém, não apenas o que custa. Um tipo de documento genérico transforma «lemos quatro campos» num acordo em vez de uma propriedade.
- Um controlador traz um construtor de consultas. Se a promessa é que nenhuma consulta arbitrária pode ser enviada, o código para a enviar não devia existir no binário.
- Duas pilhas TLS num binário são um problema de política, não de tamanho. A que não escolheu é a que não pode corrigir.
- Num leitor escrito à mão, a tabela de saltos é a parte perigosa. Um tipo desconhecido tem de ser um erro; saltá-lo em zero bytes desloca tudo o que vem depois.
- Aceite um número em cada largura que o protocolo permita. Um campo que chega em vírgula flutuante basta para ler cada falha como um sucesso.
- Um recuo para um mecanismo mais fraco tem de ser uma recusa, e não um recuo. Verde por cima de um início de sessão mais fraco é pior do que vermelho.
- Se o protocolo torna uma falha indistinguível, diga-o na mensagem. A redação é o único sítio onde essa distinção ainda cabe.
- Alguns servidores não são mensuráveis, e isso vai para a documentação e não para um desvio. O desvio aqui teria sido deixar de verificar assinaturas.
Para que fique claro o que isto é: as três verificações estão construídas, provadas contra servidores reais e atrás de um interruptor de lançamento. Fica por provar que as três viajem pelo fio até um agente real de cliente, a contabilidade RADIUS para além do pedido de estado, e o SQL Server em arranjos diferentes de uma instância única em Linux.
Do trabalho de plataforma na Provibr. Como a mesma abordagem de canário revelou dois defeitos nas verificações de bases de dados anteriores está em um canário que corre contra servidores reais, e como chegam ao agente as credenciais que estas verificações usam está em um segredo que viaja uma só vez.