Usando Imagens Base Seguras
Tags: docker, containers
Published: 2024-06-12 01:25:28

![Hello There!](https://media1.tenor.com/m/DSG9ZID25nsAAAAC/hello-there-general-kenobi.gif)

Escrevi este artigo para compartilhar um pouco do que já aprendi no [PICK](https://www.linuxtips.io/pick) da [LinuxTips](https://www.linuxtips.io/). Então, pegue sua bebida e me acompanhe.

Tudo começou quando, por vezes, as ferramentas de segurança reportavam vulnerabilidades low/mid e, quando íamos avaliar o que era essa vulnerabilidade, nós sempre acabávamos no grande acordo mental: "não é algo que a gente fez, então não tem como resolver".

Durante as aulas do PICK, conheci a Chainguard. E aí surgiu a ideia de montar este artigo para mostrar como utilizar uma imagem base segura para construir o container da minha aplicação.

Para mostrar isso, vamos containerizar uma aplicação de console bem básica de "hello world" em DotNet ao longo deste artigo, visto que o foco aqui é como montar um Dockerfile para a aplicação de forma mais segura e não a aplicação em si.

## Criando a aplicação

Assumindo que você já tenha o SDK do DotNet instalado e configurado em seu ambiente, vamos abrir nosso terminal e começar a criação do projeto. 

Vamos criar nossa aplicação usando o [template `console` da CLI do DotNet](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-new-sdk-templates#console). Faremos isso utilizando o comando a seguir:

```bash
dotnet new console -o HelloWorldApp
```

Feito isto, vamos para nosso editor de texto preferido para começar a manipular os arquivos contidos no diretório do projeto.

Com seu editor de texto aberto, vamos alterar o arquivo `Program.cs` para que ele tenha nosso "Hello World". Edite seu arquivo para que ele se pareça com o seguinte:

```csharp
namespace HelloWorldApp
{
    static class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
```

## Criando o Dockerfile

Perfeito, agora que você criou nossa aplicação (que tem potencial para hackear a NASA), é hora de criarmos nosso Dockerfile para containerizar nossa aplicação. 

_Vale lembrar que o Dockerfile precisa ficar no mesmo nível do arquivo csproj, no nosso caso, dentro do diretório `HelloWorldApp`._

Para construir nosso Dockerfile, além de utilizar imagens base seguras, vamos utilizar um conceito de organização e performance chamado [multi-stage builds](https://docs.docker.com/build/building/multi-stage/).

### Primeira etapa

Sem mais delongas, vamos para a primeira linha do nosso Dockerfile:

```bash
FROM cgr.dev/chainguard/dotnet-sdk:latest AS build
```

A imagem base que estamos utilizando possui um escopo reduzido para que apenas haja dependências que satisfaçam o uso do SDK do DotNet. 

Portanto, se comparado com o escopo de uma imagem base alpine, por exemplo, as chances de nosso container ter vulnerabilidades que não dizem respeito apenas às dependências do SDK do DotNet são bem menores. E este é o grande diferencial de utilizar as imagens base da Chainguard.

Ainda sobre a primeira linha, perceba que utilizamos um alias para identificar a etapa que será executada. Neste caso, chamamos a etapa atual de `build`.

Seguindo em frente, para que nós possamos executar nosso comando que irá compilar nossa aplicação e gerar nossa dll (`dotnet publish`), precisaremos antes declarar que nossos arquivos pertencem a um usuário não root para poderem ser compilados. Faremos isto da seguinte forma:

```bash
COPY --chown=nonroot:nonroot . /source
```

Aqui estamos utilizando o comando `COPY` para copiarmos todos os arquivos do diretório atual onde está o Dockerfile, sob as permissões de um usuário que não é root, para um diretório dentro do container chamado `source` que será utilizado posteriormente.

Por se tratar de uma imagem base segura, algumas operações (como o publish no nosso caso) requerem um pouco mais de atenção a níveis de permissão, visto que deixar coisas serem compiladas a um nível elevado levaria por água abaixo toda a segurança da imagem.

Ao final desta etapa, vamos definir nosso diretório de trabalho padrão e realizar o processo de criação da nossa dll que será direcionada para um diretório chamado `Release`. Isto será feito nas linhas a seguir:

```bash
WORKDIR /source
RUN dotnet publish --use-current-runtime --self-contained false -o Release
```

### Etapa final

Agora, nesta etapa, não precisamos mais que haja dependências relacionadas ao SDK; precisamos agora ter recursos referentes ao runtime do DotNet para executar nossa dll. Para isso, vamos utilizar a seguinte imagem base:

```bash
FROM cgr.dev/chainguard/dotnet-runtime:latest AS final
```

Após isso, vamos então partir para a definição do nosso diretório de trabalho padrão e vamos agora utilizar a grande vantagem de se utilizar o multi-stage. Como na etapa de `build` nós já geramos a nossa dll, podemos então copiar nossa dll para a etapa atual para podermos utilizá-la. Vamos fazer isso da seguinte forma:

```bash
WORKDIR /
COPY --from=build /source .
```

Perceba que no comando `COPY` estamos informando que queremos que seja copiado para o contexto raiz `.` o que foi gerado no diretório `/source` da etapa `build`. E é aí que ganhamos organização e performance em nosso Dockerfile, segmentando a criação e reutilização de artefatos.

Por fim, vamos definir qual será nosso comando principal que será executado quando nosso container for iniciado, ou seja, vamos indicar que usemos o DotNet para executar nossa dll. Fazemos isso da seguinte forma:

```bash
ENTRYPOINT ["dotnet", "Release/HelloWorldApp.dll"]
```

### Dockerfile Completo

Com tudo isso feito, nosso Dockerfile final deve se parecer com o seguinte:

```bash
FROM cgr.dev/chainguard/dotnet-sdk:latest AS build
COPY --chown=nonroot:nonroot . /source
WORKDIR /source

RUN dotnet publish --use-current-runtime --self-contained false -o Release

FROM cgr.dev/chainguard/dotnet-runtime:latest AS final
WORKDIR /
COPY --from=build /source .

ENTRYPOINT ["dotnet", "Release/HelloWorldApp.dll"]
```

## Build e Execução da Imagem

Com nosso Dockerfile criado, é hora de fazer o build da nossa imagem e ver se tudo funciona como o esperado (geralmente é aqui que tudo pega fogo). Para fazer isso, estando no mesmo diretório onde nosso Dockerfile está, vamos executar o seguinte comando:

```bash
docker build -t helloworldapp .
```

Após o build concluído, vamos para o momento mais aguardado: a execução de um container que possuirá nossa dll sendo executada. Para isso, utilize o comando:

```bash
docker run --rm helloworldapp
```

## Isso É Tudo, Pessoal

Isto conclui nossa jornada com o uso de imagens base seguras e multi-stage em Dockerfiles. Claramente, você pode se aventurar indo além, por exemplo, criando workflows no GitHub que fazem o scan do código ou do container a cada push/pull request usando ferramentas como o Snyk ou o Trivy.

Agora é com você: abuse e use o que passamos por aqui! Explore outras imagens base, tente entender mais como funcionam, tente refatorar Dockerfiles para utilizar multi-stage. Vá além!

Lembre-se: que a força esteja com você, tenha uma vida longa e próspera e não entre em pânico! Allons-y!