Monitorando ambientes conteinerizados com osquery – IBM Developer

Monitorando ambientes conteinerizados com osquery

Os contêineres se tornaram mais prolíficos devido à sua mobilidade e à sua eficiência de armazenamento. Existem muitas soluções para monitorar sistemas tradicionais, mas não existe uma em vigor para monitorar ambientes conteinerizados.

Minha equipe, referida como “KuberNets”, estabeleceu o uso de osquery para ampliar a visibilidade nos contêineres. Nós somos um grupo de estagiários que foram incumbidos de pesquisar soluções disponíveis que aprimorariam a visibilidade em ambientes conteinerizados. O objetivo final era aprimorar recursos de monitoramento de contêiner para uma equipe de segurança, fornecendo uma solução que estaria pronta para implementação em nossa partida.

De acordo com nosso conhecimento, não havia nenhuma solução de monitoramento de contêiner existente que satisfizesse nossas necessidades de detectar ameaças. Alguns exemplos básicos de monitoramento incluem saber quando um processo está ativo, se um arquivo foi excluído e quais conexões de rede estão se comunicando com um contêiner. O mais importante é saber em qual contêiner a atividade se originou. Surpreendentemente, apesar da ampla adoção do Kubernetes e da presença de uma grande comunidade de implementação na nuvem de software livre, não existe uma solução que aborde totalmente esse problema. O que está acontecendo realmente nos contêineres?

Leia caso você seja um indivíduo de segurança com os mesmos interesses buscando aprimorar sua configuração de monitoramento do Kubernetes. O osquery não é a solução final para monitoramento de contêiner, mas é um avanço, pois agora podemos visualizar os processos de contêiner e a atividade de rede.

Este artigo é baseado no trabalho feito pelos KuberNets, uma equipe de estagiários da IBM que colaborou com o projeto. Os membros da equipe eram John Do, Zane Doleh, Tabor Kvasnicka e Joshua Stuifbergen.

Pré-requisitos

Para tirar o máximo proveito deste artigo, é ideal ter conhecimento de Kubernetes, Docker, contêineres, SQL, Linux e osquery.

Tempo estimado

Leva cerca de 10 minutos para ler este artigo.

Contêineres, Docker, Kubernetes, containerd e o Proc Pseudo Filesystem

Contêineres, Docker, Kubernetes, containerd e o Linux Proc Pseudo Filesystem são os principais componentes de nosso projeto. Com um entendimento básico de suas arquiteturas, nós conseguimos avaliar adequadamente o osquery. É possível ignorar essas seções se você já está familiarizado com os componentes mencionados.

Contêineres

Contêineres são uma unidade padrão de software. Eles permitem a separação entre aplicativo e infraestrutura e adotam totalmente a metodologia “implementar em qualquer lugar”.

Os aplicativos e suas dependências são empacotados juntos nas imagens no momento da criação. Essas imagens contêm tudo o que é necessário para implementar o aplicativo, portanto, elas podem ser implementadas em qualquer sistema operacional ou hardware. Elas são implementadas em contêineres no tempo de execução e o mecanismo usado para implementar o contêiner configura os firewalls e a rede necessária para que os usuários acessem o aplicativo.

Essa arquitetura permite que os desenvolvedores foquem mais em seus aplicativos e menos em detalhes da implementação.

Docker

Docker é uma organização que fornece contêineres no nível corporativo. O mecanismo de Docker é o coração da solução e permite que os desenvolvedores construam, controlem e protejam aplicativos.

O Docker é fornecido com ferramentas de contêiner líderes do setor, incluindo o containerd, uma poderosa interface da linha de comandos, um BuildKit integrado e outros recursos úteis.

Para o projeto de minha equipe rastrear o processo ativo e a atividade de rede, nós usamos principalmente o Docker para criar imagens com o BuildKit, que lê Dockerfiles e acelera a criação de imagens.

Kubernetes

