Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Running Containers as Non-Root Users

Recommended resources

Olá! Bem-vindo ao Módulo 5.

Introdução

Nas últimas lições, você dominou o Docker Compose, aprendendo a orquestrar aplicações multi-contêiner e a garantir uma inicialização robusta com health checks. Agora que sabemos como construir e executar aplicações complexas de forma confiável, é hora de mudar nosso foco para a segurança.

Neste novo módulo, Segurança de Contêineres e Boas Práticas, exploraremos as técnicas essenciais para proteger suas aplicações containerizadas. Começamos com um dos pilares da segurança em TI: o princípio do menor privilégio.

O objetivo desta lição é aprender a aplicar o princípio do menor privilégio, criando e executando contêineres como usuários não-root. Por padrão, os processos dentro de um contêiner Docker são executados como o usuário root, o que representa um risco de segurança significativo. Vamos entender por que isso é um problema e como corrigi-lo de maneira eficaz, especialmente no contexto de suas aplicações .NET.


1. O Risco de Executar como root

Executar seus contêineres como root é como dar a chave mestra do prédio a um funcionário quando ele só precisa da chave do seu próprio escritório. Se algo der errado – por exemplo, um invasor explorar uma vulnerabilidade em sua aplicação – ele terá privilégios de superusuário dentro do contêiner. Isso facilita muito a escalada de ataques, a modificação do sistema de arquivos do contêiner ou a tentativa de "escapar" do contêiner para o sistema operacional hospedeiro.

A solução é seguir o princípio do menor privilégio: conceda a um processo apenas as permissões necessárias para realizar seu trabalho, e nada mais.

Top 8 Docker Best Practices for using Docker in Production

Para começar, assista a este trecho do vídeo de TechWorld with Nana. Ele explica de forma concisa por que executar contêineres com privilégios de root é uma má prática de segurança e introduz a solução de alto nível.

Assista do timestamp 14:08 a 16:02. Preste atenção à explicação sobre o risco de escalonamento de privilégios no host e à demonstração de como a diretiva USER no Dockerfile resolve isso.

Como o vídeo destaca, o risco principal é que um invasor possa usar os privilégios de root dentro do contêiner para escalar privilégios no host do Docker. Ao criar um usuário dedicado e com poucos privilégios, você limita drasticamente o que um invasor pode fazer, mesmo que ele consiga comprometer sua aplicação.


2. Como Funciona: Uma Análise dos User Namespaces

Sua preferência por entender "como as coisas funcionam" nos leva a uma das funcionalidades mais importantes do kernel Linux para a segurança de contêineres: os user namespaces.

Um namespace isola a visão que um processo tem de um recurso do sistema. Por exemplo, um PID namespace faz com que um processo veja apenas os processos dentro de seu próprio namespace, não todos os processos do host.

Da mesma forma, um user namespace isola os identificadores de usuário e grupo (UIDs e GIDs). Ele permite mapear um intervalo de UIDs dentro do namespace para um intervalo diferente de UIDs no host. Isso significa que um processo pode ter UID=0 (ou seja, ser root) dentro do seu user namespace, mas ser mapeado para um UID não privilegiado (por exemplo, UID=1654) no host. Para o kernel do host, o processo é executado como um usuário comum, mesmo que o próprio processo pense que é root.

Unsharing the user namespace for rootless containers

Este vídeo da Aqua Security oferece uma excelente demonstração de baixo nível sobre como os user namespaces funcionam, usando o comando unshare do Linux. Isso lhe dará uma compreensão fundamental do mecanismo que possibilita os "rootless containers".

Assista ao vídeo inteiro (00:00 a 07:04). Observe como um usuário não privilegiado no host pode criar um namespace, se tornar root dentro desse namespace e ganhar a capacidade de criar outros namespaces, tudo isso sem nunca ter privilégios de root no sistema host.

O vídeo ilustra perfeitamente o conceito central: isolamento de privilégios. O processo dentro do contêiner pode precisar de privilégios de root para certas operações (como instalar um pacote ou vincular-se a uma porta de baixo número), mas do ponto de vista do host, ele é apenas mais um processo de usuário sem privilégios.

A imagem a seguir visualiza esse mapeamento de UID:

Rootless Container UID Mapping
Neste diagrama, o "Container Runtime" e o "Container A" são executados como `uid=0` (root) dentro de seus próprios contextos. No entanto, graças ao User Namespace, eles são mapeados para o `uid=1600` (um usuário não privilegiado chamado Maria) no sistema host. Isso encapsula o poder do `root` dentro do contêiner, protegendo o host.

3. Implementando Contêineres Não-Root para Aplicações .NET

Agora que entendemos o "porquê" e o "como", vamos ao "o que fazer". Felizmente, para desenvolvedores .NET, a equipe do .NET simplificou muito esse processo a partir do .NET 8.

Secure your .NET cloud apps with rootless Linux Containers

Este post do blog da Microsoft é a referência definitiva para este tópico no ecossistema .NET. Ele detalha as mudanças feitas nas imagens de contêiner do .NET 8 para facilitar a execução segura como não-root.

Leia as seções "Least privilege", "Meet app", "Using app", "Switching to port 8080", e "Non-root in action". Elas cobrem o princípio, a implementação da Microsoft, como usá-lo em seu Dockerfile, a consequência importante para as portas da web e uma demonstração prática.

Com base na sua leitura, vamos destacar os pontos mais cruciais para você.

O Usuário app

A partir do .NET 8, todas as imagens de contêiner Linux oficiais da Microsoft vêm com um usuário não-root pré-configurado chamado app. Este usuário tem o UID 1654 e um diretório home em /home/app.

Isso significa que você não precisa mais criar manualmente um usuário com RUN useradd. Para usar este usuário, basta adicionar uma única linha ao final do seu Dockerfile (na fase final de runtime):

