Fossil SCM

Fossil's chroot feature drops root permissions based on file ownership, but since the container was built with everything-root, its HTTP hit handling children would run as whatever host-side UID/GID pair you used for file ownership. What happened next was complex. If you let the container create the repo internally, it would be owned as root, so it would drop root permissions for…root! This isn't super-bad, since Fossil is presumed secure and is double-jailed besides. The risk is, if anyone works out an RCE for Fossil, they might be able to get it to create raw sockets or do various other types of escapes despite the double-jail dance. Attaching a Docker volume brings external permisssions into the container. We were recommending a "chown 0" command on the shared volume to make it similar to the in-container case, but that opens you to the same risks above. If you ignored this and used host-side UID/GID pairs, Fossil would then be left running under IDs that didn't exist internally, which could cause assorted weirdness. We're now creating an explicit "fossil" user/group pair inside the container and recommending that Docker volumes use these IDs for copied-in files to batten down something that shouldn't've been left flapping. Updated build.wiki to cover all this.

wyoung 2022-08-14 19:42 trunk
Commit ba21bc0b8fcf754d20de4a65a5640c9f7de441eac5bcbfa9c892a3f202cf48e0
2 files changed +16 -5 +32 -9
+16 -5
--- Dockerfile
+++ Dockerfile
@@ -22,23 +22,34 @@
2222
2323
# STAGE 2: Pare that back to the bare essentials.
2424
2525
FROM scratch
2626
WORKDIR /jail
27
-COPY --from=builder /tmp/fossil /jail/bin/
27
+ENV UID 499
28
+ENV PATH "/bin:/jail/bin"
29
+COPY --from=builder /tmp/fossil bin/
2830
COPY --from=builder /bin/busybox.static /bin/busybox
2931
RUN [ "/bin/busybox", "--install", "/bin" ]
30
-RUN mkdir -m 700 dev museum \
31
- && mknod -m 600 dev/null c 1 3 \
32
- && mknod -m 600 dev/urandom c 1 9
32
+RUN mkdir -m 700 dev museum \
33
+ && mknod -m 600 dev/null c 1 3 \
34
+ && mknod -m 600 dev/urandom c 1 9 \
35
+ && echo 'root:x:0:0:Fossil Init:/:/bin/nologin' > /etc/passwd \
36
+ && echo 'root:x:0:root' > /etc/group \
37
+ && addgroup -g ${UID} fossil \
38
+ && adduser -h `pwd` -g 'Fossil User' -G fossil -u ${UID} -S fossil \
39
+ && chown -R fossil:fossil .
3340
3441
# Now we can run the stripped-down environment in a chroot jail, while
3542
# leaving open the option to debug it live via the Busybox shell.
43
+#
44
+# Implicit: We don't set USER here on purpose because we want Fossil to
45
+# start as root so it can chroot itself away inside /jail. Since that's
46
+# owned by the special fossil user, it drops root privileges for that
47
+# user, preventing exotic root-based hacks on Docker.
3648
3749
EXPOSE 8080/tcp
3850
CMD [ \
3951
"bin/fossil", "server", \
4052
"--chroot", "/jail", \
4153
"--create", \
4254
"--jsmode", "bundled", \
43
- "--user", "admin", \
4455
"museum/repo.fossil"]
4556
--- Dockerfile
+++ Dockerfile
@@ -22,23 +22,34 @@
22
23 # STAGE 2: Pare that back to the bare essentials.
24
25 FROM scratch
26 WORKDIR /jail
27 COPY --from=builder /tmp/fossil /jail/bin/
 
 
28 COPY --from=builder /bin/busybox.static /bin/busybox
29 RUN [ "/bin/busybox", "--install", "/bin" ]
30 RUN mkdir -m 700 dev museum \
31 && mknod -m 600 dev/null c 1 3 \
32 && mknod -m 600 dev/urandom c 1 9
 
 
 
 
 
33
34 # Now we can run the stripped-down environment in a chroot jail, while
35 # leaving open the option to debug it live via the Busybox shell.
 
 
 
 
 
