rbacuri.dpdns.org — status: online

O SIEM que vigiava o honeypot era a coisa mais exposta do servidor

Meu laboratório tem um honeypot recebendo tentativas reais de login de bots da internet, e um Wazuh (SIEM) analisando esses eventos. A ideia é simples: o honeypot é a isca exposta de propósito, e o SIEM é a parte protegida que observa. Numa atualização de rotina do Wazuh, descobri que essa divisão estava invertida — o componente mais exposto do servidor não era o honeypot. Era o próprio SIEM.

A atualização que puxou o fio

Tudo começou com uma atualização de pacote no servidor: o agente do Wazuh tinha versão nova (4.14.8), mas o servidor do Wazuh, que roda em containers, ainda estava na 4.14.7. O Wazuh não suporta agente mais novo que o servidor, então segurei o pacote do agente (apt-mark hold) e fui atualizar o servidor primeiro.

Antes de mexer, comparei o repositório oficial de deploy entre as duas versões:

git diff v4.14.7 v4.14.8 -- single-node/

A única mudança eram as três tags de imagem. Nenhum arquivo de configuração tinha mudado — então a atualização era trocar as tags, com backup antes. Fiz o backup dos volumes pequenos (inclusive o que guarda as chaves dos agentes), baixei as imagens novas com o Wazuh ainda no ar e recriei os containers.

Os agentes reconectaram. Mas o dashboard começou a responder 502 pelo proxy reverso.

O primeiro sinal: uma ligação feita à mão

O dashboard estava funcionando por dentro — o problema era rede. Meses antes, eu tinha ligado o container do dashboard à rede do proxy reverso com um docker network connect manual. Funcionou desde então, mas essa ligação não estava escrita em lugar nenhum. Ao recriar o container, ela simplesmente sumiu.

A correção imediata foi religar. A correção de verdade foi declarar a rede no próprio docker-compose.yml, para a próxima atualização não repetir o problema. E foi editando esse arquivo que vi o resto.

O que estava aberto

Lendo o compose com atenção, encontrei senhas de usuários internos definidas como variáveis — e algumas pareciam familiares. Em vez de tentar logar com elas, escrevi uma checagem que comparava cada valor configurado com os valores padrão publicados na documentação oficial, sem exibir nenhum deles. Depois testei as portas de fora do servidor, a partir de outra rede, sem usar credencial nenhuma:

ServiçoPortaAcessível pela internet?Senha
API do Wazuh (usuário com papel de administrador)55000simpadrão da documentação
Indexer — usuário do dashboard9200simpadrão da documentação
Indexer — 4 usuários de demonstração (incluindo um que lê todos os índices)9200simpadrão da documentação
Indexer — usuário admin9200simalterada
Cadastro de novos agentes1515simsem senha

O ponto mais grave é a primeira linha. A API do Wazuh é a interface de administração do SIEM: com o usuário que o dashboard usa, dá para ler todos os alertas, alterar a configuração do servidor, mexer na configuração centralizada dos agentes e disparar respostas ativas nas máquinas monitoradas. E a senha dele estava num exemplo público da documentação. Uma senha que está na documentação, na prática, não é senha.

Eu tinha trocado a senha do admin na instalação — e achei que isso cobria o resto. Não cobria: os outros usuários internos, que eu nunca uso diretamente, continuavam com os valores de fábrica.

Por que o UFW não pegou

O servidor tem UFW ativo, com política de bloquear entrada. Mesmo assim, essas portas respondiam da internet.

O motivo é como o Docker publica portas: ele cria as próprias regras de iptables para redirecionar o tráfego para o container, e esse tráfego redirecionado passa pela cadeia FORWARD, não pela INPUT onde as regras do UFW atuam. Uma porta publicada com "55000:55000" no compose fica aberta para o mundo, independente do que o UFW diz.

Já escrevi aqui sobre um firewall que reportava “ativo” mas não filtrava nada. Este é um primo desse caso: o UFW estava funcionando de verdade — só não estava no caminho desse tráfego.

As correções, em ordem de impacto

1. Tirar as portas da internet. O dashboard fala com a API e com o indexer pela rede interna do Docker, não pelas portas publicadas no host. Então bastava publicá-las só no loopback:

ports:
  - "127.0.0.1:55000:55000"

Verifiquei de fora que as portas fecharam, e de dentro do servidor que continuavam respondendo. Isso fechou a exposição em um minuto, antes de mexer em qualquer senha.

2. Trocar a senha do usuário da API. O script de inicialização do container reaplica a senha definida no compose toda vez que ele sobe, então bastava trocar o valor nos lugares certos e recriar. Gerei a senha direto no servidor — ela nunca apareceu no terminal nem saiu da máquina. A verificação teve três partes: a senha nova autentica, a senha padrão é recusada, e o dashboard, de dentro do próprio container, continua autenticando na API.

