# 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"]