Skip to main content


Introdução técnica sobre Docker e Podman


O Docker segue um modelo cliente-servidor tradicional. O componente central é o daemon dockerd, que geralmente roda em segundo plano com privilégios de root no host. Quando o usuário executa comandos pelo CLI do Docker (docker run, docker build etc.), essas requisições são enviadas via socket para o daemon, que então gerencia todas as etapas do ciclo de vida dos containers (criação, execução, monitoramento).


O daemon invoca runtimes de contêiner (como containerd e runc) para isolar processos em namespaces, cgroups, etc. Essa arquitetura centralizada facilita o gerenciamento remoto (um cliente em outra máquina pode controlar o daemon via API REST) e permite que o Docker supervisione containers (reiniciando-os, por exemplo).


Entretanto, o daemon se torna um ponto único de falha, se o dockerd parar ou travar, todos os containers associados podem ser afetados. Além disso, executar um daemon onipresente como root implica que uma vulnerabilidade nesse serviço representa um risco para todo o sistema host.


Já o Podman foi projetado para ser daemonless, ou seja, não possui um serviço persistente equivalente ao dockerd. A CLI do Podman interage diretamente com as bibliotecas de contêiner (libpod), criando processos de contêiner como filhos do processo do usuário que invoca o comando.


Internamente, o Podman utiliza processos auxiliares leves (como o conmon) apenas para monitorar cada container, mas não há um processo centralizado com privilégios elevados mantendo o controle. Cada contêiner é iniciado via chamada direta ao runtime OCI (por exemplo, runc ou crun), o que elimina a necessidade de intermediar as ações através de um daemon pesado.


Consequentemente, se o processo do Podman encerrar, os containers continuam rodando (podem inclusive ser gerenciados via systemd ou outros mecanismos). Essa abordagem descentralizada remove o ponto único de falha e reduz overhead, em certos cenários, o Podman chega a iniciar contêiners um pouco mais rápido, já que evita a etapa de comunicação via socket com um daemon.


Em contrapartida, o Podman não possuía API remota nativa, versões recentes adicionaram um serviço REST opcional (podman system service) para compatibilidade com ferramentas Docker, mas o Docker ainda tem vantagem em maturidade de APIs remotas.



Privilégios de Root


A diferença fundamental entre as arquiteturas é como lidam com privilégio de root no host. No Docker (modo padrão), o daemon roda como root, então qualquer container lançado por ele, por padrão, terá processos rodando efetivamente como root do ponto de vista do kernel host (exceto se medidas adicionais forem tomadas, como uso de user namespaces).


Já o Podman foi concebido para operar sem exigir root. Por padrão, se você executar podman run como usuário comum, o processo do container não terá privilégios de root no host, graças ao uso de namespaces de usuário. Vale notar que o Podman também pode ser executado como root (ex.: sudo podman), caso em que se comporta similarmente ao Docker rootful, mas o design padrão prioriza a execução rootless.



Execução Rootless - Podman


Ser rootless significa que o usuário root dentro do container não será root fora dele. O Podman possui essa característica, ele consegue fazer isso nativamente (sem configuração adicional como no Docker). A execução dos containers é sem privilégios, aproveitando os namespaces de usuário (user namespaces) para isolar o UID 0 do contêiner.


Dentro do contêiner, o processo pode ter UID 0 (ou seja, aparecer como "root" para si mesmo), mas esse UID 0 está mapeado para um UID não privilegiado do usuário no host (fora do container). Na prática, o usuário que invoca o Podman é mapeado como root dentro de seu contêiner.


Por exemplo, se o seu UID no host é 1000, o processo no container executará com UID 0 internamente, mas o kernel o trata como UID 1000 externamente. Cada usuário do sistema possui um intervalo separado de UIDs subordinados (tipicamente 65.536 IDs) definido em /etc/subuid e /etc/subgid para esse mapeamento. Com isso, múltiplos usuários podem rodar contêineres isoladamente.


Com o Podman, os contêineres de usuários diferentes não interferem uns nos outros e não podem escalonar privilégios no host. Não há daemon central, os contêineres que Alice cria pertencem a Alice, os de Bob pertencem a Bob, e assim por diante, cada um com armazenamento de imagens separado e rede separada.


