Segurança
A segurança em ambientes Docker é crucial para garantir que os contêineres e as aplicações que eles hospedam estejam protegidos contra vulnerabilidades e ameaças. Como o Docker cria ambientes isolados para aplicações, ele oferece um nível básico de segurança. No entanto, essa isolação não é infalível, e a falta de boas práticas pode expor o ambiente a riscos como contêineres maliciosos, violações de rede e acessos não autorizados.
Executar Docker com usuário não root
Por padrão, o daemon do Docker (dockerd) escuta em um soquete Unix, normalmente localizado em /var/run/docker.sock. Esse soquete é usado para a comunicação entre o cliente Docker (que é o docker CLI) e o daemon, sem precisar abrir uma porta TCP. Isso evita exposição desnecessária da API na rede e aumenta a segurança.
Se quiser, o daemon pode ser configurado para aceitar conexões TCP (por exemplo, via -H tcp://0.0.0.0:2375), mas isso não é recomendado sem TLS, pois permitiria controle remoto completo sobre o Docker host.
Por padrão,o usuário root é o dono desse soquete Unix (sendo o docker o grupo dono), outros usuários só podem acessá-lo usando sudo (O daemon Docker sempre é executado como usuário root) ou configurando os usuário no grupo docker.
Por padrão, os contêineres Docker são executados com privilégios de superusuário (root) dentro e fora do contêiner. Isso significa que o processo dentro do contêiner é executado com permissões elevadas, independentemente do usuário que o iniciou no ambiente Docker host. O motivo para isso é que, no nível do sistema operacional, um contêiner Docker é simplesmente um processo comum no sistema hospedeiro. Todo processo em um sistema operacional tem um "dono", e o "dono" do processo do contêiner é o usuário do sistema hospedeiro que executou o comando docker run.
Para ilustrar, se o comando docker run for executado por um usuário com privilégios de root, o processo dentro do contêiner também será executado como root, herdando assim todas as permissões associadas. No entanto, se o docker run for executado por um usuário não root, o correto seria dizer que o usuário root dentro do container não terá privilégios de root né?
Sim e não, o processo dentro do contêiner ainda será executado como root em algumas situações.
Se você está criando um grupo chamado docker e colocando todos os usuários que devam usar o docker dentro desse grupo, o usuário root dentro do container vai ter privilégios elevados porque o grupo docker concede privilégios de root aos usuários dentro do grupo. Isso significa que, mesmo que o usuário que iniciou o contêiner não tenha privilégios de root no sistema host, o processo dentro do contêiner ainda terá esses privilégios de root, devido à associação com o grupo docker.
Essa abordagem tem implicações importantes em termos de segurança, pois um processo dentro de um contêiner com privilégios de root pode potencialmente realizar operações de alto impacto no sistema hospedeiro, como instalação de software, modificação de arquivos críticos do sistema e acesso a recursos sensíveis.
Mas entenda que, embora o usuário que executa o docker run tenha influência sobre o usuário padrão dentro do container, o usuário root dentro do container não tem acesso direto ao sistema de arquivos ou recursos do host. Isso se deve ao isolamento fornecido pelos containers Docker, que implementam namespaces de processos, rede e sistema de arquivos. Para que ele possa ter acesso, como já foi dito, a imagem teria que estar infectada com algum malware.
Para evitar esse tipo de coisa é crucial utilizar imagens confiáveis de fontes oficiais e manter os containers atualizados para reduzir o risco de infecções por malware.
Existem quatro abordagens que podemos seguir para deixar a execução dos containers um pouco mais segura, do melhor método para o menos indicado (pelo menos se usado sem nenhum outro método):
- Utilização do Docker em modo rootless;
- Usar o recurso user-namespace;
- Usa a opção
USERdentro do Dockerfile; - Usar o grupo Docker;
A planilha abaixo representa as vantagens e desvantagens de cada método descrito acima:
| Método | Vantagens | Desvantagens |
|---|---|---|
| Modo Rootless | Maior segurança, os daemon e containers são executados como usuários não root. | Nem todas as funcionalidades do Docker disponíveis, como a utilização de volumes anônimos e a configuração de redes complexas. A implementação e o gerenciamento do modo Rootless podem exigir mais conhecimento técnico e familiaridade com namespaces de usuários do Linux, o que pode ser um obstáculo para usuários menos experientes. Nem todas as ferramentas e workflows Docker são compatíveis com o modo Rootless. Isso pode exigir a adaptação de ferramentas ou a busca por alternativas compatíveis, o que pode gerar trabalho adicional. |
| User Namespace | Mapeamento do usuário root para um específico no host. | Requer conhecimento de namespaces de usuários, daemon do Docker com privilégios de root. Apesar de oferecer isolamento granular, o próprio daemon do Docker ainda é executado com privilégios de root, o que pode representar um risco de segurança se houver vulnerabilidades no daemon. |
Opção USER no Dockerfile | Limitação de privilégios do processo principal, redução da superfície de ataque. | Usuário root dentro do container ainda existe com privilégios administrativos, isolamento menos granular. |
| Grupo Docker | - Não recomendado: riscos de segurança e baixa segurança. | - Não recomendado: evitar para aumentar a segurança. |
Grupo Docker
Esse método é útil quando você não quer executar os comandos docker com sudo, para isso, crie um grupo Unix chamado docker e adicione usuários a ele. Quando o daemon Docker é iniciado, ele cria um soquete Unix acessível aos membros do grupo docker. Em algumas distribuições Linux, o sistema cria automaticamente este grupo ao instalar o Docker Engine usando um gerenciador de pacotes. Nesse caso, não há necessidade de criar o grupo manualmente.
Colocando usuário no grupo docker:
# Verifique se o grupo existe, se não existir, nada será retornado:
╼ $ sudo getent group docker
docker:x:999:
### Caso não exista, crie o grupo:
╼ $ sudo groupadd docker
# Adicione os usuários dentro do grupo docker:
╼ $ sudo gpasswd -a username docker
Adding user username to group docker
# Agora reinicie o daemon do docker:
╼ $ sudo systemctl restart docker
# Verifique se funcionou:
╼ $ id
uid=1000(username) gid=1000(username) groups=1000(username)
## Não vemos o grupo docker aqui, então devemos reiniciar o sistema!
# Caso não possa reiniciar o sistema agora, use o comando abaixo
# para ativar as alterações nos grupos (se deslogar perderá a ação realizada):
╼ $ newgrp docker
# Quando puder reiniciar, faça:
╼ $ sudo reboot
# Verifique se funcionou:
╼ $ docker run hello-world
Hello from Docker!
Docker daemon attack surface
O daemon do Docker apresenta uma superfície significativa de ataque devido aos seus privilégios de root, a menos que o modo Rootless seja utilizado. Em primeiro lugar, apenas usuários confiáveis devem ter permissão para controlar seu daemon Docker.
O Docker permite compartilhar um diretório entre o host do Docker e um contêiner convidado; e permite que você faça isso sem limitar os direitos de acesso do contêiner. Isso significa que você pode iniciar um contêiner onde o diretório /host é o diretório / do seu host; e o contêiner pode alterar seu sistema de arquivos host sem qualquer restrição.
Isso é semelhante ao modo como os sistemas de virtualização permitem o compartilhamento de recursos do sistema de arquivos. Nada impede que você compartilhe seu sistema de arquivos raiz.
Isso tem uma forte implicação de segurança: por exemplo, se você instrumentar o Docker a partir de um servidor web para provisionar contêineres por meio de uma API, deverá ser ainda mais cuidadoso do que o normal com a verificação de parâmetros, para garantir que um usuário mal-intencionado não possa passar parâmetros criados causando danos ao Docker para criar contêineres arbitrários.
Por esse motivo, o endpoint da API REST (usado pela CLI do Docker para se comunicar com o daemon do Docker) mudou no Docker 0.5.2 e agora usa um soquete UNIX em vez de um soquete TCP vinculado a 127.0.0.1. Você pode então usar verificações de permissão tradicionais do UNIX para limitar o acesso ao soquete de controle.
Mesmo que você coloque um firewall para limitar o acesso ao endpoint da API REST do Docker de outros hosts na rede, o endpoint ainda poderá ser acessível a partir de contêineres e poderá facilmente resultar na escalada de privilégios. Portanto, é obrigatório proteger os endpoints da API com HTTPS e certificados. Também é recomendável garantir que ele seja acessível apenas por uma rede confiável ou VPN (o padrão é estar desativado).
O daemon também é potencialmente vulnerável a outras entradas, como carregamento de imagem de qualquer disco com docker load ou da rede com docker pull. A partir do Docker 1.3.2, as imagens agora são extraídas em um subprocesso chroot em plataformas Linux/Unix, sendo o primeiro passo em um esforço mais amplo em direção à separação de privilégios. A partir do Docker 1.10.0, todas as imagens são armazenadas e acessadas pelos checksums criptográficos de seu conteúdo, limitando a possibilidade de um invasor causar uma colisão com uma imagem existente.
Finalmente, se você executar o Docker em um servidor, é recomendado executar exclusivamente o Docker no servidor e mover todos os outros serviços para dentro de contêineres controlados pelo Docker. Você ainda pode ter outros serviços rodando no host Docker, mas minimize o máximo possível.
Docker daemon com modo Rootless
O modo (rootless) no Docker permite executar o daemon do Docker e os contêineres como um usuário não root para mitigar possíveis vulnerabilidades no daemon e no tempo de execução do contêiner. Esse modo não requer privilégios de root, mesmo durante a instalação do daemon do Docker, desde que os pré-requisitos sejam atendidos.
O modo rootless executa o daemon do Docker e os contêineres dentro de um espaço de nomes de usuário (user namespace). Isso é muito semelhante ao modo userns-remap (veremos mais abaixo), exceto que, com o modo userns-remap, o próprio daemon está sendo executado com privilégios de root, enquanto que no modo rootless, tanto o daemon quanto o contêiner estão sendo executados sem privilégios de root.
O modo rootless não usa binários com bits SETUID ou capacidades de arquivo, exceto newuidmap e newgidmap, que são necessários para permitir que vários UID/GID sejam usados no espaço de nomes de usuário.
Para usar esse método, é necessário atender a certos requisitos:
Você deve instalar o
newuidmape onewgidmapno host. Esses comandos são fornecidos pelo pacoteuidmapna maioria das distribuições.Os arquivos
/etc/subuide/etc/subgiddevem conter pelo menos 65.536 UIDs/GIDs subordinados para o usuário.
Para ver como implantar esse método, veja a documentação oficial.
Eu vou deixar uma documentação separada e bem completa da diferença entre Rootless e Rootful.
User Namespace (Userns-Remap)
Os Linux namespaces fornece isolamento para a execução de processos, limitando o acesso deles aos recursos do sistema sem que o processo em execução esteja ciente das limitações. A melhor maneira de prevenir ataques de escalonamento de privilégios de dentro de um contêiner é configurar as aplicações do seu contêiner para serem executadas como usuários não privilegiados.
Para contêineres cujos processos devem ser executados como usuário root dentro do contêiner, você pode remapear esse usuário para um usuário menos privilegiado no host Docker. O usuário mapeado recebe uma faixa de UIDs que funcionam dentro do namespace como UIDs normais de 0 a 65536, mas não têm privilégios na própria máquina host.
Ou seja, ao utilizar o user-namespaces, o usuário root dentro do container é mapeado para um usuário não root na máquina host Docker, reforçando a segurança através do isolamento de privilégios.
Pré-requisitos
Para usar esse método, é necessário atender a certos requisitos:
Requisito 1
Para usar namespaces de usuário no Docker, é necessário associar faixas de IDs de usuário (UIDs) e IDs de grupo (GIDs) a um usuário existente. Essas faixas de IDs são responsáveis por mapear os usuários e grupos dentro do contêiner para usuários e grupos no host Docker. Mesmo que essa associação seja um detalhe de implementação, é importante que exista.Se você preferir não usar um usuário existente, o Docker pode criar um para você. No entanto, se você optar por usar um usuário existente, é necessário garantir que ele já esteja definido no sistema, o que geralmente é feito através de entradas correspondentes nos arquivos
/etc/passwde/etc/group.Além disso, é importante notar que o usuário associado às faixas de UID e GID é o proprietário dos diretórios de armazenamento namespace localizados em
/var/lib/docker/. Isso significa que ele terá permissões sobre esses diretórios.Requisito 2
No host, o remapeamento de namespaces vai ser controlado por dois arquivos:/etc/subuide/etc/subgid. Esses arquivos geralmente são gerenciados automaticamente quando você adiciona ou remove usuários ou grupos, mas em algumas distribuições como RHEL e CentOS 7.3, pode ser necessário gerenciá-los manualmente.Cada arquivo contém três campos: o nome de usuário ou ID do usuário, seguido por um UID ou GID inicial (que é tratado como UID ou GID 0 dentro do namespace) e o número máximo de UIDs ou GIDs disponíveis para o usuário. Por exemplo, considerando a seguinte entrada:
vagrant:100000:65536Isso significa que os processos com namespaces de usuário iniciados pelo usuário vagrant são de propriedade do UID do host 100000 (que parece ser o UID 0 dentro do namespace) até 165535 (100000 + 65536 - 1). Esses intervalos não devem se sobrepor para garantir que os processos com namespaces não possam acessar os namespaces uns dos outros.
Depois de adicionar seu usuário, verifique se há uma entrada para o usuário em
/etc/subuide/etc/subgid. Se não houver, será necessário adicioná-lo, tomando cuidado para evitar sobreposições. Veja o exemplo abaixo após adicionar um novo usuário:/etc/subuidvagrant:100000:65536
fulano:165536:65536/etc/subgidvagrant:100000:65536
fulano:165536:65536Requisito 3
Se houver algum local no host do Docker onde o usuário não privilegiado precise gravar, ajuste as permissões desses locais conforme necessário. Isso também é verdadeiro se você quiser usar o usuáriodockremapcriado automaticamente pelo Docker, mas não puder modificar as permissões até depois de configurar e reiniciar o Docker.Requisito 4
Habilitaruserns-remapefetivamente mascara as camadas existentes de imagens e contêineres, bem como outros objetos do Docker dentro de/var/lib/docker/. Isso ocorre porque o Docker precisa ajustar a propriedade desses recursos e, na verdade, armazená-los em um subdiretório dentro de/var/lib/docker/. É melhor habilitar esse recurso em uma nova instalação do Docker em vez de uma existente.Da mesma forma, se você desativar
userns-remap, não poderá acessar nenhum dos recursos criados enquanto estiver habilitado.
Habilitando o userns-remap
Existem duas forma de habilitar o userns-remap:
Você pode iniciar o dockerd com a flag --userns-remap, para isso é necessário modificar o .service em sistemas que usam o SystemD. Ou configurar o daemon do Docker usando o arquivo de configuração daemon.json, sendo este o método mais recomendado.
{
"userns-remap": "testuser:testuser"
}
Para usar o usuário
dockremape fazer com que o Docker o crie para você, defina o valor comodefaultem vez de testuser.
Dockerfile com a opção USER
Por padrão, a maioria dos contêineres Docker é executada com o usuário root, o que pode trazer riscos significativos de segurança, especialmente em ambientes de produção. Contudo, algumas tarefas específicas durante o processo de build e deploy podem exigir permissões de root para serem concluídas sem erros.
Após realizar essas tarefas que exigem privilégios elevados, é uma boa prática criar explicitamente um usuário não root e conceder a ele o nível mínimo de acesso necessário para executar o aplicativo. Para isso, utilize ferramentas como chmod e chown para ajustar as permissões de arquivos e diretórios.
Antes das diretivas ENTRYPOINT ou CMD, adicione a instrução USER para garantir que o aplicativo seja executado como um usuário não root. Isso protege o ambiente de execução, restringindo o que pode ser acessado pelo contêiner.
Para ver como implementar, acesse aqui.
Qual opção usar?
Vamos usar o princípio do menor privilégio que é uma abordagem fundamental e deve ser levada em conta ao trabalharmos com containers. O princípio do menor privilégio se resume a conceder apenas os privilégios necessários para uma determinada tarefa, processo ou usuário. Para mais detalhes leia aqui.
Para uma melhor prática no uso dos containers, eu recomendo criar um usuário e um grupo específicos para cada aplicação que o container vai executar (basicamente todo container terá um usuário não root dentro dele), após isso, deve-se fazer uso da opção USER no dockerfile ou user no docker-compose. A criação dos usuários é feita usando o comando useradd, já a criação de grupo é feita usando o comando groupadd no Linux (da para usar apenas useradd para criar tanto o usuário quanto o grupo).
É fundamental garantir que apenas as permissões necessárias sejam concedidas a esses usuários e grupos criados. Se você usar os usuários que já existem no container, como no caso de container de banco de dados, não precisa se preocupar em criar o usuário.
A criação do usuário dentro do container vai depender do container que está sendo inicializado. Por exemplo, se estiver executando um container com o MySQL, você configuraria o Dockerfile ou Docker-compose para usar o usuário do MySQL para executar o serviço dentro do container (normalmente é o usuário com UID 999 e com GID 999). Da mesma forma, para um container com MongoDB, você usaria o usuário dedicado ao MongoDB (normalmente é o usuário com UID 999 e com GID 999).
Agora, se estiver executando uma aplicação em NodeJS, Java, Python, ou qualquer outra linguagem, você pode fazer a criação do usuário dentro do container.
Abaixo segue um exemplo de como seria a configuração para container com banco de dados. Já temos um exemplo de como seria usando dockerfile com a opção USER quando se deve criar o usuário.
mongodb:
image: docker.io/mongo:4.4.25
command: mongod --nojournal --storageEngine wiredTiger
restart: unless-stopped
user: "999:999"
volumes:
- ../../data/mongodb:/data/db
mysql:
image: docker.io/mysql:8.1.0
command: >
mysqld
--character-set-server=utf8mb3
--collation-server=utf8mb3_general_ci
--binlog-expire-logs-seconds=259200
restart: unless-stopped
user: "999:999"
volumes:
- ../../data/mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: "SENHA"
Isso ajuda a limitar os privilégios do serviço apenas ao que é necessário para sua execução, reduzindo assim a superfície de ataque e potenciais riscos de segurança.
A abordagem descrita acima é eficaz e granular, pois restringe os privilégios ao mínimo necessário para a execução da aplicação. Ainda recomendo liberar o uso do docker com sudo para um maior controle de quais usuários podem usar o docker; lembrando que deve ser liberado o sudo apenas para executar os comandos docker e nada mais. Ainda é possível usar o grupo do docker para isso, mas não terá os registros de logs dos comandos executados.
A opção do rootless é uma opção viável, mas ela tem suas limitações e pode não oferecer a mesma granularidade de controle de privilégios.
Caso o usuário deva ser criado via docker-compose, não será possível, mas podemos adotar uma abordagem simples. Crie o script abaixo:
#!/bin/bash
# Cria um grupo e um usuário dentro do container
useradd -r myuser
# Executa o comando principal do container (no caso do Node.js, você pode iniciar sua aplicação Node.js aqui)
exec "$@"
Agora no docker file, adicione o script como command e logo em seguida os comandos necessário para que o container tenha uma razão/propósito contínuo de existir:
version: '3'
services:
node_app:
image: node:14
volumes:
- ./init.sh:/usr/local/bin/init.sh
command: /usr/local/bin/init.sh npm start
Capabilities
No Linux, Capabilities são mecanismos de segurança que permite um controle mais granular das permissões de um processo. Ao invés de simplesmente permitir ou negar acesso a recursos do sistema, as capabilities oferecem um conjunto mais refinado de permissões que podem ser concedidas ou negadas individualmente a um processo. Isso melhora a segurança, reduzindo o risco de abuso de privilégios caso um processo seja comprometido.
Os containers Docker não recebem acesso irrestrito de root no host, mesmo quando o processo dentro do container está rodando como root. Por padrão, o Docker remove muitas capabilities sensíveis para proteger o host de ações perigosas feitas por processos no container. Algumas capabilities críticas, como CAP_SYS_ADMIN e CAP_NET_RAW, são removidas para evitar abuso.
Você pode modificar as capabilities de um container com as opções:
- --cap-add: Adiciona uma capability específica ao container.
- --cap-drop: Remove uma capability específica.
Abaixo podemos ver um exemplo de como usar. Estamos adicionando a capability CAP_NET_ADMIN (gerenciar interfaces de rede) e removendo a capability CAP_NET_RAW (uso de sockets brutos).
╼ $ sudo docker run --cap-add=NET_ADMIN --cap-drop=NET_RAW nginx
Na tabela abaixo podemos ver as principais capabilities:
| Capabilities | Descrição |
|---|---|
CAP_CHOWN | Permite alterar o proprietário de arquivos (como com chown), mesmo sem ser o dono. |
CAP_DAC_OVERRIDE | Ignora as verificações de permissão padrão de arquivos (bypass de DAC). |
CAP_DAC_READ_SEARCH | Permite ler qualquer diretório ou arquivo, ignorando permissões de leitura. |
CAP_FOWNER | Permite modificar arquivos mesmo sem ser o proprietário, caso as permissões permitam. |
CAP_FSETID | Permite modificar bits de set-user-ID e set-group-ID em arquivos. |
CAP_KILL | Permite enviar sinais para processos pertencentes a outros usuários. |
CAP_NET_BIND_SERVICE | Permite bind em portas < 1024 (portas "privilegiadas", como 80, 443). |
CAP_NET_ADMIN | Permite várias operações de rede, como configurar interfaces, roteamento, etc. |
CAP_NET_RAW | Permite uso de sockets raw e criação de pacotes personalizados. |
CAP_SYS_ADMIN | Capacidade muito poderosa; permite operações como montagem de sistemas de arquivos. |
CAP_SYS_CHROOT | Permite uso do chroot. |
CAP_SYS_PTRACE | Permite usar ptrace, ou seja, inspecionar e manipular outros processos (debug). |
CAP_SYS_TIME | Permite alterar o relógio do sistema. |
CAP_MKNOD | Permite criar dispositivos especiais com mknod. |
CAP_SETUID | Permite alterar o UID efetivo de um processo. |
CAP_SETGID | Permite alterar o GID efetivo de um processo. |
Diferentes formas de conceder uma Capability
Imagine que você precisa subir um container com Apache, ele deve ouvir na porta 80/443 do host hospedeiro. Em sistemas Linux, apenas processos com privilégios elevados (como root) podem fazer bind em portas abaixo de 1024, por exemplo, as portas 80 (HTTP) e 443 (HTTPS).
Quando trabalhamos com contêineres Docker e queremos que uma aplicação escute diretamente nessas portas sem rodar como root, é necessário conceder a capability CAP_NET_BIND_SERVICE.
Existem duas formas principais de conceder essa capacidade, via modificação do binário com setcap ou usando --cap-add na execução do contêiner. Ambas funcionam, mas têm diferenças importantes em escopo, persistência e segurança.
Usando setcap no Dockerfile
Este método modifica o binário da aplicação para que ele tenha a capability embutida como metadado. Exemplo:
RUN apk add --no-cache libcap libcap-utils \
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/httpd
# e (Effective): a capability será usada automaticamente quando o binário for executado.
# p (Permitted): a capability está permitida ao processo e pode ser usada.
Nesse cenário, o comando setcap aplica a capability diretamente ao binário (/usr/sbin/httpd), isso permite que a aplicação escute em portas baixas sem precisar rodar como root, isso é bem útil quando usamos a opção USER dentro do container para que ele seja executado com um usuáro não root.
Uma vantagem é que esse método permite rodar como usuário não-root, aumentando a segurança (usando a opção USER). Outro ponto forte é que a capability fica restrita ao binário específico, isso fornece um controle mais granular.
Como nem tudo são flores, alguns pontos precisam de atenção. O setcap só funciona corretamente se o sistema de arquivos suportar atributos estendidos (xattrs). Além disso, o metadado pode ser perdido ao copiar o binário para outros sistemas ou ao extrair a imagem incorretamente, mas isso não deve ser um problema se você fizer o build corretamente.
Usando --cap-add no Docker run
Este método adiciona a capability ao processo principal do contêiner em tempo de execução:
docker run --cap-add=CAP_NET_BIND_SERVICE myimage
Nesse caso, o Docker concede a capability CAP_NET_BIND_SERVICE ao processo PID 1 do contêiner. Isso permite escutar em portas privilegiadas, mas só enquanto o contêiner estiver rodando. Aqui, estamos aplicando no container inteiro e não em um processo dentro do container.
A vantagem é a simplicidade, recomandado para testes ou ambientes de desenvolvimento. Além disso, não precisa modificar o binário ou a imagem.
Mas atenção, esse método pode conceder a capability a mais processos do que o necessário, se o processo dentro do contêiner estiver rodando como não-root, pode não funcionar dependendo da configuração.
Seccomp security profiles for Docker
Os Seccomp Security Profiles no Docker são arquivos de configuração que definem as syscalls (chamadas de sistema) permitidas ou bloqueadas para os containers. Eles ajudam a controlar as ações que os processos dentro do container podem executar no kernel do host, oferecendo uma camada adicional de segurança.
Esses perfis utilizam o Seccomp (Secure Computing Mode), uma funcionalidade do kernel Linux, para limitar as interações de syscalls. Isso reduz a superfície de ataque no sistema operacional, tornando os containers mais seguros contra exploits que possam comprometer o host.
Por padrão, o Docker aplica um perfil Seccomp default a todos os containers, a menos que um perfil personalizado seja especificado. O perfil padrão permite as syscalls mais comuns usadas pela maioria das aplicações, bloqueando chamadas de sistema consideradas perigosas.
Com as Seccomp personalizadas, podemos definir perfis para atender às necessidades de segurança específicas de uma aplicação. Isso permite ajustar o nível de restrição, permitindo ou bloqueando syscalls com base no comportamento esperado do container. Exemplo de Uso de Perfis Seccomp no Docker:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["read", "write", "exit", "exit_group"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["execve"],
"action": "SCMP_ACT_ALLOW"
}
]
}
O defaultAction: SCMP_ACT_ERRNO bloqueia qualquer syscall não listada explicitamente. Já SCMP_ACT_ALLOW, permite syscalls necessárias, como leitura/escrita e execução de programas. Agora basta iniciar um container usando o perfil Seccomp personalizado:
╼ $ sudo docker run --security-opt seccomp=custom-seccomp.json ubuntu:latest
Seccomp e Capabilities
No começo eu achava que as capabilities bloqueavam totalmente as syscalls, mas descobri que o buraco é mais embaixo. As capabilities não bloqueiam syscalls diretamente. Basicamente, as capabilities controlam se a syscall será bem-sucedida ou não.
Vamos pegar o exemplo da CAP_NET_BIND_SERVICE. Um processo sem essa capability ainda pode chamar bind() para escutar na porta 80. Mas o kernel vai negar a execução com EPERM (Permission Denied) se a porta for <1024 e a capability não estiver presente.
A syscall bind() ocorre, mas falha se as permissões exigidas pela capability não estiverem presentes.
Já com Seccomp o comportamento é diferente. Ele pode bloquear completamente a syscall antes mesmo de ser executada, ou ter outros comportamentos como:
Deixar passar (SCMP_ACT_ALLOW)
- A syscall é permitida e executada normalmente. No entanto, o kernel ainda pode retornar EPERM (Permission Denied) se o processo não tiver a capability necessária.
Retornar erro (SCMP_ACT_ERRNO)
- A syscall é interceptada pelo Seccomp, e o kernel retorna imediatamente um código de erro definido (por padrão, EPERM).
Encerrar o processo (SCMP_ACT_KILL)
- A syscall é interceptada e o processo é finalizado imediatamente, a syscall nem chega a ser executada.
Docker Content Trust - DCT
O Docker Content Trust (DCT) é uma funcionalidade do Docker que assegura a integridade e autenticidade das imagens de container. Ele utiliza assinaturas digitais para garantir que as imagens utilizadas no processo de build ou execução sejam provenientes de fontes confiáveis e não tenham sido alteradas.
Com o DCT habilitado, o Docker só permite a execução ou pull de imagens que estejam devidamente assinadas. Isso protege contra o uso de imagens maliciosas ou adulteradas, aumentando a segurança do ambiente de execução. O DCT é baseado no Notary, uma tecnologia da CNCF (Cloud Native Computing Foundation), que implementa o protocolo TUF (The Update Framework). Esse protocolo gerencia as assinaturas digitais e os metadados das imagens.
O DCT pode ser usado por qualquer pessoa que utilize o Docker, mas ele depende da existência de um registry para armazenar as assinaturas e os metadados das imagens. Por padrão, ele funciona com o Docker Hub ou qualquer outro registry que suporte o Notary, que é a tecnologia subjacente ao DCT.
Para ativar o DCT, defina a variável de ambiente DOCKER_CONTENT_TRUST como 1:
╼ $ export DOCKER_CONTENT_TRUST=1
Quando ativado o docker pull, docker run e docker build só funcionarão para imagens assinadas. As imagens sem assinatura ou invalidadas serão rejeitadas.
Como Assinar Imagens com Docker Content Trust
O Docker cria automaticamente as chaves de assinatura ao fazer o push de uma imagem pela primeira vez. Durante o processo, o Docker solicita uma senha para proteger a chave de root.
╼ $ sudo docker push <imagem>:<tag>
Antes de baixar ou executar uma imagem, o Docker verifica a assinatura com os metadados armazenados no servidor Notary. Ao fazer o build da imagem, podemos forçar assinatura, para isso, adicione a flag --sign ao construir imagens:
╼ $ sudo docker build --sign -t <imagem>:<tag> .
Se você já tem a imagem e quer assinar, faça:
╼ $ sudo docker trust sign <imagem>:<tag>
Trivy
O Trivy é uma ferramenta de scanner de vulnerabilidades para contêineres e sistemas de imagens de contêineres. Ele é usado para detectar vulnerabilidades conhecidas em imagens Docker, imagens OCI (Open Container Initiative) e sistemas de arquivos locais.
# Baixe o arquivo binário:
wget -P /usr/src/ https://github.com/aquasecurity/trivy/releases/download/v0.50.1/trivy_0.50.1_Linux-64bit.tar.gz
# Extraia o binário movendo ele para outro diretório:
tar -xf /usr/src/trivy_0.50.1_Linux-64bit.tar.gz -C /usr/local/bin/
# Com a instalação feita, garantiremos que está tudo certo verificando a versão:
trivy version
Version: 0.50.1
Para usar o Trivy a imagem já deve estar buildada (já tem que ter a imagem pronta para uso). Vamos construir uma:
FROM node:latest
RUN adduser fulano
USER fulano
ENV LAST_NAME=ciclano
RUN export LAST_NAME
RUN mkdir /tmp/users && echo "Docker Build" > /tmp/users/users.txt
CMD cat /tmp/users/users.txt
Agora vamos fazer o build da imagem:
docker build -t test-build -f dockerfile .
[+] Building 58.4s (8/8) FINISHED docker:default
=> [internal] load build definition from dockerfile 0.0s
=> => transferring dockerfile: 232B 0.0s
=> [internal] load metadata for docker.io/library/node:latest 4.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/4] FROM docker.io/library/node:latest@sha256:b9ccc4aca32eebf124e0ca0fd573dacffba2b9236987a1d4d2625ce3c162ecc8 52.3s
=> => resolve docker.io/library/node:latest@sha256:b9ccc4aca32eebf124e0ca0fd573dacffba2b9236987a1d4d2625ce3c162ecc8 0.0s
=> => sha256:c3978d05bc68452fec21b7e4e76399380f4e7505d7f9f034c5981678732f6e96 7.34kB / 7.34kB 0.0s
=> => sha256:71215d55680cf0ab2dcc0e1dd65ed76414e3fb0c294249b5b9319a8fa7c398e4 49.55MB / 49.55MB 10.2s
=> => sha256:3cb8f9c23302e175d87a827f0a1c376bd59b1f6949bd3bc24ab8da0d669cdfa0 24.05MB / 24.05MB 10.5s
=> => sha256:b9ccc4aca32eebf124e0ca0fd573dacffba2b9236987a1d4d2625ce3c162ecc8 1.21kB / 1.21kB 0.0s
=> => sha256:5f899db30843f8330d5a40d1acb26bb00e93a9f21bff253f31c20562fa264767 64.14MB / 64.14MB 29.4s
=> => sha256:ca75f977b1c9e9a2cc5480efe5b7df07ce38373316e30b91eeadddb6a025b8a8 2.00kB / 2.00kB 0.0s
=> => extracting sha256:71215d55680cf0ab2dcc0e1dd65ed76414e3fb0c294249b5b9319a8fa7c398e4 1.1s
=> => sha256:567db630df8d441ffe43e050ede26996c87e3b33c99f79d4fba0bf6b7ffa0213 211.14MB / 211.14MB 46.3s
=> => sha256:f4ac4e9f5ffb96287f2ff537a9cf450fc883facf1276832aac2360270cb0af2b 3.37kB / 3.37kB 10.7s
=> => sha256:375735fcaa7ab4b06a8024c6f4c7a7c809f60d45fb0b37b2969db6ffaf68d5a9 49.72MB / 49.72MB 27.0s
=> => extracting sha256:3cb8f9c23302e175d87a827f0a1c376bd59b1f6949bd3bc24ab8da0d669cdfa0 0.4s
=> => sha256:c12db77023cdbf9c549ddbe6de3ca1c22511ca898d103c91ff5552d09adecc79 2.23MB / 2.23MB 28.0s
=> => sha256:ac50344c1606288c28b458e45a6220b5a18711af76ef6f32247d86c59bc6cce1 450B / 450B 28.3s
=> => extracting sha256:5f899db30843f8330d5a40d1acb26bb00e93a9f21bff253f31c20562fa264767 1.5s
=> => extracting sha256:567db630df8d441ffe43e050ede26996c87e3b33c99f79d4fba0bf6b7ffa0213 3.9s
=> => extracting sha256:f4ac4e9f5ffb96287f2ff537a9cf450fc883facf1276832aac2360270cb0af2b 0.0s
=> => extracting sha256:375735fcaa7ab4b06a8024c6f4c7a7c809f60d45fb0b37b2969db6ffaf68d5a9 1.6s
=> => extracting sha256:c12db77023cdbf9c549ddbe6de3ca1c22511ca898d103c91ff5552d09adecc79 0.0s
=> => extracting sha256:ac50344c1606288c28b458e45a6220b5a18711af76ef6f32247d86c59bc6cce1 0.0s
=> [2/4] RUN adduser fulano 0.6s
=> [3/4] RUN export LAST_NAME 0.2s
=> [4/4] RUN mkdir /tmp/users && echo "Docker Build" > /tmp/users/users.txt 0.2s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:7e8a9189d6a292eaae3d002d9d5809685b2121ee0a5af464ed59ad083bdc241a 0.0s
=> => naming to docker.io/library/test-build 0.0s
Como no exemplo colocamos a tag test-build:latest. Podemos executar o comando trivy image com o nome da imagem para iniciar a análise.
trivy image test-build:latest
Caso você queira ver somente a saída de um nível específico, adicione a flag
--severitye escolha um ou mais níveis citados. Exemplo:--severity CRITICAL,HIGH.
O comando acima é usado para realizar uma varredura na imagem de contêiner usando a ferramenta Trivy. Esta varredura irá analisar a imagem em busca de vulnerabilidades conhecidas e gerar um relatório sobre essas vulnerabilidades. Por padrão, o Trivy classifica as vulnerabilidades em cinco níveis de severidade:
| Severidade | Descrição |
|---|---|
| Unknown | Severidade desconhecida, não determinada ou definida |
| Low | Baixa severidade, menos crítica ou explorável |
| Medium | Severidade moderada, riscos moderados para segurança |
| High | Alta severidade, risco significativo para segurança |
| Critical | Severidade crítica, risco extremamente alto |
Essa ferramenta gera um link na coluna Title apontando para uma página que fornecerá mais detalhes a respeito da vulnerabilidade. Basta clicar no link azul que ele redirecionará você para a página automaticamente. Escolher imagens de contêiner com bases mais enxutas é uma abordagem eficaz para reduzir as vulnerabilidades e melhorar a segurança do seu ambiente de contêineres.
Docker Scout
O Docker Scout é uma ferramenta oficial da Docker para análise de vulnerabilidades (assim como o Trivy), recomendações de atualização e inspeção de componentes internos de imagens de contêiner. Ele permite identificar vulnerabilidades conhecidas e verificar se há versões de base mais seguras para suas imagens, além de gerar um SBOM (Software Bill of Materials), uma lista de todos os pacotes e bibliotecas incluídos na imagem.
O Docker Scout frequentemente reporta mais vulnerabilidades que o Trivy. Como o Docker Scout recebe feeds diretamente das imagens mantidas na própria plataforma Docker, ele pode listar CVEs recém-adicionados ou relacionados a metadados que o Trivy ainda não incorporou.
O Docker Scout é um plugin do Docker CLI, e pode ser instalado facilmente em sistemas Linux.
# Crie o diretório de plugins (caso não exista)
mkdir -p ~/.docker/cli-plugins
# Baixe a versão mais recente do plugin
sudo curl -fsSL https://github.com/docker/scout-cli/releases/latest/download/docker-scout_linux_amd64 -o /usr/bin/docker-scout
# Dê permissão de execução
chmod +x /usr/bin/docker-scout
Com a instalação feita, verifique a versão:
docker scout version
Login necessário
O Docker Scout utiliza a API da Docker para comparar pacotes e consultar bancos de CVEs, portanto requer login no Docker Hub:
docker login -u <username>
Após autenticação, o plugin conseguirá enviar os metadados da imagem (manifest digest) e receber a análise completa.
Classificação de Severidade
O Docker Scout utiliza a mesma classificação padrão de CVEs:
| Severidade | Descrição |
|---|---|
| Unknown | Severidade desconhecida ou ainda não avaliada |
| Low | Vulnerabilidade de baixo risco |
| Medium | Impacto moderado na segurança |
| High | Alto risco, potencial de exploração relevante |
| Critical | Vulnerabilidade crítica, explorável com grande impacto |
Construindo uma imagem para análise
Assim como no Trivy, o Docker Scout precisa que a imagem já esteja construída. Vamos usar o mesmo exemplo:
FROM node:latest
RUN adduser fulano
USER fulano
ENV LAST_NAME=ciclano
RUN export LAST_NAME
RUN mkdir /tmp/users && echo "Docker Build" > /tmp/users/users.txt
CMD cat /tmp/users/users.txt
Construa a imagem:
docker build -t test-build -f dockerfile .
Eu vou usar uma que já tenho aqui!
Analisando vulnerabilidades com o Docker Scout
Com a imagem pronta, podemos executar o Docker Scout para gerar um panorama da segurança e da composição da imagem.
$ sudo docker-scout quickview dmarcts_parser:latest
✓ SBOM of image already cached, 128 packages indexed
i Base image was auto-detected. To get more accurate results, build images with max-mode provenance attestations.
Review docs.docker.com ↗ for more information.
Target │ dmarcts_parser:latest │ 0C 1H 1M 2L
digest │ 916b58340b26 │
Base image │ alpine:3 │ 0C 0H 0M 2L
Updated base image │ alpine:3.21 │ 0C 0H 0M 2L
│ │
What's next:
View vulnerabilities → docker scout cves dmarcts_parser:latest
View base image update recommendations → docker scout recommendations dmarcts_parser:latest
Include policy results in your quickview by supplying an organization → docker scout quickview dmarcts_parser:latest --org <organization>
Esse comando faz uma varredura inicial, mostrando:
- Quantos pacotes foram analisados (SBOM gerado);
- A imagem base e sua versão;
- Quantidade de vulnerabilidades por severidade (Críticas, Altas, Médias, Baixas);
- Sugestão de imagem base atualizada (ex:
alpine:3 → alpine:3.21).
Podemos consultar mais detalhes dos CVEs:
$ sudo docker-scout cves dmarcts_parser:latest
✓ SBOM of image already cached, 128 packages indexed
✗ Detected 3 vulnerable packages with a total of 4 vulnerabilities
## Overview
│ Analyzed Image
────────────────────┼──────────────────────────────
Target │ dmarcts_parser:latest
digest │ 916b58340b26
platform │ linux/amd64
vulnerabilities │ 0C 1H 1M 2L
size │ 137 MB
packages │ 128
## Packages and Vulnerabilities
0C 1H 0M 0L unzip 6.0-r15
pkg:apk/alpine/unzip@6.0-r15?os_name=alpine&os_version=3.22
✗ HIGH CVE-2008-0888
https://scout.docker.com/v/CVE-2008-0888
Affected range : <=6.0-r15
Fixed version : not fixed
0C 0H 1M 0L perl 5.40.3-r0
pkg:apk/alpine/perl@5.40.3-r0?os_name=alpine&os_version=3.22
✗ MEDIUM CVE-2025-40909
https://scout.docker.com/v/CVE-2025-40909
Affected range : <=5.40.3-r0
Fixed version : not fixed
0C 0H 0M 2L busybox 1.37.0-r19
pkg:apk/alpine/busybox@1.37.0-r19?os_name=alpine&os_version=3.22
✗ LOW CVE-2025-46394
https://scout.docker.com/v/CVE-2025-46394
Affected range : <=1.37.0-r19
Fixed version : not fixed
✗ LOW CVE-2024-58251
https://scout.docker.com/v/CVE-2024-58251
Affected range : <=1.37.0-r19
Fixed version : not fixed
4 vulnerabilities found in 3 packages
CRITICAL 0
HIGH 1
MEDIUM 1
LOW 2
What's next:
View base image update recommendations → docker scout recommendations dmarcts_parser:latest
O Scout exibirá uma tabela com:
- Pacote e versão afetada;
- CVE ID;
- Nível de severidade;
- Versão corrigida (quando disponível);
- E um link direto para mais detalhes da vulnerabilidade.
Você também pode filtrar por severidade específica:
docker scout cves test-build:latest --only-severity critical,high