feat: the daemon keeps no privilege, and the container drops what it can
Build docker image and push to registry.bitdeals.org / main-build-job (push) Failing after 3m41s
Build docker image and push to registry.bitdeals.org / main-build-job (push) Failing after 3m41s
PyBitmessage is a Python 2 daemon parsing untrusted data from an open port, so the question is not whether it can be taken over but what is left once it has been. Until now: uid 2000, but the full fourteen capabilities Docker hands a container in the bounding set, no_new_privs off, and a root PID 1 holding all fourteen for the life of the container. run.sh now needs root exactly once. keys.dat can arrive from a bind mount owned by anyone, so it is chowned, given mode 600 and read first; the block that does it ends by re-executing the file with a bounding set of four -- SETUID and SETGID to start the daemon as its own user, KILL for the fallback stop, SETPCAP to drop the rest. Every start of the daemon then goes through setpriv rather than gosu: uid 2000, every capability set empty, no_new_privs on. gosu changed the user and left everything else alone; moreutils went with it, nothing here ever called any of its tools. No file in the image carries a setuid or setgid bit any more, which is what makes no_new_privs worth having: there is nothing left to climb. Both setpriv lines are checked at build time, in the arrangement run.sh uses, against uid, capability sets and no_new_privs read back from /proc. The base image and the PyBitmessage clone are both unpinned, so an option that quietly changed meaning would otherwise ship as a container that looks confined and is not. Two things that check caught while it was being written: Ubuntu 18.04's setpriv refuses the "all" keyword under a kernel that knows more capabilities than its headers did (40 against 37), and capability names there carry no cap_ prefix. Hence the lists written out by hand. A caller that sets cap_drop: ALL now needs seven back, not six: SETPCAP joins CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID and KILL, because dropping a bounding set takes it. The example compose file and both READMEs say so, and say what else only a caller can set: read_only with tmpfs, and pids_limit.
This commit is contained in:
@@ -160,3 +160,21 @@ docker run -d \
|
||||
кому угодно, а реальный риск — исчерпание ресурсов, а не выполнение кода.
|
||||
Держите `BITMESSAGE_MAXTOTALCONNECTIONS` низким и ограничьте контейнер по
|
||||
памяти и CPU.
|
||||
- **У демона нет собственных прав, а контейнер снимает с себя остальное.**
|
||||
`run.sh` нужен root ровно один раз, на старте: `keys.dat` может приехать из
|
||||
bind-монтирования с чужим владельцем, поэтому сначала ему меняют владельца,
|
||||
ставят режим 600 и читают. Сразу после этого скрипт перезапускает сам себя с
|
||||
bounding set из четырёх capability — `SETUID` и `SETGID`, чтобы запускать
|
||||
демона под его собственным пользователем, `KILL` для запасной остановки и
|
||||
`SETPCAP`, чтобы снять остальные, — а каждый запуск демона идёт через
|
||||
`setpriv`: uid 2000, все четыре набора capability пустые, `no_new_privs`
|
||||
включён. Ни на одном файле образа нет бита setuid или setgid, так что
|
||||
захваченному демону не по чему подниматься.
|
||||
|
||||
Три вещи может задать только вызывающая сторона, и все три стоит задать.
|
||||
`cap_drop: ALL` с возвращёнными `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`,
|
||||
`SETGID`, `KILL` и `SETPCAP`: эти семь нужны описанному выше старту, а сузить
|
||||
набор, с которым работают healthcheck и `docker exec`, контейнер сам не может
|
||||
— только это. `read_only: true` с `tmpfs` под `/tmp` и `/run`: демон пишет
|
||||
только в свой домашний каталог. И `pids_limit` — рядом с ограничениями по
|
||||
памяти и CPU, о которых просит предыдущее замечание.
|
||||
|
||||
Reference in New Issue
Block a user