# [EN] Building a secure container image

To ensure the security of an application, not only we have to keep the code safe but also keep them safe when storing and distributing it. I learned some of the best practices to do that and I’ll share them here.

# This Dockerfile Works... But Should You Use It?

```dockerfile
# Mistake #1: Using a single stage (no build separation)
# This makes the image larger than necessary
# and enlarge the attack surface, as the final image includes the build tools
# and source code, which are not needed for the final image
FROM golang:1.21-alpine
WORKDIR /app

# Mistake #2: Using ENV instead of ARG and Hardcoded sensitive information
# This is a security risk, as ENV variables are stored in the image
# and can be accessed by anyone with access to the image
# Fortunately, Docker can detect hardcoded sensitive information
# and will warn you about it.
ENV DBPASSWORD=acompletelyinsecurepassword
ENV DBNAME=postgres
ENV DBUSER=postgres

# Mistake #3: Not cleaning up unnecessary files
# Copies everything, including .git, .env, and dev files
COPY . .

# There is a good dicsussion about CGO_ENABLED=0
# https://www.reddit.com/r/golang/comments/pi97sp/what_is_the_consequence_of_using_cgo_enabled0/
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server .

# Mistake #4: Running the server as root or not specifying a user
# This is a security risk, as the server has full access to the system
# and can be used to escalate privileges
# In this example the default user for golang:1.21-alpine is root, which is a bad practice and should be avoided
# You can check by running the following command:
# docker run -it --rm golang:1.21-alpine whoami
USER root

# Mistake #5: Exposing unnecessary ports
# Exposes SSH and an additional port, which are not needed for the server
# and can be used to attack the system
EXPOSE 22 8080
CMD ["./server"]
```

# Let’s Fix This Dockerfile

```dockerfile
# Correction #1: Use multi-stage builds to reduce the image size
# and reduce the attack surface
FROM golang:1.21-alpine AS build
WORKDIR /app

# Correction #2: Use .dockerignore to exclude unnecessary files
# The copy command here is okay to do because when using multi-stage builds
# the final image will only contain the binary file and not the source code
COPY . .
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server .

# Correction #3: Use smaller base image as the final image
# You can use Alpine, Scratch, Ubuntu, or any other small base image depending on your needs
# You can also use Distroless images for more security
# Using Scratch or Distroless images will not have a shell, package manager, or any other programs in typical Linux distributions.
# So if a hacker gains access to the container, they won't be able to do much
# However, those images are not recommended for development because they are harder to debug
# since you can't exec into the container and run commands.
# They also requires more technical knowledge to use.
# So to balance between security and ease of use, I'll use Alpine here
# https://medium.com/google-cloud/alpine-distroless-or-scratch-caac35250e0b
FROM alpine:3.21.3 AS final

# Correction #4: Use ARG instead of ENV to pass build-time variables
# This is because ARG is only available during the build stage
# and the values are not stored in the final image
ARG DBPASSWORD
ARG DBNAME
ARG DBUSER

# Correction #5: Use non-root user
# However, you need to make sure that the user has the necessary permissions to run the application
USER nobody:nogroup

WORKDIR /app
# The COPY command here is to copy the binary file from the build stage
# to the final image
COPY --from=build --chown=nobody:nogroup /app/server .

# Correction #6: Only expose the necessary port
EXPOSE 8080

CMD ["/app/server"]
```

By making the image secure, usually you ended up with a smaller image. From what I’ve tried, here is the result:

| Image | Size |
| --- | --- |
| Insecure | 292MB |
| Secure + Distroless Non-root | 27MB |
| Secure + Alpine | 14.6MB |
| Secure + Scratch | 6.72MB |
