ariadne.space/content/blog/introducing-witchery-tools-...

72 lines
6.2 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

---
title: "introducing witchery: tools for building distroless images with alpine"
date: "2021-09-09"
coverImage: "Screen-Shot-2021-09-09-at-7.37.24-AM.png"
---
As I noted [in my last blog](https://ariadne.space/2021/09/07/bits-relating-to-alpine-security-initiatives-in-august/), I have been working on a set of tools which enable the building of so-called "distroless" images based on Alpine.  These tools have now evolved to a point where they are usable for testing in lab environments, thus I am happy to announce [the witchery project](https://github.com/kaniini/witchery).
For the uninitiated, a "distroless" image is one which contains _only_ the application and its dependencies.  This has some desirable qualities: since the image is only the application and its immediate dependencies, there is less attack surface to worry about.  For example, a simple hello-world application built with witchery clocks in at 619kB, while that same hello-world application deployed on `alpine:3.14` clocks in at 5.6MB.  There are also drawbacks: a distroless image typically does not include a package manager, so there is generally no ability to add new packages to a distroless image.
As for why it's called witchery: we are using Alpine's package manager in new ways to perform truly deep magic.  The basic idea behind witchery is that you use it to stuff your application into an `.apk` file, and then use `apk` to install _only_ that `.apk` and its dependencies into a rootfs: no `alpine-base`, no `apk-tools`, no `busybox` (though witchery allows you to install those things if you want them).
## Deploying an an example application with witchery
For those who want to see the source code without commentary, you [can find the `Dockerfile` for this example on the witchery GitHub repo](https://github.com/kaniini/witchery/blob/master/examples/hello-world/Dockerfile).  For everyone else, I am going to try to break down what each part is doing, so that you can hopefully understand how it all fits together. We will be looking at the `Dockerfile` in the `hello-world` example.
The first thing the reader will likely notice is that Docker images built with witchery are done in three stages.  First, you build the application itself, then you use witchery to build what will become the final image, and finally, you copy that image over to a blank filesystem.
FROM alpine:3.14 AS build
WORKDIR /root
COPY . .
RUN apk add --no-cache build-base && gcc -o hello-world hello-world.c
The first stage to build the application is hopefully self explanatory, and is aptly named `build`.  We fetch the `alpine:3.14` image from Dockerhub, then install a compiler (`build-base`) and finally use `gcc` to build the application.
The second stage has a few steps to it, that I will split up so that its easier to follow along.
FROM kaniini/witchery:latest AS witchery
First, we fetch the `kaniini/witchery:latest` image, and name it `witchery`.  This image contains `alpine-sdk`, which is needed to make packages, and the witchery tools which drive the `alpine-sdk` tools, such as `abuild`.
RUN adduser -D builder && addgroup builder abuild
USER builder
WORKDIR /home/builder
Anybody who is familiar with `abuild` will tell you that it cannot be used as root.  Accordingly, we create a user for running `abuild`, and add it to the `abuild` group.  We then tell Docker that we want to run commands as this new user, and do so from its home directory.
COPY --from=build /root/hello-world .
RUN mkdir -p payloadfs/app && mv hello-world payloadfs/app/hello-world
RUN abuild-keygen -na && fakeroot witchery-buildapk -n payload payloadfs/ payloadout/
The next step is to package our application.  The first step in doing so involves copying the application from our `build` stage.  We ultimately want the application to wind up in `/app/hello-world`, so we make a directory for the package filesystem, then move the application into place.  Finally, we generate a signing key for the package, and then generate a signed `.apk` for the application named `payload`.
At this point, we have a signed `.apk` package containing our application, but how do we actually build the image?  Well, just as we drove `abuild` with `witchery-buildapk` to build the `.apk` package and sign it, we will have `apk` build the image for us.  But first, we need to switch back to being root:
USER root
WORKDIR /root
Now that we are root again, we can generate the image.  But first, we need to add the signing key we generated in the earlier step to `apk`'s trusted keys.  To do that, we simply copy it from the builder user's home directory.
RUN cp /home/builder/.abuild/\*.pub /etc/apk/keys
And finally, we build the image.  Witchery contains a helper tool, `witchery-compose` that makes doing this with `apk` really easy.
RUN witchery-compose -p ~builder/payloadout/payload\*.apk -k /etc/apk/keys -X http://dl-cdn.alpinelinux.org/alpine/v3.14/main /root/outimg/
In this case, we want `witchery-compose` to grab the application package from `~builder/payloadout/payload*.apk`.  We use a wildcard there because we don't know the full filename of the generated package.  There are options that can be passed to `witchery-buildapk` to allow you to control all parts of the `.apk` package's filename, so you don't necessarily have to do this.  We also want `witchery-compose` to use the system's trusted keys for validating signatures, and we want to pull dependencies from an Alpine mirror.
Once `witchery-compose` finishes, you will have a full image in `/root/outimg`.  The final step is to copy that to a new blank image.
FROM scratch
CMD \["/app/hello-world"\]
COPY --from=witchery /root/outimg/ .
And that's all there is to it!
## Things left to do
There are still a lot of things left to do.  For example, we might want to implement layers that users can build from when deploying their apps, like one containing `s6` for example.  We also don't have a great answer for applications written in things like Python yet, so far this only works well for programs that are compiled in the traditional sense.
But its a starting point none the less.  I'll be writing more about witchery over the coming months as the tools evolve into something even more powerful.  This is only the beginning.