[EN] Building a secure container image
![[EN] Building a secure container image](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fstock%2Funsplash%2Fyx20mpDyr2I%2Fupload%2F40e6866f72de27ec15cc7bbcee694443.jpeg&w=3840&q=75)
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?
# 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
# 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 |
![[EN] Set Up Amazon ECR Pull-Through Cache for Docker Hub](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F631dd8693e8d6f3497ad63e7%2F1ca35a5a-6303-4a86-badb-91961cf65694.jpg&w=3840&q=75)
![[EN] Track progress of MySQL Import/Export process using PV](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fstock%2Funsplash%2Fjf1EomjlQi0%2Fupload%2Fa7ee07f61c1dc2ad71b4cf2bb4523765.jpeg&w=3840&q=75)
![[EN] Lesson learned from using the wrong AWS ElastiCache Redis endpoint](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fstock%2Funsplash%2FemolMCqnKfg%2Fupload%2Fc7eb8197eb9ef632459ae6612b861cc6.jpeg&w=3840&q=75)
![[EN] My experience taking the KCNA certification](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fstock%2Funsplash%2FKXwPJtAJLfU%2Fupload%2F3deba4b52e1b8e442179a495944ccb9e.jpeg&w=3840&q=75)
![[EN] How I Stopped Copy-Pasting AWS EC2 IPs and Started SSHing Smarter](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fstock%2Funsplash%2FDXRP2PKlsFQ%2Fupload%2F8767e91ce57fa7ff90bca9149c142626.jpeg&w=3840&q=75)