221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
|
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
+
-
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
+
|
$ docker exec -it -u fossil $(make container-version) sh
(That command assumes you built the container via “`make container`” and
are therefore using its versioning scheme.)
Another case where you might need to replace this bare-bones “`run`”
layer with something more functional is that you’ve installed a [server
layer with something more functional is that you’re setting up [email
alerts](./alerts.md) and need some way to integrate with the host’s
[MTA]. There are a number of alternatives in that linked document, so
for the sake of discussion, we’ll say you’ve chosen [Method
2](./alerts.md#db), which requires a Tcl interpreter and its SQLite
extension to push messages into the outbound email queue DB, presumably
bind-mounted into the container.
One way to do that is to replace STAGE 2 and 3 in the stock `Dockerfile`
with this:
```
## ---------------------------------------------------------------------
## STAGE 2: Pare that back to the bare essentials, plus Tcl.
## ---------------------------------------------------------------------
FROM alpine AS run
ARG UID=499
ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin"
COPY --from=builder /tmp/fossil /bin/
RUN set -x \
&& echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \
&& echo "fossil:x:${UID}:fossil" >> /etc/group \
&& install -d -m 700 -o fossil -g fossil log museum \
&& apk add --no-cache tcl sqlite-tcl
```
Build it and test that it works like so:
```
$ make container-run &&
echo 'puts [info patchlevel]' |
docker exec -i $(make container-version) tclsh
8.6.12
```
You can remove the installation of `busybox-static` in STAGE 1 since
Alpine is already based on BusyBox.(^We can’t do “`FROM busybox`” since
we need `apk` in this new second stage. Although this means we end up
with back-to-back Alpine stages, it isn’t redundant; the second one
starts fresh, allowing us to copy in only what we absolutely need from
the first.) You should also remove the `PATH` override in the “RUN”
stage, since it’s written for the case where everything is in `/bin`.
Another useful case to consider is that you’ve installed a [server
extension](./serverext.wiki) and you need an interpreter for that
script. The advice above won’t work except in the unlikely case that
script. The first option above won’t work except in the unlikely case that
it’s written in one of the bare-bones script interpreters that BusyBox
ships.(^BusyBox’s `/bin/sh` is based on the old 4.4BSD Lite Almquist
shell, implementing little more than what POSIX specified in 1989, plus
equally stripped-down versions of AWK and `sed`.)
Let’s say the extension is written in Python. You could inject that into
the stock container via one of “[distroless]” images. Because this will
conflict with the bare-bones “`os`” layer we create, the method is more
Let’s say the extension is written in Python. While you could handle it
the same way we do with Tcl, because Python is more popular, we have
more options. Let’s inject that into the stock container via a suitable
“[distroless]” image instead. Because this will conflict with the
bare-bones “`os`” layer we create, the method is more complicated:
complicated. Essentially, you replace everything in STAGE 2 and 3 inside
the `Dockerfile` with:
```
## ---------------------------------------------------------------------
## STAGE 2: Pare that back to the bare essentials, plus Python.
## ---------------------------------------------------------------------
FROM grc.io/distroless/python3-debian11 AS run
ARG UID=499
RUN set -x \
FROM cgr.dev/chainguard/python:latest
USER root
ARG UID=499
ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin"
COPY --from=builder /tmp/fossil /bin/
COPY --from=builder /bin/busybox.static /bin/busybox
RUN [ "/bin/busybox", "--install", "/bin" ]
RUN set -x \
&& install -d -m 700 -o fossil -g fossil log museum \
&& echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \
&& echo "fossil:x:${UID}:fossil" >> /etc/group
COPY --from=builder /tmp/fossil /bin/
&& echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \
&& echo "fossil:x:${UID}:fossil" >> /etc/group \
&& install -d -m 700 -o fossil -g fossil log museum
```
Another case is that you’re setting up [email alerts](./alerts.md) and
need some way to integrate with the host’s [MTA]. There are a number of
alternatives in that linked document, so for the sake of discussion,
we’ll say you’ve chosen Method 2, which requires a Tcl interpreter to
push messages into the outbound email queue DB, presumably bind-mounted
into the container. As of this writing, Google offers no “distroless”
container images for Tcl, but you *could* replace the `FROM` line above
with:
Build it and test that it works like so:
```
FROM alpine AS run
$ make container-run &&
RUN apk add --no-cache tcl
docker exec -i $(make container-version) python --version
3.11.2
```
Relative to the Tcl example, the change from “`alpine`” to Chainguard’s
Everything else remains the same as in the distroless Python example
Python image means we have no BusyBox environment to execute the `RUN`
because even Alpine will conflict with the way we set up core Linux
directories like `/etc` and `/tmp` in the absence of any OS image.
command with, so we have to copy the `busybox.static` binary in from
STAGE 1 and install it in this new STAGE 2 for the same reason the stock
container does.(^This is the main reason we change `USER` temporarily to
`root` here.) The compensating bonus is huge: we don’t leave a package
manager sitting around inside the image, waiting to be abused.
Beware that there’s a limit to how much the über-jail nature of
containers can save you when you go and provide a more capable OS layer
like this. For instance, you might have enabled Fossil’s [risky TH1 docs
feature][th1docrisk] along with the Tcl integration feature, which
effectively gives anyone with check-in rights on your repo the ability
to run arbitrary Tcl code on the host when that document is rendered.
The container layer should stop that script from accessing any files out
on the host that you haven’t explicitly mounted into the container’s
namespace, but it *can* still make network connections, modify the repo
DB inside the container, and who knows what else.
[distroless]: https://github.com/GoogleContainerTools/distroless
[cgimgs]: https://github.com/chainguard-images/images/tree/main/images
[distroless]: https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future
[MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
[th1docrisk]: https://fossil-scm.org/forum/forumpost/42e0c16544
### 3.3 <a id="caps"></a>Dropping Unnecessary Capabilities
The example commands above create the container with [a default set of
|