Skip to main content

Command Palette

Search for a command to run...

[EN] Building a secure container image

Updated
4 min read
[EN] Building a secure container image
M
Started my IT career as a Technical Support at an Indonesian web hosting provider, then progressed through various roles as a Linux SysAdmin, Network Engineer, Product Designer, and DevOps Engineer. I moved to a SaaS company and since then I’ve built hands-on experience mainly with AWS and GCP and work daily with popular cloud native tools.

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:

ImageSize
Insecure292MB
Secure + Distroless Non-root27MB
Secure + Alpine14.6MB
Secure + Scratch6.72MB
8 views

Containers

Part 1 of 1

Berisi materi belajar container khususnya Docker dari awal hingga menengah.

More from this blog

Syaifuddin’s Growing Space

10 posts

A personal documentation of what I've learned. Mostly about tech-related stuff.