DEV Community

Gabriel Batista
Gabriel Batista

Posted on

Using Secure Base Images

Hello There!

I wrote this article to share a bit of what I've learned in the PICK from LinuxTips. So, grab your drink and join me.

It all started when, sometimes, security tools reported low/mid vulnerabilities, and when we went to assess what these vulnerabilities were, we always ended up in the mental agreement: "it's not something we did, so there's no way to fix it."

During the PICK classes, I got to know Chainguard. And then the idea came up to write this article to show how to use a secure base image to build the container for my application.

To demonstrate this, we will containerize a very basic console application of "hello world" in DotNet throughout this article, as the focus here is on how to build a Dockerfile for the application in a more secure way, not the application itself.

Creating the application

Assuming you already have the DotNet SDK installed and configured in your environment, let's open our terminal and start creating the project.

We will create our application using the console template of the DotNet CLI. We will do this using the following command:

dotnet new console -o HelloWorldApp
Enter fullscreen mode Exit fullscreen mode

Once this is done, let's move to our favorite text editor to start manipulating the files contained in the project directory.

With your text editor open, let's modify the Program.cs file to have our Hello World. Edit your file to look like the following:

namespace HelloWorldApp
{
    static class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Dockerfile

Perfect, now that you have created our application (which has the potential to hack NASA), it's time to create our Dockerfile to containerize our application.

It is worth remembering that the Dockerfile needs to be at the same level as the csproj file, in our case, inside the HelloWorldApp directory.

To build our Dockerfile, in addition to using secure base images, we will use an organization and performance concept called multi-stage builds.

First stage

Without further ado, let's move to the first line of our Dockerfile:

FROM cgr.dev/chainguard/dotnet-sdk:latest AS build
Enter fullscreen mode Exit fullscreen mode

The base image we are using has a reduced scope so that there are only dependencies that satisfy the use of the DotNet SDK.

Therefore, compared to the scope of a base alpine image, for example, the chances of our container having vulnerabilities that do not pertain only to the DotNet SDK dependencies are much smaller. And this is the great advantage of using Chainguard's base images.

Still, regarding the first line, note that we used an alias to identify the stage that will be executed. In this case, we called the current stage build.

Moving on, so that we can execute our command that will compile our application and generate our dll (dotnet publish), we need to first declare that our files belong to a non-root user so they can be compiled. We will do this as follows:

COPY --chown=nonroot:nonroot . /source
Enter fullscreen mode Exit fullscreen mode

Here we are using the COPY command to copy all the files from the current directory where the Dockerfile is located, under the permissions of a non-root user, to a directory inside the container called source which will be used later.

Since it is a secure base image, some operations (such as publish in our case) require a little more attention to permission levels, since letting things be compiled at a high level would undermine the security of the image.

At the end of this stage, we will define our default working directory and carry out the process of creating our dll, which will be directed to a directory called Release. This will be done in the following lines:

WORKDIR /source
RUN dotnet publish --use-current-runtime --self-contained false -o Release
Enter fullscreen mode Exit fullscreen mode

Final stage

Now, in this stage, we no longer need dependencies related to the SDK; we now need resources related to the DotNet runtime to run our dll. For this, we will use the following base image:

FROM cgr.dev/chainguard/dotnet-runtime:latest AS final
Enter fullscreen mode Exit fullscreen mode

After this, we will proceed to define our default working directory and now use the great advantage of using multi-stage. As in the build stage, we have already generated our dll; we can now copy our dll to the current stage to use it. We will do this as follows:

WORKDIR /
COPY --from=build /source .
Enter fullscreen mode Exit fullscreen mode

Note that in the COPY command we are stating that we want what was generated in the /source directory of the build stage to be copied to the root context .. And this is where we gain organization and performance in our Dockerfile, segmenting the creation and reuse of artifacts.

Finally, we will define our main command that will be executed when our container starts, that is, we will indicate that we use DotNet to run our dll. We do this as follows:

ENTRYPOINT ["dotnet", "Release/HelloWorldApp.dll"]
Enter fullscreen mode Exit fullscreen mode

Complete Dockerfile

With all this done, our final Dockerfile should look like the following:

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"]
Enter fullscreen mode Exit fullscreen mode

Building and Running the Image

With our Dockerfile created, it is time to build our image and see if everything works as expected (this is usually where everything catches fire). To do this, being in the same directory where our Dockerfile is, we will run the following command:

docker build -t helloworldapp .
Enter fullscreen mode Exit fullscreen mode

Once the build is complete, let's move to the most awaited moment: running a container that will have our dll being executed. To do this, use the command:

docker run --rm helloworldapp
Enter fullscreen mode Exit fullscreen mode

That's All, Folks

This concludes our journey with the use of secure base images and multi-stage Dockerfiles. Clearly, you can venture further, for example, by creating GitHub workflows that scan the code or container with each push/pull request using tools like Snyk or Trivy.

Now it's up to you: abuse and use what we've covered here! Explore other base images, try to understand more how they work, try refactoring Dockerfiles to use multi-stage. Go beyond!

Remember: may the force be with you, live long and prosper, and don't panic! Allons-y!

Top comments (0)