36
37 EXPOSE 8080/tcp
38 CMD [ \
39 "bin/fossil", "server", \
40 "--chroot", "/jail", \
41 "--create", \
42 "--jsmode", "bundled", \
43 "--user", "admin", \
44 "museum/repo.fossil"]
45
--- Dockerfile
+++ Dockerfile
@@ -22,23 +22,34 @@
22
23 # STAGE 2: Pare that back to the bare essentials.
24
25 FROM scratch
26 WORKDIR /jail
27 ENV UID 499
28 ENV PATH "/bin:/jail/bin"
29 COPY --from=builder /tmp/fossil bin/
30 COPY --from=builder /bin/busybox.static /bin/busybox
31 RUN [ "/bin/busybox", "--install", "/bin" ]
32 RUN mkdir -m 700 dev museum \
33 && mknod -m 600 dev/null c 1 3 \
34 && mknod -m 600 dev/urandom c 1 9 \
35 && echo 'root:x:0:0:Fossil Init:/:/bin/nologin' > /etc/passwd \
36 && echo 'root:x:0:root' > /etc/group \
37 && addgroup -g ${UID} fossil \
38 && adduser -h `pwd` -g 'Fossil User' -G fossil -u ${UID} -S fossil \
39 && chown -R fossil:fossil .
40
41 # Now we can run the stripped-down environment in a chroot jail, while
42 # leaving open the option to debug it live via the Busybox shell.
43 #
44 # Implicit: We don't set USER here on purpose because we want Fossil to
45 # start as root so it can chroot itself away inside /jail. Since that's
46 # owned by the special fossil user, it drops root privileges for that
47 # user, preventing exotic root-based hacks on Docker.
48
49 EXPOSE 8080/tcp
50 CMD [ \
51 "bin/fossil", "server", \
52 "--chroot", "/jail", \
53 "--create", \
54 "--jsmode", "bundled", \
 
55 "museum/repo.fossil"]
56
+32 -9
--- www/build.wiki
+++ www/build.wiki
@@ -297,11 +297,11 @@
297297
The simplest method is to stop the container if it was running,
298298
then say:
299299
300300
<pre><code> $ docker cp /path/to/my-project.fossil fossil:/jail/museum/repo.fossil
301301
$ docker start fossil
302
- $ docker exec fossil chown 0 /jail/museum/repo.fossil</code></pre>
302
+ $ docker exec fossil chown -R 499 /jail/museum</code></pre>
303303
304304
That copies the local Fossil repo into the container where the server
305305
expects to find it, so that the "start" command causes it to serve from
306306
that copied-in file instead. Since it lives atop the immutable base layers, it
307307
persists as part of the container proper, surviving restarts.
@@ -315,19 +315,31 @@
315315
<tt>repo.fossil</tt>, which it almost certainly was not out on the host
316316
system. This is because there is only one repository inside this
317317
container, so we don't have to name it after the project it contains, as
318318
is traditional.
319319
320
-If you skip the following "chown" command and put "http://localhost:9999/" into
320
+If you skip the "chown" command above and put "http://localhost:9999/" into
321321
your browser, expecting to see the copied-in repo's home page, you will
322322
get an opaque "Not Found" error. This is because the user and
323323
group ID of the file will be that of your local user on the container's
324
-host machine, which won't map to anything in the container's
324
+host machine, which is unlikely to map to anything in the container's
325325
<tt>/etc/passwd</tt> and <tt>/etc/group</tt> files, effectively
326
-preventing the server from reading the copied-in repository file. You
327
-don't have to restart the server after fixing this with <tt>chmod</tt>: simply reload the
328
-browser, and Fossil will try again.
326
+preventing the server from reading the copied-in repository file.
327
+499 is the default "fossil" user ID inside the container, causing Fossil to run
328
+with that user's privileges after it enters the chroot.
329
+You don't have to restart the server after fixing this with
330
+<tt>chmod</tt>: simply reload the browser, and Fossil will try again.
331
+
332
+(Why 499? Because regular user IDs start at 500 or 1000 on most Unix
333
+type systems, leaving those below it for "system" users like this Fossil
334
+daemon owner; standard OS system users start at 0 and go upward, so we
335
+started at 500 and went down one to avoid a conflict. If you use Docker
336
+volumes, you may wish to override this at container creation time — the
337
+"run" or "create" command — by passing an option like "<tt>-e
338
+UID=501</tt>". The internal container user IDs are used for permissions
339
+on the volume, where the host's user/group IDs come into play, so
340
+syncing the in-container user ID with the host can be useful.)
329341
330342
This simple method has a problem: Docker containers are designed to be
331343
killed off at the slightest cause, rebuilt, and redeployed. If you do
332344
that with the repo inside the container, it gets destroyed, too. The
333345
solution is to replace the "run" command above with the following:
@@ -346,16 +358,17 @@
346358
347359
348360
<h3>5.2 Why Chroot?</h3>
349361
350362
A potentially surprising feature of this container is that it runs
351
-Fossil as root, which causes [./chroot.md | Fossil's chroot jail
352
-feature] to kick in. Since a Docker container is a type of über-jail
363
+Fossil as root. Since that causes [./chroot.md | Fossil's chroot jail
364
+feature] to kick in, and a Docker container is a type of über-jail
353365
already, you may be wondering why we don't either:
354366
355367
# run <tt>fossil server --nojail</tt> to skip the internal chroot; or
356
- # create a non-root user and force Docker to use that instead
368
+ # set "<tt>USER fossil</tt>" in the Dockerfile so it starts Fossil as
369
+ that user instead
357370
358371
The reason is, although this container is quite stripped-down by today's
359372
standards, it's based on the [https://www.busybox.net/BusyBox.html |
360373
surprisingly powerful Busybox project]. (This author made a living for
361374
years in the early 1990s using Unix systems that were less powerful than
@@ -366,10 +379,20 @@
366379
from accessing the Busybox features.
367380
368381
We deem this risk low since a) it's never happened, that we know of;
369382
and b) we've turned off all of the risky features like TH1 docs.
370383
Nevertheless, we believe in defense-in-depth.
384
+
385
+If you shell into the container and do a <tt>ps</tt>, you'll find
386
+<tt>fossil</tt> running as <tt>root</tt>. Why? Because that's the parent
387
+process, which may need to do rootly things like listening on port 80 or
388
+443. Fossil's chroot feature takes effect in the child processes forked
389
+off to handle each HTTP/CGI hit. This is why you can fix broken
390
+permissions with <tt>chown</tt> after the container is already running,
391
+without restarting it: each hit reevaluates the repository file
392
+permissions when deciding what user to become when dropping root
393
+privileges.
371394
372395
373396
<h3>5.3 Extracting a Static Binary</h3>
374397
375398
Our 2-stage build process uses Alpine Linux only as a build host. Once
376399
--- www/build.wiki
+++ www/build.wiki
@@ -297,11 +297,11 @@
297 The simplest method is to stop the container if it was running,
298 then say:
299
300 <pre><code> $ docker cp /path/to/my-project.fossil fossil:/jail/museum/repo.fossil
301 $ docker start fossil
302 $ docker exec fossil chown 0 /jail/museum/repo.fossil</code></pre>
303
304 That copies the local Fossil repo into the container where the server
305 expects to find it, so that the "start" command causes it to serve from
306 that copied-in file instead. Since it lives atop the immutable base layers, it
307 persists as part of the container proper, surviving restarts.
@@ -315,19 +315,31 @@
315 <tt>repo.fossil</tt>, which it almost certainly was not out on the host
316 system. This is because there is only one repository inside this
317 container, so we don't have to name it after the project it contains, as
318 is traditional.
319
320 If you skip the following "chown" command and put "http://localhost:9999/" into
321 your browser, expecting to see the copied-in repo's home page, you will
322 get an opaque "Not Found" error. This is because the user and
323 group ID of the file will be that of your local user on the container's
324 host machine, which won't map to anything in the container's
325 <tt>/etc/passwd</tt> and <tt>/etc/group</tt> files, effectively
326 preventing the server from reading the copied-in repository file. You
327 don't have to restart the server after fixing this with <tt>chmod</tt>: simply reload the
328 browser, and Fossil will try again.
 
 
 
 
 
 
 
 
 
 
 
 
329
330 This simple method has a problem: Docker containers are designed to be
331 killed off at the slightest cause, rebuilt, and redeployed. If you do
332 that with the repo inside the container, it gets destroyed, too. The
333 solution is to replace the "run" command above with the following:
@@ -346,16 +358,17 @@
346
347
348 <h3>5.2 Why Chroot?</h3>
349
350 A potentially surprising feature of this container is that it runs
351 Fossil as root, which causes [./chroot.md | Fossil's chroot jail
352 feature] to kick in. Since a Docker container is a type of über-jail
353 already, you may be wondering why we don't either:
354
355 # run <tt>fossil server --nojail</tt> to skip the internal chroot; or
356 # create a non-root user and force Docker to use that instead
 
