Files
bitmessage/README.ru-RU.md
T
bitdeals c66cea2009
Build docker image and push to registry.bitdeals.org / main-build-job (push) Failing after 3m41s
feat: the daemon keeps no privilege, and the container drops what it can
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.
2026-09-09 10:29:32 +00:00

17 KiB
Raw Blame History

Общие сведения

English version: README.md

PyBitmessage — клиент P2P-протокола обмена сообщениями Bitmessage, служащий для отправки шифрованных сообщений как одному адресату, так и множеству подписчиков.

Клиент PyBitmessage, работающий демоном в docker-контейнере с включённым XML-RPC API.

Репозиторий описывает только развёртывание в docker.

Использование

Контейнер создаёт детерминированные адреса Bitmessage на основе переменной BITMESSAGE_SEED_PHRASE.

Ниже — примеры, с которых удобно начать создание контейнера.

У контейнера два порта, и они не взаимозаменяемы. 8442 — XML-RPC API: он полностью управляет демоном и не имеет TLS, поэтому задайте свои учётные данные и оставьте его на loopback. 8444 — P2P-порт Bitmessage: опубликуйте его, чтобы к узлу могли подключаться другие, или не публикуйте, и тогда узел работает только на исходящих. Внутри контейнера демон слушает оба в любом случае.

docker-compose

services:
  pybitmessage:
    build:
      context: https://git.bitdeals.org/private/bitmessage.git
      dockerfile: ./docker/Dockerfile
    image: registry.bitdeals.org/bitmessage
    environment:
      - BITMESSAGE_API_USER=CHANGE_ME
      - BITMESSAGE_API_PASSWORD=CHANGE_ME
      - BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase
      - BITMESSAGE_SEED_ADDRESSES=1
      - BITMESSAGE_TTL=172800
      - BITMESSAGE_STOPRESENDINGAFTERXDAYS=60
      - BITMESSAGE_MAXTOTALCONNECTIONS=40
    ports:
      - 127.0.0.1:8442:8442   # API — только loopback
      - 8444:8444             # P2P — уберите строку, чтобы остаться на исходящих
    volumes:
      - bitmessage:/home/bitmessage

volumes:
  bitmessage:

docker cli

docker run -d \
  -e BITMESSAGE_API_USER=CHANGE_ME \
  -e BITMESSAGE_API_PASSWORD=CHANGE_ME \
  -e BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase \
  -e BITMESSAGE_SEED_ADDRESSES=1 \
  -e BITMESSAGE_TTL=172800 \
  -e BITMESSAGE_STOPRESENDINGAFTERXDAYS=60 \
  -e BITMESSAGE_MAXTOTALCONNECTIONS=40 \
  -p 127.0.0.1:8442:8442 \
  -p 8444:8444 \
  -v bitmessage:/home/bitmessage \
  registry.bitdeals.org/bitmessage

Параметры

Образы контейнера настраиваются параметрами, передаваемыми при запуске.

Параметр Назначение
-p 127.0.0.1:8442 Порт API. Внутри контейнера демон всегда слушает 0.0.0.0, поэтому доступность определяет то, что опубликовано
-p 8444 P2P-порт Bitmessage. Необязательный: без него узел всё равно подключается к пирам сам, просто к нему подключиться нельзя. Публиковать только как 8444:8444 — см. «Замечания»
-v /home/bitmessage Каталог данных: keys.dat (личность, настройки) и messages.dat. Без него после каждого обновления это новый узел
-e BITMESSAGE_API_USER Пользователь XML-RPC API. По умолчанию: bitmessage_api_user — измените
-e BITMESSAGE_API_PASSWORD Пароль XML-RPC API. По умолчанию: bitmessage_api_password — измените, см. «Замечания»
-e BITMESSAGE_SEED_PHRASE Парольная фраза для создания детерминированных адресов. По умолчанию: генерируется заново при каждом старте, то есть адреса каждый раз другие. Используется только при BITMESSAGE_SEED_ADDRESSES больше 0
-e BITMESSAGE_SEED_ADDRESSES Количество создаваемых детерминированных адресов. По умолчанию: 0
-e BITMESSAGE_TTL Срок жизни вновь отправляемых сообщений, в секундах. По умолчанию: 172800
-e BITMESSAGE_STOPRESENDINGAFTERXDAYS Прекратить повторную отправку недоставленного сообщения через X дней. По умолчанию: 30
-e BITMESSAGE_APIVARIANT Предоставляемый API: xml или json-RPC. По умолчанию: legacy
-e BITMESSAGE_MAXTOTALCONNECTIONS Предел одновременных соединений, входящих и исходящих вместе (maxoutboundconnections равен 8, то есть это значение минус 8 — запас на входящие). По умолчанию: 200, штатное значение PyBitmessage — снижайте, если P2P-порт опубликован
-e BITMESSAGE_TRUSTED_PEER host:port единственного пира, к которому узел подключается наружу; больше ни к кому. По умолчанию: пусто — узел выбирает пиров сам. См. «Замечания»
-e BITMESSAGE_SEND_OUTGOING Подключается ли узел наружу вообще: True или False. По умолчанию: True. При False получается узел, который только принимает входящие, — центр приватного контура
-e BITMESSAGE_KNOWN_NODES Список host:port через запятую; записывается в knownnodes.dat при каждом старте вместо того, что там было. По умолчанию: пусто — файл не трогается. Заодно выключает бутстрап по DNS, см. «Замечания»
-e BITMESSAGE_WATCHDOG Перезапускать ли демона, потерявшего всех пиров: True или False. По умолчанию: True. См. «Замечания»
-e BITMESSAGE_WATCHDOG_PERIOD Секунд между проверками числа пиров. По умолчанию: 60
-e BITMESSAGE_WATCHDOG_AFTER Сколько проверок подряд без пиров нужно, чтобы вмешаться. По умолчанию: 5 — то есть пять минут при периоде по умолчанию
-e BITMESSAGE_WATCHDOG_COOLDOWN Минимальный промежуток между двумя перезапусками, в секундах. По умолчанию: 900. 0 снимает ограничение

