refactor: no root in this container at all
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 2m21s
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 2m21s
The chown at startup was the only thing that ever needed root here, and it served a case this project does not have: keys.dat arriving from a bind mount owned by somebody else. Every deployment uses a named volume, which takes its ownership from the image. So the chown goes, and everything that existed to survive it goes with it. USER bitmessage in the Dockerfile, from PID 1 onwards. drop_privs.py is deleted, the supervisor no longer re-executes itself with a trimmed bounding set, run.sh has no privileged prologue and no wrapper around the eight commands that used to run through one. What is left of run.sh differs from the version before any of this by eleven lines: two chowns gone, seven `gosu bitmessage` prefixes gone, one comment reworded. keys.dat gets its mode 600 at build time instead of on every start, because on every start there is now no root to set it. Its mode and ownership reach a fresh volume from the image, and every volume in service already carries them -- checked on all four live nodes: nothing under /home/bitmessage is owned by anyone but 2000. The setuid strip stays. It is two lines and it closes the one way a taken-over daemon could still have climbed. What the caller sets changes too, and in the right direction: `cap_drop: ALL` with nothing added back, where the previous commit needed seven capabilities handed in. Confinement that used to be split between the image and the caller now sits in one place. The image gives up defending itself when run with no options at all, which is the trade named in the README along with the bind mount it costs.
This commit is contained in:
@@ -161,21 +161,24 @@ Container images are configured using parameters passed at runtime.
|
||||
practical risk is resource exhaustion rather than code execution. Keep
|
||||
`BITMESSAGE_MAXTOTALCONNECTIONS` low and put memory and CPU limits on the
|
||||
container.
|
||||
- **The daemon holds no privilege of its own, and the container drops what it
|
||||
can.** `run.sh` needs root exactly once, at startup: `keys.dat` can arrive
|
||||
from a bind mount owned by anyone, so it is chowned, given mode 600 and read
|
||||
before anything else. The moment that is done the script re-executes itself
|
||||
with a bounding set of four capabilities -- `SETUID` and `SETGID` to start the
|
||||
daemon as its own user, `KILL` for the fallback stop, `SETPCAP` to drop the
|
||||
rest -- and every start of the daemon goes through `drop_privs.py`, at uid
|
||||
2000, with all four capability sets empty and `no_new_privs` on. No file in the
|
||||
image carries a setuid or setgid bit, so a daemon that has been taken over has
|
||||
nothing left to climb.
|
||||
- **There is no root in this container.** It starts as uid 2000 and stays
|
||||
there, PID 1 included: `run.sh` only ever touches files that belong to that
|
||||
user, and the two the daemon creates -- `knownnodes.dat` and `messages.dat` --
|
||||
it creates in its own home. Nothing is dropped at run time because nothing
|
||||
privileged is held in the first place. No file in the image carries a setuid
|
||||
or setgid bit either, so a daemon that has been taken over has nothing left to
|
||||
climb.
|
||||
|
||||
The price is a bind mount. `/home/bitmessage` mounted from the host has to be
|
||||
owned by uid 2000, because the container can no longer chown it on the way in;
|
||||
a named volume, which is what this image is meant for, takes the ownership and
|
||||
the modes from the image and needs nothing done to it.
|
||||
|
||||
Three things only the caller can set, and all three are worth setting.
|
||||
`cap_drop: ALL` with `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`,
|
||||
`KILL` and `SETPCAP` added back: those seven are what the startup above needs,
|
||||
and the container cannot narrow what the healthcheck and `docker exec` run
|
||||
with -- only this can. `read_only: true`, with `tmpfs` for `/tmp` and `/run`;
|
||||
the daemon writes only into its own home. And `pids_limit`, next to the memory
|
||||
and CPU limits the note above asks for.
|
||||
`cap_drop: ALL`, with nothing added back -- the image asks for no capability,
|
||||
and this is also what a healthcheck and a `docker exec` run with, which the
|
||||
container cannot narrow for itself. `read_only: true`, with `tmpfs` for `/tmp`
|
||||
and `/run`; the daemon writes only into its own home. And `pids_limit`, next
|
||||
to the memory and CPU limits the note above asks for. Add
|
||||
`no-new-privileges:true` while you are there: with no setuid file left in the
|
||||
image it has little to bite on, but it costs nothing.
|
||||
|
||||
Reference in New Issue
Block a user