357
358 The reason is, although this container is quite stripped-down by today's
359 standards, it's based on the [https://www.busybox.net/BusyBox.html |
360 surprisingly powerful Busybox project]. (This author made a living for
361 years in the early 1990s using Unix systems that were less powerful than
@@ -366,10 +379,20 @@
366 from accessing the Busybox features.
367
368 We deem this risk low since a) it's never happened, that we know of;
369 and b) we've turned off all of the risky features like TH1 docs.
370 Nevertheless, we believe in defense-in-depth.
 
 
 
 
 
 
 
 
 
 
371
372
373 <h3>5.3 Extracting a Static Binary</h3>
374
375 Our 2-stage build process uses Alpine Linux only as a build host. Once
376
--- www/build.wiki
+++ www/build.wiki
@@ -297,11 +297,11 @@
297 The simplest method is to stop the container if it was running,
298 then say:
299
300 <pre><code> $ docker cp /path/to/my-project.fossil fossil:/jail/museum/repo.fossil
301 $ docker start fossil
302 $ docker exec fossil chown -R 499 /jail/museum</code></pre>
303
304 That copies the local Fossil repo into the container where the server
305 expects to find it, so that the "start" command causes it to serve from
306 that copied-in file instead. Since it lives atop the immutable base layers, it
307 persists as part of the container proper, surviving restarts.
@@ -315,19 +315,31 @@
315 <tt>repo.fossil</tt>, which it almost certainly was not out on the host
316 system. This is because there is only one repository inside this
317 container, so we don't have to name it after the project it contains, as
318 is traditional.
319
320 If you skip the "chown" command above and put "http://localhost:9999/" into
321 your browser, expecting to see the copied-in repo's home page, you will
322 get an opaque "Not Found" error. This is because the user and
323 group ID of the file will be that of your local user on the container's
324 host machine, which is unlikely to map to anything in the container's
325 <tt>/etc/passwd</tt> and <tt>/etc/group</tt> files, effectively
326 preventing the server from reading the copied-in repository file.
327 499 is the default "fossil" user ID inside the container, causing Fossil to run
328 with that user's privileges after it enters the chroot.
329 You don't have to restart the server after fixing this with
330 <tt>chmod</tt>: simply reload the browser, and Fossil will try again.
331
332 (Why 499? Because regular user IDs start at 500 or 1000 on most Unix
333 type systems, leaving those below it for "system" users like this Fossil
334 daemon owner; standard OS system users start at 0 and go upward, so we
335 started at 500 and went down one to avoid a conflict. If you use Docker
336 volumes, you may wish to override this at container creation time — the
337 "run" or "create" command — by passing an option like "<tt>-e
338 UID=501</tt>". The internal container user IDs are used for permissions
339 on the volume, where the host's user/group IDs come into play, so
340 syncing the in-container user ID with the host can be useful.)
341
342 This simple method has a problem: Docker containers are designed to be
343 killed off at the slightest cause, rebuilt, and redeployed. If you do
344 that with the repo inside the container, it gets destroyed, too. The
345 solution is to replace the "run" command above with the following:
@@ -346,16 +358,17 @@
358
359
360 <h3>5.2 Why Chroot?</h3>
361
362 A potentially surprising feature of this container is that it runs
363 Fossil as root. Since that causes [./chroot.md | Fossil's chroot jail
364 feature] to kick in, and a Docker container is a type of über-jail
365 already, you may be wondering why we don't either:
366
367 # run <tt>fossil server --nojail</tt> to skip the internal chroot; or
368 # set "<tt>USER fossil</tt>" in the Dockerfile so it starts Fossil as
369 that user instead
370
371 The reason is, although this container is quite stripped-down by today's
372 standards, it's based on the [https://www.busybox.net/BusyBox.html |
373 surprisingly powerful Busybox project]. (This author made a living for
374 years in the early 1990s using Unix systems that were less powerful than
@@ -366,10 +379,20 @@
379 from accessing the Busybox features.
380
381 We deem this risk low since a) it's never happened, that we know of;
382 and b) we've turned off all of the risky features like TH1 docs.
383 Nevertheless, we believe in defense-in-depth.
384
385 If you shell into the container and do a <tt>ps</tt>, you'll find
386 <tt>fossil</tt> running as <tt>root</tt>. Why? Because that's the parent
387 process, which may need to do rootly things like listening on port 80 or
388 443. Fossil's chroot feature takes effect in the child processes forked
389 off to handle each HTTP/CGI hit. This is why you can fix broken
390 permissions with <tt>chown</tt> after the container is already running,
391 without restarting it: each hit reevaluates the repository file
392 permissions when deciding what user to become when dropping root
393 privileges.
394
395
396 <h3>5.3 Extracting a Static Binary</h3>
397
398 Our 2-stage build process uses Alpine Linux only as a build host. Once
399

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button