Kubernetes é uma plataforma de software livre, desenvolvida originalmente pela Google, para gerenciar cargas de trabalho e serviços em pods. Pods são as menores unidades de software no Kubernetes e consistem em um ou mais contêineres. O Kubernetes orquestra a infraestrutura de rede e armazenamento para ajudar a carga de trabalho. A plataforma é um ambiente de gerenciamento central para contêineres que desenvolvem um ecossistema de componentes e ferramentas para implementar, escalar e gerenciar aplicativos.

Embora essa arquitetura seja conveniente, ela tem um custo. Se invasores ganharem o controle sobre o Kubernetes, eles terão controle sobre os contêineres dentro dele. Eles poderão excluir ou mudar os pods existentes, criar novos pods com intento malicioso ou executar essencialmente qualquer ação maliciosa.

Nosso projeto não foca na proteção desses sistemas. No entanto, a visibilidade nesses sistemas pode ajudar na identificação de ameaças potenciais, de modo que elas possam ser minimizadas.

Containerd

Containerd é o padrão de mercado atual para tempos de execução de contêiner. Esse projeto da Cloud Native Computing Foundation é usado pelo Docker e pelo Kubernetes.

Responsável por gerenciar os contêineres, o containerd expõe uma API para esse serviço. Ele não foi projetado para uso com uma interface da linha de comandos. Anteriormente, o Kubernetes usava o Docker por padrão para gerenciar contêineres. No entanto, como o Docker usa o containerd no plano de fundo, o Kubernetes agora usa o containerd diretamente por padrão.

Proc Pseudo Filesystem

O Linux e outros sistemas operacionais baseados em Unix adotam a metodologia “tudo é um arquivo”. Essa abordagem é importante porque qualquer informação sobre um sistema pode ser localizada em um arquivo no sistema. Isso é útil ao procurar informações sobre processos em execução em um sistema. Todas essas informações podem ser localizadas no proc pseudo filesystem, ou procfs, geralmente montado em /proc.

É possível localizar uma variedade de informações sob o procfs, incluindo as informações de CPU, as informações de memória e as estatísticas de processo. Ele também contém informações sobre todos os processos em execução no sistema. Cada pasta neste diretório contém um ID do processo no qual é possível obter informações mais específicas sobre esse processo, como o nome do processo, o caminho para o executável e os IDs do usuário e do grupo. Essas pastas contêm informações de rede para esses processos. Portanto, o acesso a esse sistema de arquivos é essencial ao monitorar sistemas baseados em Linux.

Uma introdução ao osquery

Osquery é uma solução de monitoramento de sistema desenvolvida pelo Facebook que se tornou software livre em 2014. Ele estrutura o sistema operacional para um banco de dados relacional que pode ser consultado com SQL. O osquery está disponível no Mac OS X, no Windows e em várias distribuições populares do Linux.

Ao mudar a maneira como você procura informações, o osquery torna sua rotina de monitoramento do sistema operacional mais fácil. A consulta de tabelas predefinidas com o osquery torna mais fácil verificar o desempenho e o estado de sua máquina, como visualizar o processo em execução ou os eventos de hardware. As consultas executadas do contêiner do osquery fornecem informações sobre o processo em execução e o tráfego de rede dos outros contêineres no nó. Essas informações oferecem insight sobre processos irregulares ou conexões incomuns.

As seções a seguir mostram como nossa equipe chegou a uma solução de monitoramento de contêiner osquery operacional. Mas primeiro, familiarize-se com a maneira como o osquery funciona.

A arquitetura do osquery

Conforme mostrado no diagrama a seguir, o osquery possui dois tipos de tabelas: tabelas virtuais e tabelas de eventos.

alt

As tabelas virtuais são geradas quando as consultas são feitas. Essas tabelas são formadas com base em syscalls, APIs do S.O. e arquivos de configuração. Nem todos os tipos de dados funcionam sob esse paradigma. Por exemplo, monitorar o sistema de arquivos inteiro seria inviável. Gerar tabelas toda vez que a consulta fosse executada incorreria em muito gasto adicional. Em vez disso, o osquery pode usar o sistema de eventos para armazenar dados e recuperar as informações solicitadas com base nas tabelas existentes.

