14.Container Image Security
Container Image Security
Pro:
Con:
simple distroless Golang Example
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
FROM golang:1.18 as build
WORKDIR /go/src/app
COPY . .
RUN go mod download
RUN go vet -v
RUN go test -v
RUN CGO_ENABLED=0 go build -o /go/bin/app
FROM gcr.io/distroless/static-debian11
COPY --from=build /go/bin/app /
CMD ["/app"]
Distroless 2.0 project - uses Alpine as a minimalistic & secure base image, and with the help of two tools, apko and melange, allows to build an application-tailored image containing only (mostly?) the necessary bits.
What is apko ?
example of apko.yaml file
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
packages:
- alpine-base
entrypoint:
command: /bin/sh -l
# optional environment configuration
environment:
PATH: /usr/sbin:/sbin:/usr/bin:/bin
Buiding the images with apko via Docker
docker run -v "$PWD":/work cgr.dev/chainguard/apko build examples/alpine-base.yaml apko-alpine:edge apko-alpine.tar
test the image with docker
$ docker load < alpine.tar
$ docker run -it apko-alpine:test
Why apko ?
where do package come from ?
why distroless ?
docker run cgr.dev/chainguard/apko version
docker run -v "$PWD":/work cgr.dev/chainguard/apko build examples/alpine-base.yaml apko-alpine:edge apko-alpine.tar
The default golang image is great! It allows you to quickly build and test your golang projects. But it has a few draw backs, it is a massive 964 MB even the slimmed down alpine based image is 327 MB, not only that but having unused binaries and packages opens you up to security flaws.
Using a multi-stage image will allow you to build smaller images by dropping all the packages used to build the binaries and only including the ones required during runtime.
# Create a builder stage
FROM golang:alpine as builder
RUN apk update
RUN apk add --no-cache git ca-certificates \
&& update-ca-certificates
COPY . .
# Fetch dependencies
RUN go mod download
RUN go mod verify
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" \
-o /go/bin/my-docker-binary
# Create clean image
FROM alpine:latest
# Copy only the static binary
COPY --from=builder /go/bin/my-docker-binary \
/go/bin/my-docker-binary
# Run the binary
ENTRYPOINT ["/go/bin/my-docker-binary"]
Great now we have an image thats 20 MB thats a 95% reduction! Remember these are production images so we use -ldflags="-w -s" to turn off debug information -w and Go symbols -s.
Now to get rid of all those unused packages. Instead of using the alpine image as our final stage we will use the scratch image which has literally nothing!
Will will take this opportunity to also create a non-root user. Add the following snippet to your builder stage
ENV USER=appuser
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "$\{UID\}" \
"$\{USER\}"
We will need to copy over the ca-certificates to the final stage, this is only required if you are making https calls and we will also need to copy over the passwd and group files to use our appuser. Finally we need get the stage to use our user.
# Copy over the necessary files
COPY --from=builder \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Use our user!
USER appuser:appuser
So finally your Dockerfile should look something like this:
# Create a builder stage
FROM golang:alpine as builder
RUN apk update
RUN apk add --no-cache git ca-certificates \
&& update-ca-certificates
ENV USER=appuser
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
COPY . .
# Fetch dependencies
RUN go mod download
RUN go mod verify
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" \
-o /go/bin/my-docker-binary
# Create clean image
FROM scratch
# Copy only the static binary
COPY --from=builder \
/go/bin/my-docker-binary \
/go/bin/my-docker-binary
COPY --from=builder \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Use our user!
USER appuser:appuser
# Run the binary
ENTRYPOINT ["/go/bin/my-docker-binary"]