Files
bitmessage/README.md
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

182 lines
11 KiB
Markdown

# Intro
> Русская версия: [README.ru-RU.md](README.ru-RU.md)
[PyBitmessage](https://bitmessage.org/) is a client of the Bitmessages P2P communication protocol used to send encrypted messages to another person or to many subscribers.
PyBitmessage client running as a daemon in docker container with XML-RPC API enabled.
This repository covers the docker deployment only.
# Usage
The container generates a Bitmessage Deterministic Addresses based on a `BITMESSAGE_SEED_PHRASE` variable.
Here are some example snippets to help you get started creating a container.
The container has two ports and they are not interchangeable. **8442** is the
XML-RPC API: it controls the daemon completely and has no TLS, so set your own
credentials and keep it on loopback. **8444** is the Bitmessage P2P port:
publish it to let other nodes connect in, leave it unpublished to stay
outbound-only. The daemon listens on both inside the container either way.
## docker-compose
```yaml
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 only
- 8444:8444 # P2P — omit this line to stay outbound-only
volumes:
- bitmessage:/home/bitmessage
volumes:
bitmessage:
```
## docker cli
```sh
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
```
# Parameters
Container images are configured using parameters passed at runtime.
|Parameter|Function|
|:--------|:-------|
|-p 127.0.0.1:8442|API port. The daemon always binds `0.0.0.0` inside the container, so what you publish decides who reaches it|
|-p 8444|Bitmessage P2P port. Optional: without it the node still connects out to peers, it just cannot be connected to. Must be published as `8444:8444` — see Notes|
|-v /home/bitmessage|Data directory: `keys.dat` (identity, settings) and `messages.dat`. Without it the node is a new node after every update|
|-e BITMESSAGE_API_USER|XML-RPC API user. Default: `bitmessage_api_user` — change it|
|-e BITMESSAGE_API_PASSWORD|XML-RPC API password. Default: `bitmessage_api_password` — change it, see Notes|
|-e BITMESSAGE_SEED_PHRASE|Create Deterministic Addresses password. Default: regenerated on every start, giving different addresses each time. Only used when `BITMESSAGE_SEED_ADDRESSES` is above `0`|
|-e BITMESSAGE_SEED_ADDRESSES|Number of Deterministic Addresses to generate. Default: `0`|
|-e BITMESSAGE_TTL|The expiration of newly send messages, in seconds. Default: `172800`|
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Stop resending unreceived message after X days. Default: `30`|
|-e BITMESSAGE_APIVARIANT|provides xml or json-RPC API. Default: `legacy`|
|-e BITMESSAGE_MAXTOTALCONNECTIONS|Cap on all connections at once, inbound and outbound together (`maxoutboundconnections` is 8, so this minus 8 is the inbound headroom). Default: `200`, the PyBitmessage stock value — lower it when the P2P port is published|
|-e BITMESSAGE_TRUSTED_PEER|`host:port` of the one peer this node may connect out to; it dials nothing else. Default: empty — the node chooses its own peers. See Notes|
|-e BITMESSAGE_SEND_OUTGOING|Whether the node dials out at all, `True` or `False`. Default: `True`. `False` gives a node that only accepts inbound connections — the hub of a private contour|
|-e BITMESSAGE_KNOWN_NODES|Comma-separated `host:port` list, written into `knownnodes.dat` on every start in place of whatever was there. Default: empty — the file is left as it is. Also switches the DNS bootstrap off, see Notes|
|-e BITMESSAGE_WATCHDOG|Whether to restart a daemon that has lost every peer, `True` or `False`. Default: `True`. See Notes|
|-e BITMESSAGE_WATCHDOG_PERIOD|Seconds between peer checks. Default: `60`|
|-e BITMESSAGE_WATCHDOG_AFTER|How many peerless checks in a row it takes to act. Default: `5`, so five minutes at the default period|
|-e BITMESSAGE_WATCHDOG_COOLDOWN|Floor between two restarts, in seconds. Default: `900`. `0` removes it|
# Notes
- `%` cannot be used in the API password: it breaks PyBitmessage's own config
reader, and every API call then returns `500` while `keys.dat` looks correct.
Other characters are fine, the container escapes them.
- Clients must percent-encode the credentials — they go into
`http://user:password@host:port/`, where `@`, `#`, `/` and `:` change how the
URL parses. The scripts in this image do; yours has to as well.
- The container turns healthy once the daemon has a network connection, which
on a new node takes a few minutes. The start period also covers the startup
`VACUUM` of `messages.dat`.
- **A peerless daemon is restarted; a silent one is not.** A daemon that has
lost every peer does not find its way back — it can sit at zero connections
for as long as you leave it — while one that has just started dials hard and
does. So the container supervises its own daemon: it asks the API for the
connection count every `BITMESSAGE_WATCHDOG_PERIOD`, and after
`BITMESSAGE_WATCHDOG_AFTER` answers of zero in a row it stops the daemon
through its own API and starts it again. The *container* is not restarted and
nothing outside it is involved, so a node that depends on this one keeps its
addresses and only sees the API blink.
A daemon whose API does not answer at all is left alone, deliberately. That is
the node trapped retrying a startup `VACUUM` it cannot finish, restarting does
not cure it, and restarting anyway cuts the next `VACUUM` in half too. The
healthcheck reports both cases as unhealthy; only one of them is something to
do anything about, and a person has to look at the other.
**Give the container `stop_grace_period: 90s`** (compose) or
`--stop-timeout 90` (`docker run`). Closing `messages.dat` properly takes
longer than Docker's default ten seconds — measured at about thirteen on a
small database, and it grows with the file — so without this the daemon is
SIGKILLed mid-write, which is the very thing the supervisor is there to
prevent. An image cannot set this for itself; only the caller can.
`BITMESSAGE_WATCHDOG=False` turns the peer rule off. The supervisor stays
either way — it is also what stops the daemon cleanly on `docker stop`, and
what restarts one that died outright rather than leaving the container up
around a corpse.
- **The P2P port must be published as `8444:8444`.** The daemon tells peers the
port from its own config (`port` in `keys.dat`), not the port you mapped it
to, so `8555:8444` advertises a port nobody can reach. A different host port
needs `extport` in `keys.dat`, which this container does not template.
- **Your own address is never configured.** The version message carries a
hardcoded `127.0.0.1` that every peer discards in favour of the IP it sees on
the socket, and it takes the port from that same message. So a node with 8444
published is found by the network on its own, as soon as it connects out —
there is no host IP or DNS name to set anywhere. The one exception is a Tor
hidden service, which needs an explicit `onionhostname`.
- **A private contour needs its peers pinned, and a star to pin them into.**
PyBitmessage refuses a candidate whose network group (the /16 for IPv4) is
already represented among its outbound connections. Every container of one
compose project lives in a single /16, so each node keeps exactly one outbound
connection, to a peer it picked at random — which as often as not leaves the
contour split into components. Give one node `BITMESSAGE_SEND_OUTGOING=False`
so it becomes a hub that only accepts (the check looks at outbound connections
only, so inbound are not capped by it), point the rest at it with
`BITMESSAGE_TRUSTED_PEER=<hub-ip>:8444`, and objects travel spoke → hub →
spokes. Use IP addresses, not service names: the sybil check parses the host
as an IP, and the `addr` exchange between nodes carries IPs anyway.
- **`BITMESSAGE_KNOWN_NODES` is what keeps a private contour private.** A node
whose `knownnodes.dat` names a peer outside PyBitmessage's built-in default
list stops asking `bootstrap8080.bitmessage.org` for more; without it even a
pinned node resolves the public bootstrap host on every start. The file is
rewritten on each start, so the variable, not the container's history, is what
the node believes on boot.
- **Publishing 8444 is a deliberate security trade-off.** The daemon runs on
Python 2 and already parses untrusted data from its outbound peers, so an open
port does not create that exposure — it changes *who* may connect, *when*, and
*how many*. The pre-handshake parser becomes reachable by anyone, and the
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.
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.