A bug in a small shop's web server
A safe container image is small, pinned to one version, runs as an ordinary user and holds no secrets. Each of these limits what an attacker can do if the program inside is broken.
Somebody finds a bug in the program that serves a shop's pages. They can now make that program do things.
What they can reach next depends on who the program runs as, what else is in the image, and what is written in it.
By the end of this lesson
Note
I can say what makes an image safe to ship and fix an unsafe Dockerfile.
Key words
| Word | What it means | Example |
|---|---|---|
| Root | the user who may change anything on the system | uid=0 |
| Non-root user | an ordinary user with limited rights | |
| Pinned tag | an image tag that names one exact version | nginx:1.27-alpine |
| Secret | a password or key that only the program should know | |
| Attack surface | everything in an image that an attacker could use |
The cells students fill in are left blank in this preview.
Run as a user who can do little
Unless told otherwise, a container's program runs as root. Root may change any file the container can write to, including a mounted volume.
A non-root user may not. If the program is attacked, the damage stays smaller. Compare the two main services of your lab.
Who does web run as?
docker exec web idShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Who does cache run as?
docker exec cache idShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Let cache try to write in /etc
docker exec cache touch /etc/probeShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Let web try the same
docker exec web touch /etc/probeShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Making a user in a Dockerfile (our example)
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN adduser -D -u 10001 shop
USER shop
CMD ["python", "app.py"]
A common mistake
Warning
Many people think root inside a container does not matter, because the container is walled off. Walls have bugs, and a mounted volume is real data. Add a user and switch to it with USER, as the lab's cache does.
Less in the image means less to attack
Every program in an image is one more thing that can have a flaw. Every tool in it is one more thing an attacker can use.
A slim base such as an Alpine image carries far less. The tag decides which version you get. A tag such as latest moves, so a rebuild next month may be a different image.
Worked example: two tags of one image
| Tag | Size on our test machine | What it means |
|---|---|---|
| nginx:1.27 | 284 MB | a full system underneath |
| nginx:1.27-alpine | 76.8 MB | a small system underneath |
| nginx:latest | not measured | whatever is newest when you build |
The cells students fill in are left blank in this preview.
Which tag does cache pin?
docker inspect --format '{{.Config.Image}}' cacheShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A secret written into an image stays there
An image is shared. It is pushed to a registry, copied to servers and handed to colleagues. Whatever is in a layer goes with it.
A password in a Dockerfile is in the layers for good, even if a later step deletes the file. Keep secrets out of the image and give them to the container when it starts.
A Dockerfile holding a secret (do not copy)
FROM alpine:3.20
ENV API_KEY=abc123-not-real
RUN echo hello
Worked example: where a secret showed up
| How it was set | Where we read it back | What it printed |
|---|---|---|
| ENV in the Dockerfile | docker history --no-trunc | the line ENV API_KEY=abc123-not-real |
| -e when the container started | docker inspect, Config.Env | DB_PASSWORD=hunter2 in the list |
| Neither | nothing in the image or the settings | a secret handed over at run time instead |
The cells students fill in are left blank in this preview.
Read everything cache was told
docker inspect --format '{{json .Config.Env}}' cacheShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Going further
Take the Dockerfile with a secret in it. Rewrite it so that it pins its tag, runs as a user and holds no secret.
Then run the three functions of the assignment over the old version and your new one. The new one should come back clean.