Замечания

  • % в пароле API использовать нельзя: он ломает собственный чтец конфигурации PyBitmessage, и любой вызов API отвечает 500, притом что keys.dat выглядит правильным. Остальные символы допустимы, контейнер их экранирует.

  • Клиенты обязаны кодировать учётные данные — они попадают в http://user:password@host:port/, где @, #, / и : меняют разбор URL. Скрипты этого образа кодируют, ваш клиент должен тоже.

  • Контейнер становится здоровым, когда у демона появилось сетевое соединение, — на новом узле это несколько минут. Начальный период ожидания заодно покрывает стартовый VACUUM файла messages.dat.

  • Демона без пиров перезапускаем, молчащего — нет. Демон, потерявший всех пиров, сам обратно дорогу не находит: он может простоять на нуле соединений сколько угодно, — а только что запущенный ищет пиров напористо и находит. Поэтому контейнер присматривает за собственным демоном: раз в BITMESSAGE_WATCHDOG_PERIOD спрашивает у API число соединений и после BITMESSAGE_WATCHDOG_AFTER нулей подряд останавливает демона через его же API и поднимает заново. Контейнер при этом не пересоздаётся и никто снаружи не участвует, так что соседний узел сохраняет адреса и видит лишь моргнувший API.

    Демона, у которого API не отвечает вовсе, сторож не трогает намеренно. Это узел, застрявший на стартовом VACUUM, которого он не может закончить; перезапуск его не лечит, а лишь обрывает следующий VACUUM на середине. Проверка здоровья показывает unhealthy в обоих случаях, но вмешиваться стоит только в первый, а во втором нужен человек.

    Задайте контейнеру stop_grace_period: 90s (compose) или --stop-timeout 90 (docker run). Корректное закрытие messages.dat не укладывается в десять секунд, отпущенные Docker по умолчанию, — замерено около тринадцати на небольшой базе, и растёт вместе с файлом, — так что без этого демона убьёт SIGKILL на середине записи, ровно то, ради предотвращения чего супервизор и заведён. Сам образ это задать не может, только вызывающая сторона.

    BITMESSAGE_WATCHDOG=False выключает правило о пирах. Супервизор остаётся в любом случае: он же корректно останавливает демона по docker stop и поднимает того, кто умер сам, вместо контейнера, стоящего вокруг трупа.

  • P2P-порт публикуется только как 8444:8444. Пирам демон сообщает порт из собственной конфигурации (port в keys.dat), а не тот, в который вы его отобразили, поэтому 8555:8444 объявляет сети порт, где никого нет. Для другого порта на хосте нужен extport в keys.dat, а его этот контейнер не подставляет.

  • Свой адрес нигде не указывается. В version-сообщении демон шлёт захардкоженный 127.0.0.1, который каждый пир отбрасывает и берёт вместо него IP, увиденный на сокете, а порт — из того же сообщения. Поэтому узел с опубликованным 8444 сеть находит сама, как только он подключится наружу: ни IP, ни DNS-имя хоста задавать негде. Единственное исключение — скрытый сервис Tor, которому нужен явный onionhostname.

  • Приватному контуру нужны назначенные пиры и звезда, в которую их назначать. PyBitmessage отвергает кандидата, чья сетевая группа (для IPv4 — /16) уже представлена среди его исходящих соединений. Все контейнеры одного compose-проекта живут в одной /16, поэтому каждый узел удерживает ровно одно исходящее соединение — со случайно выбранным пиром, и контур чаще всего распадается на компоненты. Задайте одному узлу BITMESSAGE_SEND_OUTGOING=False, и он станет хабом, который только принимает (проверка смотрит лишь на исходящие, входящие она не ограничивает), остальные направьте на него через BITMESSAGE_TRUSTED_PEER=<ip-хаба>:8444 — объекты пойдут спица → хаб → спицы. Адреса задавайте IP, а не именами сервисов: проверка разбирает хост как IP, да и обмен addr между узлами оперирует IP.

  • BITMESSAGE_KNOWN_NODES — то, что делает приватный контур приватным. Узел, у которого в knownnodes.dat есть пир не из встроенного списка PyBitmessage, перестаёт спрашивать адреса у bootstrap8080.bitmessage.org; без этого даже узел с назначенным пиром при каждом старте резолвит публичный бутстрап-хост. Файл переписывается на каждом старте, поэтому во что узел верит при загрузке, определяет переменная, а не история контейнера.

  • Публикация 8444 — осознанный компромисс по безопасности. Демон работает на Python 2 и уже разбирает недоверенные данные от своих исходящих пиров, так что открытый порт эту поверхность не создаёт — он меняет то, кто может подключиться, когда и сколько их. Разбор до рукопожатия становится доступен кому угодно, а реальный риск — исчерпание ресурсов, а не выполнение кода. Держите 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, о которых просит предыдущее замечание.