Você executa seus containers como raiz ou como um usuário comum? É uma pergunta aparentemente simples. Talvez você tenha a tentação de responder rápido demais. O modelo de ameaça está realmente claro para você? Suspeito que não. Esta publicação pretende ajudar a esclarecer.

Antes de responder à pergunta acima, você precisa determinar se estamos falando sobre a plataforma de containers (Podman, Docker, CRI-O, containerd etc.), o processo dentro do container (apache, postgresql, mysql etc) ou o ID do processo para a qual o containers foi mapeado (os três podem ser diferentes). À primeira vista, isso pode não parecer óbvio. A plataforma de containers ou seu subprocesso nos containers podem ser executados como praticamente qualquer usuário.

Containers illustration Quer aprender mais sobre a Universal Base Image (UBI) da Red Hat?

Introdução à raiz

Com a chegada do Podman, os containers sem raiz se tornaram uma possibilidade real. Como o Podman cria containers como subprocessos diretos de si mesmo, é fácil demonstrar que há quatro opções possíveis a serem consideradas. Mas, qual é o conceito de sem raiz? Para entender isso, você precisa entender o que é raiz dentro de um container. Para entender a raiz dentro de um container, é necessário entender a raiz fora do container. Um pouco confuso, hein?

A tabela a seguir mostra a raiz dentro e fora do container (graças a Vincent Batts, que cristalizou esses conceitos na DevConf.us 2019). Com esse framework, você pode começar a entender o conceito de sem raiz:

 

Table 1: Root inside and outside the container showing how users appear to the system

Destaquemos algumas coisas interessantes sobre o exemplo acima. Primeiro, as opções de linha de comando são -i (interativa) -t (terminal) e -u (usuário). Combinadas, essas opções fornecem um terminal interativo dentro do container e especificam que o processo em containers deve ser executado como o usuário sync. Você pode pesquisá-los melhor digitando o comando 'man podman-run'. 

Em segundo lugar, preste muita atenção a $, que implica que nosso shell está sendo executado como um usuário comum, e a #, que implica que nosso shell está sendo executado como raiz. Essas são tradições do Unix que ajudarão a explicar a raiz dentro e fora do container. 

No exemplo acima, o Podman está, por definição, fora do container e é executado como raiz ou como um usuário comum (fatherlinux), enquanto dentro do container o bash é executado como raiz ou um usuário comum (sync). Os usuários no arquivo /etc/passwd no Host de containers são usados para o Podman, enquanto os usuários no arquivo /etc/passwd na Imagem de containers são usados no container em execução. Dito de outra forma, esses dois arquivos passwd implicam que poderíamos ter dois conjuntos de usuários: um conjunto fora e outro dentro do container. É exatamente isso que os namespaces de usuário facilitam.

Namespaces de usuário

Quando o kernel cria um processo em execução dentro de um container, o namespace de usuário mapeia o ID do usuário dos processos em containers para um ID de usuário diferente fora do container. Vamos dar uma olhada:

 

Table 2: Showing user IDs and username inside and outside container

As opções de linha de comando são -i (interativo) -d (desconectado) e -u (usuário) - combinadas. Essas opções executam o container em segundo plano e especificam que o processo em containers deve ser executado como o usuário sync. Como anteriormente, você pode pesquisá-los melhor digitando o comando man podman-run

O Podman também oferece um subcomando muito legal chamado top, que permite mapear o usuário no host do container para o usuário no container em execução. O exemplo acima demonstra que, quando executamos um container como raiz, estamos mapeando o usuário sync (uid 5) no container para o usuário sync (uid 5) no host de container subjacente. Isso significa que, se um processo sair desse container, ele poderá ser executado com os privilégios do usuário sync real. 

Por outro lado, quando executamos o mesmo container como um usuário comum (fatherlinux), ele mapeia o usuário sync (uid 5) no container em execução para uid 100004 no host do container subjacente. Espere. Por que ele não mapeou o usuário sync (uid 5) para o fatherlinux (uid 1000)?

Bem, a resposta curta é porque, com os kernels e pacotes shadow-utils mais recentes (useradd, passwd, etc.), cada novo usuário recebe uma variedade de IDs de usuário à disposição. Tradicionalmente, em um sistema Unix, cada usuário tinha apenas um ID, mas agora é possível ter milhares de UIDs à disposição de cada usuário para uso dentro dos containers. 

Isso é útil quando um container usa vários usuários (como executar o Apache e o MySQL juntos em um único container ou pod ou executar um container sidecar com um agente executado como um usuário diferente). Mas, de onde vem esse mapeamento? De dois arquivos, /etc/subuid e /etc/subgid. As entradas são criadas nesses arquivos quando os usuários são adicionados, por meio do comando usermod, ou manualmente por um administrador de sistemas.

