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.
|
||||
|
||||
+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-файла в образе кусать ему почти нечего, но и стоит он ничего.
|
||||
|
||||
+4
-6
@@ -23,14 +23,12 @@ services:
|
||||
volumes:
|
||||
- bitmessage:/home/bitmessage
|
||||
# A clean shutdown does not fit in Docker's default ten seconds; the last
|
||||
# notes in the README explain this line and the six below it. The container
|
||||
# takes away every privilege it can reach from inside, but the set a
|
||||
# healthcheck and `docker exec` run with, the writability of the root
|
||||
# filesystem and the process count are not among them -- only the caller
|
||||
# can set those, and this is what setting them looks like.
|
||||
# notes in the README explain this line and the five below it. The image
|
||||
# runs as its own unprivileged user and needs no capability at all, so the
|
||||
# drop is unconditional -- but what the root filesystem and the process
|
||||
# count allow is the caller's to set, and nobody else's.
|
||||
stop_grace_period: 90s
|
||||
cap_drop: [ALL]
|
||||
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, KILL, SETPCAP]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
|
||||
+29
-36
@@ -60,13 +60,12 @@ COPY --from=0 /usr/local/ /usr/local/
|
||||
COPY ./docker/healthy_check.py /usr/local/bin/
|
||||
COPY ./docker/seed_addr_gen.py /usr/local/bin/
|
||||
COPY ./docker/watchdog.py /usr/local/bin/
|
||||
COPY ./docker/drop_privs.py /usr/local/bin/
|
||||
COPY ./docker/run.sh /usr/local/bin/
|
||||
|
||||
# Install dependencies. gosu is gone: run.sh drops privileges through
|
||||
# drop_privs.py, which does the whole job rather than only the user and needs
|
||||
# nothing beyond the Python already here. moreutils went with it -- nothing in
|
||||
# this repository ever called any of its tools.
|
||||
# Install dependencies. gosu is gone with the last thing that needed it: the
|
||||
# container starts as the daemon's user and never changes user, so there is no
|
||||
# privilege to drop at run time. moreutils went with it -- nothing in this
|
||||
# repository ever called any of its tools.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -yq --no-install-suggests --no-install-recommends python-setuptools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -80,42 +79,36 @@ WORKDIR ${HOME}
|
||||
# Generate default config
|
||||
RUN su bitmessage -c "pybitmessage -t"
|
||||
|
||||
# Prove that the two privilege drops in run.sh do what the script leans on, in
|
||||
# the arrangement run.sh uses: the supervisor's on the outside, the daemon's on
|
||||
# the inside. This base image and the PyBitmessage clone above are both
|
||||
# unpinned, so a helper that quietly stopped working would otherwise ship as a
|
||||
# container that looks confined and is not -- the trap the sql_timeout greps in
|
||||
# the first stage avoid the same way, by failing the build instead.
|
||||
#
|
||||
# Expected: the daemon at uid 2000 with every capability set empty and
|
||||
# no_new_privs on, and the supervisor holding 00000000000001e0 -- CAP_KILL,
|
||||
# CAP_SETGID, CAP_SETUID and CAP_SETPCAP, bits 5 to 8, and nothing else.
|
||||
RUN set -eu \
|
||||
&& drop=/usr/local/bin/drop_privs.py \
|
||||
&& d="$(python "$drop" --keep=kill,setgid,setuid,setpcap -- \
|
||||
python "$drop" --user -- \
|
||||
grep -E '^(Uid|Gid|CapPrm|CapEff|CapBnd|NoNewPrivs):' /proc/self/status)" \
|
||||
&& printf '%s\n' "$d" \
|
||||
&& printf '%s\n' "$d" | grep -qE '^Uid:[[:space:]]+2000[[:space:]]+2000[[:space:]]+2000' \
|
||||
&& printf '%s\n' "$d" | grep -qE '^Gid:[[:space:]]+2000[[:space:]]+2000[[:space:]]+2000' \
|
||||
&& printf '%s\n' "$d" | grep -qE '^CapPrm:[[:space:]]+0{16}$' \
|
||||
&& printf '%s\n' "$d" | grep -qE '^CapEff:[[:space:]]+0{16}$' \
|
||||
&& printf '%s\n' "$d" | grep -qE '^CapBnd:[[:space:]]+0{16}$' \
|
||||
&& printf '%s\n' "$d" | grep -qE '^NoNewPrivs:[[:space:]]+1$' \
|
||||
&& s="$(python "$drop" --keep=kill,setgid,setuid,setpcap -- \
|
||||
grep -E '^CapBnd:' /proc/self/status)" \
|
||||
&& printf '%s\n' "$s" \
|
||||
&& printf '%s\n' "$s" | grep -qE '^CapBnd:[[:space:]]+0{13}1e0$'
|
||||
# keys.dat holds the API password and the node's private keys, and the daemon
|
||||
# writes it back at that mode anyway. Set here rather than on every start,
|
||||
# because on every start there would be no root to do it: a named volume takes
|
||||
# its first contents, and their ownership and modes, from the image.
|
||||
RUN chmod 600 keys.dat
|
||||
|
||||
# Nothing here needs a setuid or setgid bit at run time, and every one of them
|
||||
# is a way back for a daemon that has been taken over -- the more so because the
|
||||
# daemon now runs with no_new_privs, which makes them the one thing that could
|
||||
# still have raised its privileges. The privilege drop is not among them: it is
|
||||
# an ordinary script that root runs. Strip them all, then insist none is left,
|
||||
# so a package added here later cannot bring one back unnoticed.
|
||||
# is a way back to root for a daemon that has been taken over -- the more so
|
||||
# under a caller that adds no-new-privileges, which leaves them as the one thing
|
||||
# that could still have raised it. Strip them all, then insist none is left, so
|
||||
# a package added here later cannot bring one back unnoticed. Last root step in
|
||||
# the file, and it has to be: after USER below there is no chmod to be had.
|
||||
RUN find / -xdev -type f -perm /6000 -exec chmod -s {} + \
|
||||
&& [ -z "$(find / -xdev -type f -perm /6000)" ]
|
||||
|
||||
# The daemon's own user, from PID 1 onwards. Nothing in this image needs root
|
||||
# once it is built: the files it works on are its own, and the only two it
|
||||
# creates -- knownnodes.dat and messages.dat -- it creates in its home. That
|
||||
# also settles what a `docker exec` and the healthcheck below run as.
|
||||
#
|
||||
# The caller no longer has to hand any capability back after cap_drop: ALL. In
|
||||
# exchange, a bind mount over /home/bitmessage has to be owned by this uid; a
|
||||
# named volume, which is what this image is meant for, inherits it from the
|
||||
# image and needs nothing.
|
||||
USER bitmessage
|
||||
|
||||
# One line, because a wrong USER is a whole class of surprise and this is where
|
||||
# it is cheapest to find out.
|
||||
RUN [ "$(id -u)" = "$USER_UID" ] && [ "$(id -g)" = "$USER_GID" ]
|
||||
|
||||
CMD ["sh", "/usr/local/bin/run.sh"]
|
||||
|
||||
## Check PyBitmessage active network connections.
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""Drop privileges, then exec -- the one errand run.sh cannot do in shell.
|
||||
|
||||
Two shapes, and both of them are in run.sh:
|
||||
|
||||
drop_privs.py --keep=kill,setgid,setuid,setpcap -- sh /usr/local/bin/run.sh
|
||||
drop_privs.py --user -- pybitmessage -d
|
||||
|
||||
The first stays root and shrinks the capability bounding set to what starting
|
||||
and stopping the daemon takes. Nothing execed below it can hold more than that
|
||||
for the rest of the container's life. The second keeps nothing and becomes the
|
||||
daemon's own user: an empty bounding set, no capability in any set, and
|
||||
no_new_privs, so not even a setuid binary could raise it -- and after the strip
|
||||
in the Dockerfile the image has none left anyway.
|
||||
|
||||
setpriv would have been the obvious tool for this. Ubuntu 18.04 does not have
|
||||
it: the program exists in util-linux 2.31 upstream, but Debian only began
|
||||
installing it at 2.32, and this image is built on bionic because PyBitmessage is
|
||||
Python 2. gosu, which the image used before, only ever changed the user.
|
||||
|
||||
The bounding set is walked up to the kernel's own cap_last_cap rather than a
|
||||
list written down here, so a capability this image has never heard of is dropped
|
||||
just the same. Everything that fails is fatal: a privilege drop that half worked
|
||||
would leave a container looking confined and not being it.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
PR_CAPBSET_DROP = 24
|
||||
PR_SET_NO_NEW_PRIVS = 38
|
||||
|
||||
# Only the ones a caller here ever keeps. A number is accepted too, for a
|
||||
# capability this table does not name.
|
||||
CAP_NUMBERS = {
|
||||
"kill": 5,
|
||||
"setgid": 6,
|
||||
"setuid": 7,
|
||||
"setpcap": 8,
|
||||
}
|
||||
|
||||
USAGE = "usage: drop_privs.py [--user] [--keep=cap,...] -- program [args]"
|
||||
|
||||
|
||||
def die(message):
|
||||
sys.stderr.write("drop_privs: %s\n" % message)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
libc = ctypes.CDLL("libc.so.6", use_errno=True)
|
||||
|
||||
|
||||
def prctl(option, arg2):
|
||||
ctypes.set_errno(0)
|
||||
if libc.prctl(option, arg2, 0, 0, 0) != 0:
|
||||
die("prctl(%d, %d): %s"
|
||||
% (option, arg2, os.strerror(ctypes.get_errno())))
|
||||
|
||||
|
||||
def cap_last_cap():
|
||||
"""The highest capability this kernel knows.
|
||||
|
||||
Read rather than guessed, and a failure to read is fatal: a guess that came
|
||||
in low would silently leave the capabilities above it in the bounding set.
|
||||
"""
|
||||
try:
|
||||
handle = open("/proc/sys/kernel/cap_last_cap")
|
||||
except IOError as exc:
|
||||
die("cannot open /proc/sys/kernel/cap_last_cap: %s" % exc)
|
||||
try:
|
||||
try:
|
||||
return int(handle.read().strip())
|
||||
except ValueError as exc:
|
||||
die("cannot read /proc/sys/kernel/cap_last_cap: %s" % exc)
|
||||
finally:
|
||||
handle.close()
|
||||
|
||||
|
||||
def parse_keep(value):
|
||||
keep = set()
|
||||
for name in value.split(","):
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
if name in CAP_NUMBERS:
|
||||
keep.add(CAP_NUMBERS[name])
|
||||
else:
|
||||
try:
|
||||
keep.add(int(name))
|
||||
except ValueError:
|
||||
die("unknown capability %r" % name)
|
||||
return keep
|
||||
|
||||
|
||||
def main(argv):
|
||||
keep = set()
|
||||
become_user = False
|
||||
args = argv[1:]
|
||||
|
||||
while args:
|
||||
if args[0] == "--":
|
||||
args = args[1:]
|
||||
break
|
||||
elif args[0] == "--user":
|
||||
become_user = True
|
||||
args = args[1:]
|
||||
elif args[0].startswith("--keep="):
|
||||
keep |= parse_keep(args[0][len("--keep="):])
|
||||
args = args[1:]
|
||||
else:
|
||||
die("unknown option %r\n%s" % (args[0], USAGE))
|
||||
|
||||
if not args:
|
||||
die("nothing to execute\n%s" % USAGE)
|
||||
|
||||
# Before anything else, so that everything below inherits it.
|
||||
prctl(PR_SET_NO_NEW_PRIVS, 1)
|
||||
|
||||
# Dropping from the bounding set takes CAP_SETPCAP, which is still held
|
||||
# here; it does not touch the sets this process is using, so the uid change
|
||||
# below still works after every capability has gone out of the set.
|
||||
for cap in range(0, cap_last_cap() + 1):
|
||||
if cap not in keep:
|
||||
prctl(PR_CAPBSET_DROP, cap)
|
||||
|
||||
if become_user:
|
||||
uid = int(os.environ.get("USER_UID", "2000"))
|
||||
gid = int(os.environ.get("USER_GID", "2000"))
|
||||
# Groups first, then gid, then uid: each of the three needs a
|
||||
# privilege the next one gives up.
|
||||
try:
|
||||
os.setgroups([])
|
||||
os.setresgid(gid, gid, gid)
|
||||
os.setresuid(uid, uid, uid)
|
||||
except OSError as exc:
|
||||
die("cannot become %d:%d: %s" % (uid, gid, exc))
|
||||
|
||||
try:
|
||||
os.execvp(args[0], args)
|
||||
except OSError as exc:
|
||||
die("cannot execute %r: %s" % (args[0], exc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
+48
-107
@@ -118,82 +118,36 @@ esc() {
|
||||
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
|
||||
}
|
||||
|
||||
# Run something as the daemon's own user with nothing left to escalate with: no
|
||||
# capability in any set, an empty bounding set so none can ever be picked up
|
||||
# again, and no_new_privs so that a setuid binary could not help either. This
|
||||
# replaces the earlier privilege drop, which changed the user and left
|
||||
# everything else alone. What the helper does, and why it is a helper rather
|
||||
# than setpriv, is at the top of drop_privs.py.
|
||||
#
|
||||
# The daemon is the only thing in this container exposed to the network, and
|
||||
# this line is the whole of what it gets. The AppArmor profile this repository
|
||||
# carried until 2022 agrees: the two capabilities it granted served the
|
||||
# AppImage's fuse mount, and PyBitmessage itself asked for none.
|
||||
as_bitmessage() {
|
||||
/usr/bin/python /usr/local/bin/drop_privs.py --user -- "$@"
|
||||
}
|
||||
|
||||
# --- everything that needs root, and the moment root stops being needed ----
|
||||
#
|
||||
# The capabilities this block uses are needed exactly once, at startup, and only
|
||||
# by uid 0: keys.dat arrives owned by whoever a bind mount says (CAP_CHOWN), the
|
||||
# greps below read it while it is mode 600 and owned by the daemon's user
|
||||
# (CAP_DAC_OVERRIDE), and its mode is set on a file that is not ours
|
||||
# (CAP_FOWNER). Measured one capability at a time: without DAC_OVERRIDE the
|
||||
# greps fail silently and the configuration keys are appended a second time on
|
||||
# every start; without FOWNER the chmod stops the container outright.
|
||||
#
|
||||
# So the block ends by re-executing this file with a bounding set that holds
|
||||
# none of the three. What comes back keeps four capabilities: SETUID and SETGID
|
||||
# to start the daemon as its own user, KILL for the supervisor's fallback stop,
|
||||
# and SETPCAP because emptying the daemon's own bounding set takes it. Four of
|
||||
# the fourteen Docker hands a container by default, and that is all there is for
|
||||
# the rest of its life, whatever a compromised daemon manages to execute. The
|
||||
# second pass runs the preamble above again: everything it computes is either
|
||||
# read back from an exported variable or recomputed identically, and the random
|
||||
# seed phrase, which is neither, was exported on the first pass.
|
||||
if [ -z "${BM_SUPERVISED:-}" ]
|
||||
# maxtotalconnections is the only brake on a node whose P2P port (8444) is
|
||||
# published: it caps inbound sockets at the total minus maxoutboundconnections.
|
||||
# The substitution below is a no-op when the key is missing, which would ship a
|
||||
# node that looks capped and is not -- and the PyBitmessage clone in the
|
||||
# Dockerfile is unpinned, so the stock config is whatever upstream generates
|
||||
# today. Add the key rather than trust the substitution alone; line 1 is the
|
||||
# [bitmessagesettings] header the daemon reads it from.
|
||||
if ! grep -q "^maxtotalconnections = " keys.dat
|
||||
then
|
||||
# A bind mount hands keys.dat over with the host's ownership on it.
|
||||
if [ -f keys.dat ]
|
||||
then
|
||||
chown bitmessage:bitmessage keys.dat
|
||||
chmod 600 keys.dat
|
||||
fi
|
||||
sed -i "1a maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS" keys.dat
|
||||
fi
|
||||
|
||||
# maxtotalconnections is the only brake on a node whose P2P port (8444)
|
||||
# is published: it caps inbound sockets at the total minus
|
||||
# maxoutboundconnections. The substitution below is a no-op when the
|
||||
# key is missing, which would ship a node that looks capped and is not
|
||||
# -- and the PyBitmessage clone in the Dockerfile is unpinned, so the
|
||||
# stock config is whatever upstream generates today. Add the key rather
|
||||
# than trust the substitution alone; line 1 is the [bitmessagesettings]
|
||||
# header the daemon reads it from.
|
||||
if ! grep -q "^maxtotalconnections = " keys.dat
|
||||
then
|
||||
as_bitmessage sed -i "1a maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS" keys.dat
|
||||
fi
|
||||
# trustedpeer is absent from the stock keys.dat entirely, so the substitution
|
||||
# below is a no-op until the key exists -- same trap as maxtotalconnections.
|
||||
# The key is added even when the value is empty, which is how it can be taken
|
||||
# back off a node that was pinned before: safeGet returns "" and connectionpool
|
||||
# falls back to chooseConnection. That empty case is also why the anchors here
|
||||
# stop at "=" instead of "= ": with nothing to the right there is no trailing
|
||||
# space to match, and the substitution would never fire again.
|
||||
if ! grep -q "^trustedpeer =" keys.dat
|
||||
then
|
||||
sed -i "1a trustedpeer = $(esc "$BITMESSAGE_TRUSTED_PEER")" keys.dat
|
||||
fi
|
||||
|
||||
# trustedpeer is absent from the stock keys.dat entirely, so the
|
||||
# substitution below is a no-op until the key exists -- same trap as
|
||||
# maxtotalconnections. The key is added even when the value is empty,
|
||||
# which is how it can be taken back off a node that was pinned before:
|
||||
# safeGet returns "" and connectionpool falls back to chooseConnection.
|
||||
# That empty case is also why the anchors here stop at "=" instead of
|
||||
# "= ": with nothing to the right there is no trailing space to match,
|
||||
# and the substitution would never fire again.
|
||||
if ! grep -q "^trustedpeer =" keys.dat
|
||||
then
|
||||
as_bitmessage sed -i "1a trustedpeer = $(esc "$BITMESSAGE_TRUSTED_PEER")" keys.dat
|
||||
fi
|
||||
|
||||
# Set config values. Every expression is anchored to the start of the
|
||||
# line and names its key in the replacement, so no backreference is
|
||||
# involved and nothing in another section can match. With set -e a
|
||||
# failure here now stops the container instead of leaving the daemon on
|
||||
# its previous settings unnoticed -- including the case of a bind mount
|
||||
# with no keys.dat at all.
|
||||
as_bitmessage sed -i \
|
||||
# Set config values. Every expression is anchored to the start of the line and
|
||||
# names its key in the replacement, so no backreference is involved and nothing
|
||||
# in another section can match. With set -e a failure here now stops the
|
||||
# container instead of leaving the daemon on its previous settings unnoticed --
|
||||
# including the case of a bind mount with no keys.dat at all.
|
||||
sed -i \
|
||||
-e "s|^apiinterface = .*|apiinterface = 0.0.0.0|" \
|
||||
-e "s|^apivariant = .*|apivariant = $(esc "$BITMESSAGE_APIVARIANT")|" \
|
||||
-e "s|^apiusername = .*|apiusername = $(esc "$BITMESSAGE_API_USER")|" \
|
||||
@@ -207,21 +161,19 @@ then
|
||||
-e "s|^sendoutgoingconnections = .*|sendoutgoingconnections = $BITMESSAGE_SEND_OUTGOING|" \
|
||||
-e "s|^udp = .*|udp = False|" keys.dat
|
||||
|
||||
# BITMESSAGE_KNOWN_NODES pins the peers the daemon starts from, and is
|
||||
# rewritten on every start: in a private contour the seed *is* the
|
||||
# topology, and a file left over from an earlier run names nodes that
|
||||
# may no longer exist. Seeding it also switches off the DNS bootstrap
|
||||
# -- json_deserialize_knownnodes raises knownNodesActual for any peer
|
||||
# that is neither DEFAULT_NODES nor "self", and connectionpool calls
|
||||
# startBootstrappers only while that flag is down, so the node never
|
||||
# reaches bootstrap8080.bitmessage.org.
|
||||
#
|
||||
# Writing it "only when the file is missing" would have been a
|
||||
# permanent no-op: the image ships a knownnodes.dat, produced by the
|
||||
# `pybitmessage -t` run in the Dockerfile, and a named volume inherits
|
||||
# it on first use.
|
||||
if [ -n "$BITMESSAGE_KNOWN_NODES" ]
|
||||
then
|
||||
# BITMESSAGE_KNOWN_NODES pins the peers the daemon starts from, and is rewritten
|
||||
# on every start: in a private contour the seed *is* the topology, and a file
|
||||
# left over from an earlier run names nodes that may no longer exist. Seeding it
|
||||
# also switches off the DNS bootstrap -- json_deserialize_knownnodes raises
|
||||
# knownNodesActual for any peer that is neither DEFAULT_NODES nor "self", and
|
||||
# connectionpool calls startBootstrappers only while that flag is down, so the
|
||||
# node never reaches bootstrap8080.bitmessage.org.
|
||||
#
|
||||
# Writing it "only when the file is missing" would have been a permanent no-op:
|
||||
# the image ships a knownnodes.dat, produced by the `pybitmessage -t` run in the
|
||||
# Dockerfile, and a named volume inherits it on first use.
|
||||
if [ -n "$BITMESSAGE_KNOWN_NODES" ]
|
||||
then
|
||||
now="$(date +%s)"
|
||||
nodes=""
|
||||
oldifs="$IFS"
|
||||
@@ -242,18 +194,7 @@ then
|
||||
done
|
||||
IFS="$oldifs"
|
||||
printf '[%s\n]\n' "$nodes" > knownnodes.dat
|
||||
chown bitmessage:bitmessage knownnodes.dat
|
||||
chmod 600 knownnodes.dat
|
||||
fi
|
||||
|
||||
# Nothing below ever touches another user's file again, so root keeps
|
||||
# only what starting and stopping the daemon takes. The marker is what
|
||||
# stops the second pass repeating this block; the Dockerfile checks
|
||||
# what both of these two drops actually leave behind.
|
||||
BM_SUPERVISED=1
|
||||
export BM_SUPERVISED
|
||||
exec /usr/bin/python /usr/local/bin/drop_privs.py \
|
||||
--keep=kill,setgid,setuid,setpcap -- /bin/sh "$0"
|
||||
fi
|
||||
|
||||
# generate address from seed
|
||||
@@ -266,16 +207,16 @@ then
|
||||
for i in 1 2 3 4
|
||||
do
|
||||
sleep 15
|
||||
as_bitmessage /usr/bin/python /usr/local/bin/seed_addr_gen.py
|
||||
/usr/bin/python /usr/local/bin/seed_addr_gen.py
|
||||
done &
|
||||
fi
|
||||
|
||||
# --- the daemon, and the supervisor that owns it --------------------------
|
||||
#
|
||||
# This file used to end at `exec pybitmessage -d` as the daemon's user, making
|
||||
# the daemon PID 1, and it is a poor PID 1. daemonize() double-forks and parks
|
||||
# the grandfather in `while True: time.sleep(1)`; the final child then SIGTERMs
|
||||
# it to say "ready", and PID 1 drops that signal for want of a handler. Three
|
||||
# This file used to end at `exec pybitmessage -d`, which made the daemon PID 1,
|
||||
# and it is a poor PID 1. daemonize() double-forks and parks the grandfather in
|
||||
# `while True: time.sleep(1)`; the final child then SIGTERMs it to say "ready",
|
||||
# and PID 1 drops that signal for want of a handler. Three
|
||||
# things followed. The grandfather slept for ever. `docker stop` reached the
|
||||
# real daemon only as the SIGKILL ten seconds later -- which is how a startup
|
||||
# VACUUM gets cut in half and the node is then trapped retrying it. And a daemon
|
||||
@@ -305,7 +246,7 @@ daemon_running() {
|
||||
|
||||
start_daemon() {
|
||||
set +e
|
||||
as_bitmessage pybitmessage -d
|
||||
pybitmessage -d
|
||||
rc=$?
|
||||
set -e
|
||||
# 143 is the ready signal reaching the grandfather, which is this call's
|
||||
@@ -322,7 +263,7 @@ stop_daemon() {
|
||||
daemon_running || return 0
|
||||
# Through the daemon's own API, which runs doCleanShutdown: the database is
|
||||
# closed instead of being cut off mid-write.
|
||||
as_bitmessage python /usr/local/bin/watchdog.py shutdown || true
|
||||
python /usr/local/bin/watchdog.py shutdown || true
|
||||
waited=0
|
||||
while daemon_running && [ "$waited" -lt "$STOP_TIMEOUT" ]
|
||||
do
|
||||
@@ -371,7 +312,7 @@ do
|
||||
[ "$BITMESSAGE_WATCHDOG" = True ] || continue
|
||||
|
||||
set +e
|
||||
as_bitmessage python /usr/local/bin/watchdog.py peers
|
||||
python /usr/local/bin/watchdog.py peers
|
||||
verdict=$?
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user