3. Trocar as senhas do indexer. Essa exigiu mais cuidado, porque a ferramenta de segurança do indexer (securityadmin) reenvia a configuração a partir de arquivos — e pode sobrescrever usuários criados pela interface que não estejam no arquivo. Por isso, antes:

  • fiz backup da configuração de segurança como ela estava ativa no indexer (securityadmin -backup);
  • comparei a lista de usuários e os hashes do backup com os do arquivo — idênticos, então reenviar não apagaria nada;
  • reenviei só a lista de usuários (-t internalusers), sem tocar em papéis e permissões.

O usuário do dashboard recebeu uma senha nova, e os quatro de demonstração receberam senhas aleatórias que ninguém conhece — na prática, desativados, sem apagar nada. Testei cada senha antiga: todas recusadas.

4. Exigir senha no cadastro de agentes. Liguei use_password na configuração do servidor e criei o arquivo de senha dentro do container. Aqui veio um tropeço instrutivo: depois de reiniciar, o log continuava dizendo No password required. O container só copia a configuração montada quando é criado, não quando reinicia. Recriado, o log passou a dizer Using password specified on file. A verificação final foi tentar cadastrar um agente sem senha, a partir da internet:

ERROR: Invalid password. Unable to add agent

Os agentes já cadastrados não foram afetados — eles usam a porta 1514, com as chaves que já têm.

Depois de fechar: alguém usou?

Corrigir não encerra o caso. A pergunta seguinte é se a falha foi usada enquanto existia — e a resposta não pode ser “acho que não”.

A API do Wazuh registra cada requisição com o usuário e o endereço de origem, e os logs cobriam todo o período desde a instalação. Analisei as cerca de 88 mil requisições, separando o tráfego interno (o próprio dashboard) do que veio de fora. O resultado:

  • 189 endereços externos testaram a porta da API sem conseguir entrar.
  • 12 endereços externos entraram com a senha padrão. O primeiro login aconteceu no mesmo dia em que o Wazuh foi instalado; o último, horas antes da correção.
  • Todos fizeram exatamente a mesma coisa: login e uma única leitura da página inicial da API — que só informa a versão — em um ou dois segundos, e foram embora. Todos vindos de faixas de datacenter de nuvem. É a assinatura de robôs que varrem a internet testando credenciais padrão e anotam onde funcionou.
  • Nenhuma requisição que altera algo — criar usuário, mudar configuração, agir sobre os agentes — aparece no período inteiro, de origem nenhuma.

No indexer, a resposta foi mais desconfortável: não dava para saber. A auditoria estava marcada como enabled: true, mas nenhum destino estava configurado — ou seja, nada era gravado. O log comum só registra logins que falharam, e o que existia se perdeu quando o container foi recriado na atualização. O que deu para verificar foram sinais de dano: nenhum índice estranho (bots que atacam OpenSearch exposto costumam apagar dados e deixar um índice de “resgate”), os índices diários de alertas contínuos, e os usuários expostos sem permissão para apagar alertas. Mas leitura dos dados, se aconteceu, não deixou rastro.

Por isso, a última correção foi fazer a auditoria gravar de verdade: logins bem-sucedidos e falhos, com usuário, origem e horário, em índices dentro do próprio indexer, com retenção de 90 dias. Ligar tudo sem filtro gerava cerca de 400 MB por dia — quase só o dashboard consultando o indexer em rotina —, então filtrei essas consultas de rotina, o que reduziu o volume em mais de 90% sem perder o que importa.

A lição

  • A ferramenta de defesa também é superfície de ataque — e das mais valiosas. Um SIEM concentra todos os logs e, pela API, tem alcance sobre as máquinas monitoradas. Eu protegia o honeypot com cuidado e tratava o Wazuh como “a parte segura”, só por estar do lado de quem defende.
  • “Tem senha” não é a pergunta certa. A pergunta é: qual senha? Valores padrão de documentação equivalem a nenhuma senha, e trocar só a do usuário que você usa no dia a dia deixa os outros para trás.
  • Verifique de fora, e verifique que o antigo parou de funcionar. Cada correção aqui foi confirmada de fora do servidor, e cada troca de senha foi validada pela senha antiga sendo recusada — não só pela nova funcionando. Uma senha nova que funciona prova que você configurou algo; a antiga sendo recusada prova que você fechou o buraco.
  • Docker e UFW não conversam. Se você publica portas com Docker num servidor com UFW, o UFW não está protegendo essas portas. Publique só no 127.0.0.1 o que não precisa ser público.
  • A janela de exposição é medida em horas, não em meses. O primeiro login com a senha padrão veio no mesmo dia da instalação. Não existe “vou trocar as senhas depois”: robôs varrem a internet continuamente atrás exatamente disso.
  • “Auditoria ligada” não é auditoria. Um log que não grava em lugar nenhum dá a mesma resposta que log nenhum: “não sei”. Vale testar o registro de ponta a ponta — gerar um login falho de propósito e ver se ele aparece — antes de precisar dele.