Aprofundamento opcional nos identificadores de usuário

Veja um exemplo de entradas no meu sistema. Com as entradas a seguir, o usuário fatherlinux pode mapear até 65.535 IDs de usuário em containers para IDs de usuário reais no sistema, começando em 100.000. Por padrão, no shadow-utils (useradd, passwd etc.), esse intervalo de IDs do usuário é reservado para apenas um usuário. O comando useradd reservará o próximo intervalo para o próximo usuário. Neste exemplo, esse é o usuário fred, começando no ID 165536:

cat /etc/subuid fatherlinux:100000:65536 fred:165536:65536 cat /etc/subgid fatherlinux:100000:65536 fred:165536:65536

Você também pode ver este mapa de dentro de um container:

 

Table 3: More on users inside and outside the container

Observe que, quando o Podman é executado como raiz, todo o intervalo de IDs de usuário fica disponível no container (4294967295 == 32 bits). No entanto, quando o Podman é executado como fatherlinux, ele mapeia a raiz dentro do container para o usuário fatherlinux (1000) e o usuário sync (uid 5) para um UID no intervalo de 100.000 e 165.535. 

Essa é uma ótima funcionalidade de segurança, porque agora a plataforma de containers e o processo em containers dentro do container em execução estão sendo executados como usuários diferentes e sem privilégios. O conjunto de IDs de usuário de 100.000 a 165.535 não tem privilégios especiais no sistema, nem mesmo como usuário fatherlinux (1000). Isso significa que, se um processo no container for interrompido, ele ficará gravemente restrito no host de containers. 

Outra pergunta que surge é: o sistema pode ficar sem UIDs ao adicionar vários usuários? A resposta curta é sim. No entanto, isso é improvável, já que  os UIDs são representados por um número 32 com 4 bilhões de UIDs. Isso significa que você poderia adicionar até 65.535 usuários a um sistema (4294967295, dividido por 65535). Isso deve ser suficiente para a maioria dos casos de uso.

Vamos nos aprofundar em uma última nuance dos containers sem raiz. O arquivo /etc/subuid é usado para mapear o usuário dentro do container para um usuário fora do container, mas o usuário (fatherlinux no exemplo abaixo) deve ser definido na imagem de container ou o Podman não poderá iniciar o container:

podman run --user fatherlinux -it ubi8 bash

Resultado:

não foi possível encontrar o usuário fatherlinux: nenhuma entrada correspondente no arquivo passwd

Você deve especificar um ID de usuário no container que exista no arquivo /etc/passwd dentro da imagem de container. Este é mais um exemplo de como os containers são intrinsecamente vinculados ao sistema operacional dentro do container e mantêm a separação do sistema operacional dos hosts de containers. Containers são Linux.

Defesa de containers em profundidade

Esse conceito não é fácil de entender com o daemon docker devido ao modelo de servidor cliente. Com o modelo de servidor cliente docker, podemos executar um container como raiz mesmo quando executamos o comando como usuário comum. Isso ocorre porque o daemon docker é executado como raiz e, portanto, tem todos os privilégios de raiz. Isso deve ficar muito mais claro agora. Para demonstrar, execute os seguintes comandos:

 

Table 4: Commands to check user IDs

Para garantir que um usuário executando um container não tenha acesso raiz ao seu host, você precisa executar a plataforma de containers e o processo em containers como um usuário não raiz. Isso fornece várias camadas de segurança entre o serviço (httpd, MySQL etc.) e os recursos privilegiados no sistema operacional. Executar a plataforma de containers como um usuário não raiz é uma camada de defesa, enquanto executar o processo no container como um usuário não raiz diferente oferece mais uma camada de defesa. 

Dan Walsh explora isso mais profundamente neste artigo: Running Rootless Podman as a  non-root User. Em um alto nível, uma plataforma de containers sem raiz como o Podman permite que você execute com sua conta de usuário. Em seguida, dentro do container, você pode usar um conjunto virtual de usuários mapeados para um conjunto de IDs de usuários controlados somente pela sua conta para os processos em containers. 

Agora você deve entender melhor os poderes da raiz dentro e fora do container.


Sobre o autor

At Red Hat, Scott McCarty is Senior Principal Product Manager for RHEL Server, arguably the largest open source software business in the world. Focus areas include cloud, containers, workload expansion, and automation. Working closely with customers, partners, engineering teams, sales, marketing, other product teams, and even in the community, he combines personal experience with customer and partner feedback to enhance and tailor strategic capabilities in Red Hat Enterprise Linux.

McCarty is a social media start-up veteran, an e-commerce old timer, and a weathered government research technologist, with experience across a variety of companies and organizations, from seven person startups to 20,000 employee technology companies. This has culminated in a unique perspective on open source software development, delivery, and maintenance.

Read full bio