O sistema de eventos registra dados à medida que os eventos ocorrem e ele os armazena na instância do RocksDB. Quando uma consulta é feita em uma das tabelas de eventos, as tabelas virtuais são geradas a partir dos dados do RocksDB em vez de a partir dos dados coletados do sistema no momento da consulta. Essa abordagem permite o monitoramento mais intensivo — e o monitoramento quase em tempo real. A tabela process_events usa o Linux Audit System para receber esses eventos do kernel e os registra no RocksDB. Quando uma consulta é feita em process_events, as tabelas são geradas desses dados.

Após as tabelas serem geradas, elas são processadas pelo SQLite Engine, permitindo os recursos usuais de um banco de dados relacional. Existem algumas otimizações em vigor para coletar dados. Em algumas consultas de tabela usando uma cláusula SQL WHERE, o osquery manipula parte da filtragem para que ele não colete dados desnecessários. Essa abordagem é mais eficiente do que coletar todos os dados e o mecanismo SQLite descartar a maioria deles.

Nossa implementação

É possível executar o osquery de dois modos diferentes: como um shell interativo (osqueryi) e como um daemon (osqueryd). Os termos osquery daemon e osqueryd são usados indistintamente. O shell interativo permite consultar manualmente o sistema. Esse shell é útil para testar consultas ou reunir informações rápidas em um sistema. No entanto, para os propósitos de nosso projeto, nós usamos o daemon do osquery. É possível configurá-lo para executar consultas periodicamente. Nós fizemos isso incluindo novas consultas no arquivo de configuração.

É possível configurar o osquery para registrar as diferenças nos resultados da consulta em vez dos resultados completos da consulta. Os resultados são alimentados no mecanismo diff, que compara os resultados atuais da consulta com os resultados anteriores e determina quais linhas foram incluídas ou removidas. Essas diferenças (em vez dos resultados inteiros) são, então, enviadas. Essa abordagem fornece melhor escalabilidade quando você está lidando com vários sistemas encaminhando dados. Em nosso projeto, nós usamos osqueryd, que usa o mecanismo diff por padrão.

Nosso principal desafio com o osquery foi que nós queríamos usá-lo de uma maneira para a qual ele não foi criado. Ele pode monitorar apenas seu sistema host. De maneira ideal, nós deveríamos implementar o osquery como um DaemonSet que pode monitorar todos os contêineres em cada nó. Mas, para que essa implementação funcionasse, nós precisaríamos que o osquery enxergasse fora do contêiner.

Um bloqueador adicional veio do ambiente Kubernetes que estávamos usando. O osquery possui tabelas integradas para visualizar o Docker, mas o Kubernetes não usa mais o Docker por padrão. Em vez disso, ele abstraiu o Docker e agora usa o containerd como o ambiente de tempo de execução de contêiner. Nós usamos a implementação de serviço do IBM Cloud Kubernetes para nossa avaliação do osquery, que também adotou o containerd.

Nós fizemos a pesquisa necessária e descobrimos como estender o osquery para obter mais visibilidade. Como os sistemas operacionais do host dos nós são baseados em Linux, eles possuem o Proc Pseudo Filesystem. Além disso, o estudo do código-fonte do osquery revelou que eles estão simplesmente enumerando /proc para coletar informações do processo. Nós usamos essas informações — combinadas com o fato de que todos os contêineres são simplesmente processos containerd-shim que enumeram o procfs do host.

Nós conseguimos obter as informações sobre os processos de contêiner que estão no nó atual. Nós montamos o proc pseudo filesystem do nó no contêiner em que o osquery foi implementado como /host/proc (conforme mostrado na imagem anterior). Em seguida, nós gravamos uma extensão de osquery em Go para ser interpretada como /host/proc e retornamos as informações do processo do nó em uma nova tabela host_processes, replicando efetivamente a tabela de processos. Nós criamos nossa extensão usando o repositório osquery-go (criado por Kolide) como um modelo.

O código a seguir mostra a nova tabela host_processes extraindo dados de /host/proc.

alt

A tabela host_processes imita essas informações obtidas da tabela de processos. A captura de tela a seguir é um exemplo de algumas das informações recuperadas.

alt