Essa isolação multiusuário evita que, por exemplo, um usuário sem querer acesse ou manipule contêineres de outro, e melhora a auditabilidade (as ações em contêineres podem ser atribuídas ao UID do usuário que as lançou).



Execução Rootless - Docker


O Docker sempre exigiu privilégios de root (diretos ou via grupo docker) para funcionar. Em resposta a preocupações de segurança, foi introduzido um modo rootless a partir do Docker Engine 19.03+. No Docker rootless, o próprio daemon Docker (dockerd) é executado como usuário não-root, dentro de um namespace de usuário, e os containers que ele gerencia também rodam sem privilégios reais de root no host.


Em essência, é semelhante ao que o userns-remap do Docker já fazia (remapeamento de UID/GID dos contêineres), porém agora inclui o daemon em si rodando sem root. Isso reduz significativamente a superfície de ataque, se antes comprometer o daemon significava comprometer o host como root, no modo rootless uma falha no daemon renderia apenas acesso com privilégios limitados do usuário do daemon.


O modo rootless do Docker não é ativado por padrão e requer configuração manual e alguns pré-requisitos (como já foi falado em Docker Daemon com modo rootless. Além disso, possui algumas limitações, por exemplo, orquestração Docker Swarm não funciona em modo rootless (o Swarm ainda requer o daemon rodando como root).


A parte de rede também é restrita, contêineres rootless no Docker não podem criar interfaces de rede no kernel do host, em vez disso usam soluções em espaço de usuário (como slirp4netns via RootlessKit) para emular networking, o que implica não poder ligar portas abaixo de 1024 diretamente e um desempenho de rede um pouco inferior em comparação ao modo rootful convencional.


O Podman já nasceu com suporte a rootless e teve tempo para amadurecer essa funcionalidade, é dito inumeras vezes em documentações que no Podman, o rootless simplesmente funciona out-of-the-box, enquanto no Docker é necessário habilitar explicitamente e lidar com possíveis problemas.


Antes do modo rootless completo, o Docker oferecia uma opção chamada userns-remap (também já foi explicado aqui User Namespace (userns-remap)) que atenua o problema de privilégios. Porém, mesmo com userns-remap ativado, o daemon Docker ainda roda como root, mas cada contêiner é criado em um namespace de usuário isolado com UIDs mapeados para um intervalo não privilegiado no host.


Isso significa que se um processo escapasse do contêiner, ele estaria com não root no host (sem privilégio administrativo) em vez de UID 0. Essa funcionalidade melhora a segurança, mas não era habilitada por padrão, e certas operações (como uso de volumes NFS ou dispositivos) ficavam mais complicadas sob remap.


O modo rootless do Docker é similar ao userns-remap em conceito, porém vai além ao também executar o daemon sem root. Portanto, ao comparar com Podman, ambos Podman rootless e Docker rootless usam namespaces de usuário e subUIDs para confinamento, mas no Docker é uma configuração opcional e relativamente nova, enquanto no Podman é o comportamento padrão e bem integrado.



Riscos de Segurança no Docker - Modo Rootful


O Docker Engine, rodando como root no host, representa uma superfície de ataque crítica. Uma vulnerabilidade explorável no dockerd pode conceder controle total do sistema ao atacante. Já houve casos e alertas de segurança onde o daemon Docker foi apontado como risco, afinal, ele expõe APIs (socket Unix e opcionalmente TCP) que, se acessadas maliciosamente, fornecem meios de escalar privilégios.


Por exemplo, um invasor que consiga acesso ao socket Docker (/var/run/docker.sock) pode executar contêineres privilegiados ou montar volumes sensíveis, efetivamente obtendo root no host. Isso é resumido frequentemente como ter acesso ao socket do Docker é equivalente a ter acesso root na máquina.


Portanto, em servidores multiusuário, simplesmente adicionar usuários ao grupo docker (para executarem comandos Docker sem sudo) já é um risco grave, esses usuários ganham indiretamente poderes de root no host.



Contêineres como Root sem Userns


Por padrão, quando se executa um contêiner Docker sem nenhuma opção especial, o processo dentro do contêiner terá UID 0 e esse UID corresponde ao root do host (no modo clássico sem userns-remap). Isso significa que se o isolamento for rompido ou mal configurado, o processo pode realizar ações destrutivas no sistema host.


Um exemplo foi a vulnerabilidade CVE-2019-5736 (uma falha no runC descoberta em 2019) onde um contêiner Docker malicioso conseguiu explorar essa falha para sobrescrever o binário do runC no host e obter execução de código como root no host.

Contêineres Docker com configuração padrão eram vulneráveis, enquanto contêineres LXC, por exemplo, só eram afetados se estivessem em modo privilegiado. Esse caso ilustra que o modelo padrão do Docker (root no contêiner = root no host) pode amplificar o impacto de vulnerabilidades de escape.


Outra vulnerabilidade de kernel (CVE-2022-0185), demonstrou algo similar, um usuário sem privilégio dentro de um contêiner conseguiu explorar uma falha no kernel (um heap overflow no subsystem de filesystem) para escalar a root no host, isso exigia capacidades avançadas (CAP_SYS_ADMIN no namespace), mas contêineres privilegiados ou cenários Kubernetes sem seccomp tornavam o exploit viável.



Contêineres Privilegiados


O Docker permite executar contêineres em modo privilegiado (docker run --privileged), o que essencialmente desabilita muitas restrições que ele possui. O contêiner recebe todas as capabilities do Linux, podendo acessar dispositivos do host, montar file systems arbitrários, etc.


Esse modo é extremamente perigoso, já que um contêiner privilegiado é quase equivalente a rodar um processo diretamente no host. Mesmo sem --privileged, o Docker por padrão ainda concede um conjunto de cerca de 14 capabilities ao contêiner, incluindo por exemplo CAP_NET_RAW e CAP_NET_ADMIN (que permitem manipular rede), CAP_CHOWN, CAP_SETUID, entre outras.


Embora seja uma redução em relação a root completo, ainda é um conjunto muito amplo, principalmente quando comparamos o Docker com o Podman. Assim, um processo mal-intencionado em um contêiner Docker padrão tem mais poder dentro do namespace do que teria sob políticas mais restritivas, em outras palavras, o Docker ajuda a aumentar as chances de explorar alguma brecha.


Por exemplo, se o seccomp do Docker não bloqueasse unshare(NEWUSER) (chamada que cria novos namespaces), um processo com CAP_SYS_ADMIN no contêiner poderia tentar habilitar ainda mais privilégios. (o Docker bloqueia certas syscalls perigosas via seccomp por padrão, como unshare sem CLONE_NEWUSER, para mitigar escapes como o CVE-2022-0185.)


Um ataque comum em ambientes Docker inseguros é montar partes críticas do host dentro do contêiner e então manipulá-las. Qualquer usuário com acesso ao Docker (grupo docker) pode executar comandos. Por exemplo, se um atacante conseguir executar o comando abaixo:

docker run -v /:/host alpine chroot /host /bin/bash

O comando acima monta todo o filesystem root (/) do host dentro do contêiner e então faz chroot para esse diretório, desviando completamente do isolamento.


O usuário vai obter um shell root no sistema host a partir do contêiner. Esse tipo de ataque aparece em certos CTFs.


Para finalizar, no Docker rootful padrão, o contêiner possui privilégios elevados e depende da configuração cuidadosa para não comprometer o host. Administradores mitigam isso usando userns-remap, não executando contêineres como root dentro (usam também USER não-root nas imagens) e ainda aplicam perfis AppArmor/SELinux, limites de recursos e evitando --privileged e montagens inseguras.


Mas se nada disso for feito, um contêiner malicioso ou mal configurado pode escapar e causar danos.



Segurança com Podman Rootless (Proteção por Padrão)


Como o Podman não possui um daemon rodando como root, elimina-se aquela preocupação de "e se o serviço do contêiner for explorado?". Não existe um processo privilegiado permanente esperando comandos. Isso significa que não há um alvo centralizado para um atacante explorar e conseguir root no host.


Mesmo quando o Podman roda em modo rootless, internamente ele utiliza as chamadas do kernel para criar namespaces e cgroups via runc/crun, mas essas chamadas são feitas pelo processo do usuário. Caso um contêiner comprometa seu próprio processo, no pior caso ele tem acesso ao que o usuário original tinha.


Esse modelo de segurança inerente é uma das razões pelas quais distribuições e ambientes de alta segurança têm preferência pelo Podman, tudo isso sem exigir tantas configurações adicionais.


Containers lançados por Podman vêm com um perfil de capabilities mais restrito, incluindo cerca de 11 (contra 14 do Docker). Coisas como alterar certos aspectos de rede, ptrace em processos fora, etc., são mais contidas. Além disso, se o kernel do host suporta SELinux, o Podman aplica automaticamente rótulos SELinux nos contêineres para adicionar uma camada extra de confinamento MAC (Mandatory Access Control).


Isso significa que, mesmo se um processo escapasse do namespace de usuário (virando UID 1000 no host), ele ainda estaria sob domínio restrito de SELinux que normalmente só permite acessar arquivos com contextos específicos associados àquele container.


O Docker também pode usar SELinux ou AppArmor, mas geralmente isso requer configuração explícita (--security-opt) ou depende da distribuição. No Podman, a integração de segurança do host (como SELinux) é primeira classe, acionada automaticamente quando disponível, sem depender do usuário lembrar de ativar.


Muitas das melhores práticas de segurança Docker envolvem fazer muitas coisas (por exemplo, não rode contêineres como root usuário, ative userns, não monte socket, etc.). Com Podman, diversas dessas práticas já estão implementadas por padrão. O Podman nem permite certas coisas facilmente em rootless, por exemplo, contêiner rootless não pode abrir portas < 1024 do host diretamente (o que evita, por exemplo, que um serviço no contêiner consiga bindar porta 80 a não ser que o administrador configure redirecionamento ou use auth. de porta).


Se um contêiner tentar carregar um módulo de kernel, ou acessar /dev/mem, ou algo intrusivo, em rootless ele não terá sucesso porque vai faltar privilégios no host. Ainda é possível rodar Podman em modo root (como já falei), e também existe --privileged no Podman (que em rootless concede todas capabilities dentro do namespace, mas não torna o processo root real no host, mesmo que permita o container abrir outros tipos de brechas).


Mesmo usando --privileged não vai abrir brechas no host, pois o processo não recebe poderes globais. Ele pode até tentar montar algo dentro do seu namespace (não afetará o host) ou insmod em um módulo (o kernel recusará, pois o processo não é root real). Assim, atividades como fork bomb (bombas de processo) ou consumo exagerado de recursos também ficam limitadas às cotas do usuário, embora contêineres rootless ainda possam, por exemplo, exaurir CPU ou memória do host se não forem controlados via cgroups.


Em ambiente de produção, recomenda-se ainda assim definir limites de recursos (memória, CPU, número de PIDs) tanto para Docker quanto para Podman, para impedir DoS caso um contêiner seja comprometido. A diferença é que no Docker, sem limites, um contêiner root podia travar o host inteiro, já no Podman rootless, um contêiner descontrolado pode afetar aquele usuário em questão, mas o administrador ainda pode ter controle (por exemplo, matar todos os processos daquele UID).


Como tudo na vida, a segurança do container nunca será absoluta, um contêiner Podman rootless ainda poderia explorar alguma vulnerabilidade do kernel que afetasse processos de usuário comum. Entretanto, a barreira para comprometer o sistema é muito maior.



Exemplos de Root vs Rootless


Vou tentar monstrar algumas diferenças maneiras de executar contêineres no Docker tradicional (rootful) e no Podman rootless, com foco na parte de vulnerabilidade que o rootful traz.



Leitura de arquivo sensível do host


Suponha que um usuário mal-intencionado tenha acesso ao Docker (por exemplo, pertence ao grupo docker). Ele pode tentar ler um arquivo confidencial do host, como /etc/shadow (que contém os hashes de senha). Com um contêiner Docker rootful, isso pode ser feito montando o arquivo no contêiner e lendo-o:

# Verificando se o usuário está no grupo docker:
$ groups | grep -o docker
docker

# Tentando ler o arquivo diretamente:
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

# Tentando com docker:
$ docker run --rm -v /etc/shadow:/host_shadow:ro alpine cat /host_shadow
root:*:19962:0:99999:7:::
daemon:*:19962:0:99999:7:::
bin:*:19962:0:99999:7:::
sys:*:19962:0:99999:7:::

Como o container está rodando como root real, ele conseguirá ler o conteúdo de /etc/shadow do host sem impedimentos. Esse é um comportamento perigoso, de fato, essa técnica de montar diretórios do host é uma das recomendações para não se fazer, pois contorna isolamentos.


Já com Podman, se o mesmo usuário tentar o mesmo com o comando abaixo:

$ podman run --rm -v /etc/shadow:/host_shadow:ro alpine cat /host_shadow
cat: can't open '/host_shadow': Permission denied

O resultado será uma falha de permissão. No Podman, o processo dentro do contêiner está rodando com o UID do usuário no host (digamos UID 1000). Mesmo que dentro do contêiner ele "ache" que é root, ao tentar abrir /host_shadow (que mapeia para /etc/shadow do host), o kernel nega acesso porque UID 1000 não tem leitura nesse arquivo (permissões 640, dono root).



Escapando para shell do host


Conforme mencionei, executar um contêiner Docker montando todo o rootfs do host e chrootando é trivial e efetivo. Podemos fazer da seguinte forma (com o mesmo usuário do teste acima):

$ docker run --rm -it -v /:/host alpine sh -c "chroot /host /bin/bash"

root@24e7561ff635:/# cat /etc/hostname
odin

Após esse comando, o usuário terá um prompt de bash que é o shell do host, como root. Ele pode inspecionar ou modificar qualquer coisa no sistema.


Novamente, vamos testar com Podman:

$ podman run --rm -it -v /:/host alpine sh -c "chroot /host /bin/bash"
groups: cannot find name for group ID 11
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

bash: /root/.bashrc: Permission denied

O comando de chroot em si pode até ser executado (já que o processo dentro do container tem CAP_SYS_CHROOT no seu namespace). Ele vai trocar o root filesystem para /host (que é / do host montado). No entanto, quem está executando /bin/bash a partir desse ponto?


É o meu usuário, então o shell obtido não terá privilégios root no host, mas sim os do usuário do meu usuário no host. Se o invasor listar processos, verá que ele está rodando como um usuário sem poderes administrativos. Ele até pode navegar pelo FS, listar diretórios públicos do host, mas qualquer ação privilegiada será negada.


Tentar editar arquivos do sistema, iniciar serviços ou mudar configurações críticas simplesmente não é possível sem root de verdade. Em essência, o atacante não ganhou nada além do que já tinha fora do contêiner. Assim, no Podman rootless esse vetor de escape se torna inútil para elevação de privilégio (no máximo, ele pode usar o contêiner para conveniência de chroot, mas não consegue nada que não conseguiria na sua conta normal).



Desempenho de I/O em storage


Contêineres rootless sofriam pequeno decréscimo de performance em operações de filesystem porque não podiam usar diretamente o driver overlayfs do kernel (que requer root). O Podman rootless então utilizava o fuse-overlayfs, um sistema em espaço de usuário, para implementar as camadas de imagem.


Isso podia ser mais lento para I/O intensivo. Entretanto, desde o kernel 5.12 e Podman v3.1+, já é possível usar overlayfs verdadeiro mesmo em rootless (através de ID mapping do kernel) quando suportado. Isso praticamente eliminou a diferença de performance de armazenamento entre Podman rootless e Docker rootful, contêineres rootless modernos conseguem I/O tão veloz quanto os tradicionais.


Ainda assim, em sistemas mais antigos, pode haver impacto mensurável: por exemplo, benchmarks mostram escrita em overlay via fuse sendo algumas vezes mais lenta. No Docker rootless, a situação é similar (ele também recorria a fuse-overlay até suporte do kernel melhorar).