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:
+17
-16
@@ -160,21 +160,22 @@ docker run -d \
|
||||
кому угодно, а реальный риск — исчерпание ресурсов, а не выполнение кода.
|
||||
Держите `BITMESSAGE_MAXTOTALCONNECTIONS` низким и ограничьте контейнер по
|
||||
памяти и CPU.
|
||||
- **У демона нет собственных прав, а контейнер снимает с себя остальное.**
|
||||
`run.sh` нужен root ровно один раз, на старте: `keys.dat` может приехать из
|
||||
bind-монтирования с чужим владельцем, поэтому сначала ему меняют владельца,
|
||||
ставят режим 600 и читают. Сразу после этого скрипт перезапускает сам себя с
|
||||
bounding set из четырёх capability — `SETUID` и `SETGID`, чтобы запускать
|
||||
демона под его собственным пользователем, `KILL` для запасной остановки и
|
||||
`SETPCAP`, чтобы снять остальные, — а каждый запуск демона идёт через
|
||||
`drop_privs.py`: uid 2000, все четыре набора capability пустые,
|
||||
`no_new_privs` включён. Ни на одном файле образа нет бита setuid или setgid, так что
|
||||
захваченному демону не по чему подниматься.
|
||||
- **Root'а в этом контейнере нет.** Он стартует под uid 2000 и остаётся под ним,
|
||||
включая PID 1: `run.sh` работает только с файлами этого пользователя, а два,
|
||||
которые заводит сам демон — `knownnodes.dat` и `messages.dat`, — он заводит в
|
||||
своём домашнем каталоге. Ничего не понижается на старте, потому что повышенных
|
||||
прав изначально нет. Ни на одном файле образа нет бита setuid или setgid, так
|
||||
что захваченному демону не по чему подниматься.
|
||||
|
||||
Плата за это — bind-монтирование. Каталог `/home/bitmessage`, приехавший с
|
||||
хоста, должен принадлежать uid 2000: сменить владельца на входе контейнер
|
||||
больше не может. Именованный том, ради которого образ и сделан, наследует
|
||||
владельца и режимы от образа, и делать с ним ничего не надо.
|
||||
|
||||
Три вещи может задать только вызывающая сторона, и все три стоит задать.
|
||||
`cap_drop: ALL` с возвращёнными `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`,
|
||||
`SETGID`, `KILL` и `SETPCAP`: эти семь нужны описанному выше старту, а сузить
|
||||
набор, с которым работают healthcheck и `docker exec`, контейнер сам не может
|
||||
— только это. `read_only: true` с `tmpfs` под `/tmp` и `/run`: демон пишет
|
||||
только в свой домашний каталог. И `pids_limit` — рядом с ограничениями по
|
||||
памяти и CPU, о которых просит предыдущее замечание.
|
||||
`cap_drop: ALL`, ничего не возвращая: образу не нужна ни одна capability, и
|
||||
этот же набор получают healthcheck и `docker exec`, а сузить его сам контейнер
|
||||
не может. `read_only: true` с `tmpfs` под `/tmp` и `/run`: демон пишет только в
|
||||
свой домашний каталог. И `pids_limit` — рядом с ограничениями по памяти и CPU,
|
||||
о которых просит предыдущее замечание. Заодно `no-new-privileges:true`: без
|
||||
единого setuid-файла в образе кусать ему почти нечего, но и стоит он ничего.
|
||||
|
||||
Reference in New Issue
Block a user