Nós pudemos ver os processos, então a próxima etapa era monitorar a atividade de rede.

Como essas informações também estão no procfs, deve ser trivial incluir uma tabela adicional para essas informações. No entanto, a biblioteca que nós usamos para a extensão não suportou nativamente a enumeração das informações de rede. Essa situação nos levou à ideia de substituir todas as instâncias de /proc por /host/proc no código base do osquery e criar na origem. Essa mudança nos deu os resultados que estávamos buscando.

Outro problema surgiu: após os processos ficarem visíveis para o osquery, os processos de contêiner eram vistos, mas não havia nenhuma maneira de ver a quais contêineres eles pertenciam. Portanto, além de monitorar os processos, nós estendemos o osquery para ver as informações de pod e de contêiner usando a API do Kubernetes. Então, nós pudemos corresponder o ID do contêiner fornecido pelos processos a um nome do contêiner correspondendo as informações entre duas consultas separadas.

A amostra de código a seguir mostra as informações que a tabela kubernetes_pods preenche com a API do Kubernetes:

func KubernetesPodsColumns() []table.ColumnDefinition {
    return []table.ColumnDefinition{
        table.TextColumn("uid"),
        table.TextColumn("name"),
        table.TextColumn("namespace"),
        table.IntegerColumn("priority"),
        table.TextColumn("node"),
        table.TextColumn("start_time"),
        table.TextColumn("labels"),
        table.TextColumn("annotations"),
        table.TextColumn("status"),
        table.TextColumn("ip"),
        table.TextColumn("controlled_by"),
        table.TextColumn("owner_uid"),
        table.TextColumn("qos_class"),
    }
}

A amostra de código a seguir mostra as informações que a tabela kubernetes_containers preenche com a API do Kubernetes:

func KubernetesContainersColumns() []table.ColumnDefinition {
    return []table.ColumnDefinition{
        table.TextColumn("id"),
        table.TextColumn("name"),
        table.TextColumn("pod_uid"),
        table.TextColumn("pod_name"),
        table.TextColumn("namespace"),
        table.TextColumn("image"),
        table.TextColumn("image_id"),
        table.TextColumn("state"),
        table.IntegerColumn("ready"),
        table.TextColumn("started_at"),
        // table.TextColumn("env_variables"),
    }
}

Nosso próximo objetivo era criar uma consulta que combinasse essas informações e pudesse ser visualizada em um log. Nós ainda estamos trabalhando nesse objetivo.

Uma nota sobre o Dockerfile e a configuração de extensão: nós seguimos a documentação do repositório osquery-go para carregar automaticamente a extensão com osqueryd. Nós criamos um arquivoextension.load com o caminho para a extensão e renomeamos a extensão para incluir .ext. As instruções de Dockerfile colocaram o arquivo de configuração, o arquivo de carregamento automático de extensão, o binário osqueryd e a extensão customizada nos diretórios padrão que o osqueryd procura ao inicializar, conforme mostrado na captura de tela a seguir. A extensão e o osqueryd exigiam privilégios de execução para serem executados sem erro.

Captura de tela

Foram necessárias duas sinalizações no arquivo de configuração para carregar a extensão. Nós incluímos uma sinalização para ativar extensões e uma sinalização para identificar onde a extensão de carregamento automático está localizada (conforme mostrado na captura de tela a seguir). Há muitas outras sinalizações importantes descritas na documentação do osquery.

Captura de tela

É possível configurar o osquery para enviar logs de várias maneiras, para arquivos ou para um terminal. Nós configuramos o osquery para enviar para arquivos, instalando um módulo do Filebeat e, em seguida, encaminhando os dados para o Elasticsearch para que pudéssemos visualizar os logs no Kibana.

Demonstração

Qualquer conexão de rede incomum ou processo desconhecido seria considerado suspeito em um contêiner implementado. Esta seção mostra como o osquery captura as ações de um invasor executando um shell de backdoor do Netcat. Esse exemplo simplesmente demonstra que o osquery detecta os processos e a atividade de rede de outro contêiner no mesmo nó.