USER app

A Mudança da Porta: de 80 para 8080

Este é o efeito colateral mais importante de rodar como não-root. Em sistemas Unix-like, portas abaixo de 1024 são consideradas "privilegiadas" e exigem permissões de root para serem vinculadas. Como o usuário app não é root, ele não pode vincular-se à porta 80, que era o padrão para aplicações ASP.NET Core em contêineres até o .NET 7.

Para resolver isso, as imagens do .NET 8 agora padronizam a porta 8080. Elas fazem isso definindo uma nova variável de ambiente:
ENV ASPNETCORE_HTTP_PORTS=8080

O que isso significa para você:

  1. Seu Dockerfile não precisa mais definir ASPNETCORE_URLS ou EXPOSE 80. A imagem base já cuida disso.
  2. Ao executar seu contêiner, você deve mapear a porta do host para a porta 8080 do contêiner:
    docker run -p 8000:8080 my-aspnet-app

Segurança de Arquivos em Ação

Como o artigo da Microsoft demonstra em "Non-root in action", quando você usa a instrução COPY em um Dockerfile, os arquivos são copiados como propriedade do usuário root. Em seguida, quando você muda para USER app, o processo da sua aplicação (executado como app) não terá permissão para modificar ou excluir esses arquivos.

Isso é um benefício de segurança! Ele ajuda a impor a imutabilidade, garantindo que o código e os binários da sua aplicação não possam ser alterados em tempo de execução, mesmo que um invasor encontre uma falha.


4. Exemplo Completo de Dockerfile para .NET 8

Vamos juntar tudo em um Dockerfile multi-stage para uma aplicação ASP.NET Core, seguindo as melhores práticas.

# Estágio de build: usa o SDK completo para restaurar, construir e publicar.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copia os arquivos de projeto e restaura as dependências
COPY ["MyWebApp.csproj", "."]
RUN dotnet restore "./MyWebApp.csproj"

# Copia o resto do código-fonte e publica a aplicação
COPY . .
WORKDIR "/src/."
RUN dotnet publish "MyWebApp.csproj" -c Release -o /app/publish --no-restore

# Estágio final: usa a imagem de runtime leve do ASP.NET.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# Copia a aplicação publicada do estágio de build
COPY --from=build /app/publish .

# Define o usuário não-root 'app'. A porta 8080 já é o padrão nesta imagem.
# Nenhum 'ENV' ou 'EXPOSE' é necessário para a porta.
USER app

ENTRYPOINT ["dotnet", "MyWebApp.dll"]

Este Dockerfile é seguro e otimizado:

  • Usa um build multi-stage para manter a imagem final pequena.
  • Copia apenas os arquivos necessários para aproveitar o cache de camadas do Docker.
  • Executa a aplicação como o usuário não privilegiado app.
  • Confia na configuração padrão da porta 8080 da imagem base, que é compatível com um usuário não-root.
Teste seu entendimento!

Você recebeu o seguinte Dockerfile legado, escrito para uma aplicação .NET 6. Ele executa a aplicação como root na porta 80.

Sua tarefa é atualizá-lo para as melhores práticas de segurança do .NET 8:

  1. Mude as imagens base para as versões 8.0.
  2. Garanta que a aplicação seja executada como o usuário app.
  3. Remova qualquer configuração de porta explícita que não seja mais necessária com a imagem base do .NET 8.
# Dockerfile Legado
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MyWebApp.csproj", "."]
RUN dotnet restore "./MyWebApp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet publish "MyWebApp.csproj" -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

# Configuração explícita da porta 80
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80

ENTRYPOINT ["dotnet", "MyWebApp.dll"]
Show answer

Aqui está o Dockerfile atualizado e seguro:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyWebApp.csproj", "."]
RUN dotnet restore "./MyWebApp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet publish "MyWebApp.csproj" -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

# Define o usuário não-root. A porta 8080 é o padrão.
USER app

ENTRYPOINT ["dotnet", "MyWebApp.dll"]

Explicação das Mudanças:

  1. As imagens base sdk e aspnet foram atualizadas de 6.0 para 8.0.
  2. As linhas ENV ASPNETCORE_URLS=http://+:80 e EXPOSE 80 foram removidas. A imagem mcr.microsoft.com/dotnet/aspnet:8.0 já configura a aplicação para ouvir na porta 8080 por padrão, que é uma porta não privilegiada.
  3. A linha USER app foi adicionada antes do ENTRYPOINT para mudar do usuário padrão (root) para o usuário não privilegiado fornecido na imagem.

Conclusão

Parabéns! Você deu um passo crucial para tornar seus contêineres muito mais seguros. Ao aplicar o princípio do menor privilégio, você reduz drasticamente a superfície de ataque da sua aplicação.

Principais Pontos:

  • Executar contêineres como root é um risco de segurança que permite fácil escalonamento de privilégios em caso de comprometimento.
  • O princípio do menor privilégio dita que um processo deve ter apenas as permissões estritamente necessárias para funcionar.
  • O mecanismo subjacente que permite o isolamento de privilégios é o User Namespace do kernel Linux.
  • A partir do .NET 8, as imagens de contêiner oficiais incluem um usuário não privilegiado chamado app e padronizam a porta web para 8080.
  • Para proteger sua aplicação .NET, basta adicionar USER app ao seu Dockerfile e mapear a porta do host para a porta 8080 do contêiner.

Na próxima lição, continuaremos nossa jornada pela segurança de contêineres. Vamos aprender a escanear imagens em busca de vulnerabilidades usando ferramentas comuns (como Docker Scout ou Trivy) e a interpretar os resultados, garantindo que as dependências que você inclui em suas imagens não introduzam riscos conhecidos.