Files
bitmessage/docker/Dockerfile
T
bitdeals 2f1b2afcb4
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 1m26s
fix: drop privileges with a helper of our own, not setpriv
Ubuntu 18.04 does not ship setpriv. The program exists in util-linux 2.31,
but Debian only began installing it at 2.32, and this image is on bionic
because PyBitmessage is Python 2. The build assertion added in the last
commit caught it, which is what it is for: `/bin/sh: 1: setpriv: not found`,
exit 127, no image pushed.

drop_privs.py does the same work with what the image already has. It sets
no_new_privs, drops the bounding set with PR_CAPBSET_DROP while CAP_SETPCAP
is still held, then optionally becomes the daemon's user. Two shapes, both in
run.sh: keep four capabilities and stay root, for the supervisor; keep none
and become uid 2000, for the daemon and everything run on its behalf.

It is better than setpriv would have been in one respect. The bounding set is
walked up to the kernel's own cap_last_cap instead of a list of names, so a
capability this image has never heard of goes too -- and the "-all" spelling
that bionic's setpriv refuses under a newer kernel is not needed at all.

Verified in a user namespace, in the arrangement run.sh uses: the outer drop
leaves 00000000000001e0, the inner leaves every capability set at zero with
no_new_privs set. The build assertion checks the same two things.
2026-09-09 12:33:18 +00:00

128 lines
5.7 KiB
Docker

# A container for PyBitmessage daemon
FROM ubuntu:bionic
SHELL ["/bin/bash", "-exo", "pipefail", "-c"]
# Install dependencies. update and install share a layer on purpose: split
# across two, a cached update feeds install package lists that may be months
# stale, and the install then fails or pulls something unintended.
RUN apt-get update \
&& apt-get install -yq --no-install-suggests --no-install-recommends \
build-essential libcap-dev libssl-dev \
python-all-dev python-msgpack python-pip python-setuptools \
git
## Do not use cache when building next layers of the image.
ARG NOCACHE=0
WORKDIR /root/PyBitmessage
RUN git clone https://github.com/Bitmessage/PyBitmessage .
# Install
RUN pip2 install jsonrpclib .
# Raise the SQL-thread startup timeout from the stock 60 s.
#
# PyBitmessage kills the daemon outright if the SQL thread is not ready within
# sql_timeout seconds (class_objectProcessor.py -> os._exit(1)). The startup
# VACUUM of a messages.dat that has grown to a few hundred MB does not fit in
# 60 s, and since the process dies mid-VACUUM lastvacuumtime is never updated,
# so every later start retries the same doomed VACUUM and the node never comes
# back. Measured: 26 s for a 264 MB database on an idle host, and the last
# start that did survive used 36 s of the 60.
#
# The greps are load-bearing: the clone above is unpinned, so if upstream ever
# moves or renames the constant, a silent no-op sed would ship an image that
# looks fixed and is not. Fail the build instead. The .pyc is refreshed because
# at runtime /usr/local is root-owned while the daemon runs as bitmessage, so a
# stale one can only be recompiled to memory on every start.
RUN f=/usr/local/lib/python2.7/dist-packages/pybitmessage/helper_sql.py \
&& grep -q '^sql_timeout = 60$' "$f" \
&& sed -i 's/^sql_timeout = 60$/sql_timeout = 600/' "$f" \
&& grep -q '^sql_timeout = 600$' "$f" \
&& rm -f "${f}c" \
&& python -c "import py_compile; py_compile.compile('$f')"
FROM ubuntu:bionic
# 8442 is the XML-RPC API (keep it on loopback), 8444 the Bitmessage P2P port.
# The daemon listens on both regardless; publishing 8444 is what makes the node
# reachable for inbound peers.
EXPOSE 8442/tcp
EXPOSE 8444/tcp
ENV USER_UID=2000
ENV USER_GID=2000
ENV HOME=/home/bitmessage
ENV BITMESSAGE_HOME=${HOME}
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.
RUN apt-get update \
&& apt-get install -yq --no-install-suggests --no-install-recommends python-setuptools \
&& rm -rf /var/lib/apt/lists/*
# Create a user
RUN addgroup --gid $USER_GID bitmessage ;\
useradd --uid $USER_UID --gid $USER_GID --skel /dev/null --create-home --home-dir $HOME bitmessage
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$'
# 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.
RUN find / -xdev -type f -perm /6000 -exec chmod -s {} + \
&& [ -z "$(find / -xdev -type f -perm /6000)" ]
CMD ["sh", "/usr/local/bin/run.sh"]
## Check PyBitmessage active network connections.
## The start period covers the startup VACUUM of messages.dat, which takes tens
## of seconds once the database reaches a few hundred MB; without it the
## container reports unhealthy for that whole window on every restart.
HEALTHCHECK --retries=0 --interval=15s --start-period=180s \
CMD ["python", "/usr/local/bin/healthy_check.py"]