Como você pode reconhecer na captura de tela a seguir, o comando inicia um listener Netcat. Quando implementado, esse contêiner se torna o destino. O número 4444 indica a porta local na qual ele atende. Quando os invasores se conectam, eles entram em uma sessão do shell bash.

Captura de tela de um comando que inicia um listener Netcat

Você começa implementando um aplicativo Netcat, nc-app, com o comando a seguir:

kubectl create deployment nc-app --image=<docker_image>

Captura de tela do comando create deployment

Para verificar se o pod está em execução, use o comando a seguir:

kubectl get pods

Captura de tela do comando get pods

Você expõe o serviço do Netcat para que uma conexão externa ao cluster possa atingir o contêiner e abre a porta 4444 com o comando a seguir:

kubectl expose deployment nc-app --type=LoadBalancer --name=nc-service --port 4444

Um IP externo também é criado.

Captura de tela do comando expose deployment

Para verificar se o serviço está ativo, use o comando a seguir:

kubectl get services nc-service

Em um ambiente externo, execute o comando do cliente Netcat a seguir usando o IP externo do serviço:

nc 46.102.66.23 4444

Captura de tela do comando do cliente Netcat com IP externo

É possível ver a saída da consulta, host_processes_query:

Captura de tela da saída de host_process_query

Ela mostra a linha de comandos do contêiner nc-app, /bin/sh -c nc -lvp 4444 -e /bin/bash, e o nome dos processos sh. Essas informações são oriundas da tabela customizada host_processes que lê o contêiner do osquery. A significância dessas informações é que um contêiner agora pode relatar sobre as ações de outros contêineres.

Aqui está o resultado da consulta de kubernetes_container:

Captura de tela do resultado da consulta de kubernetes_container

Essas informações incluem o nome, o nome do pod, o namespace, a imagem, o estado e quando foi iniciado.

E há o resultado da consulta de kubernetes_pod:

Captura de tela do resultado da consulta de kubernetes_pod

Essas informações incluem o UID, o nome, o namespace, o nó, o status, o horário de início e o IP do pod.

Observe que o IP 172.30.71.203 a seguir corresponde ao IP do pod do log kubernetes_pod:

Captura de tela do IP 172.30.71.203 correspondente ao IP do pod

Resumo

Nós tínhamos um objetivo de obter mais visibilidade nos contêineres. O osquery geralmente é implementado em um sistema host e essa implementação cria uma restrição com relação ao que você pode ver dentro dos contêineres. Com as nossas mudanças, agora podemos ver a atividade de rede que é útil para detectar conexões anômalas de um contêiner infectado para outro. Também podemos ver processos ativos e os contêineres de onde eles vieram.

Nossas modificações no osquery nos ajudaram a descobrir o que está realmente acontecendo nos contêineres? Sim, mas não completamente.

Nós expandimos nossa visibilidade nos contêineres, mas há mais a ser feito. Um recurso importante do osquery é o sistema de eventos, mas essas tabelas usam o Linux Audit System, que depende da existência de acesso direto ao sistema. Portanto, nós não conseguimos usar o sistema de eventos para permitir o monitoramento quase em tempo real. Para monitorar um Sistema Linux da mesma maneira que o Linux Audit System, mas externamente, nós precisaríamos que um novo sistema fosse desenvolvido, já que ele ainda não existe. Então, o osquery poderia ser estendido para usar esse sistema.

Se você achou o osquery atraente, experimente-o e explore mais os seus recursos. Se você não está familiarizado com o osquery, eu recomendo instalá-lo para determinar se ele é útil para monitorar seu ambiente de nuvem. Existe uma comunidade ativa do Slack, osquery.slack.com. Talvez você também queira conhecer a Uptycs, uma empresa que aplica aprimoramentos de proprietário ao osquery.

Existem muitas outras soluções de monitoramento de contêiner disponíveis e você pode desejar considerar uma abordagem híbrida para configurar seu monitoramento. Como vimos com o osquery, embora ele não tenha sido criado para monitorar contêineres com o Docker abstraído, nós encontramos uma maneira de trabalhar com esse design. Nosso exemplo provavelmente pode ser replicado em outro lugar com soluções de monitoramento tradicionais.