From 845d12154155a79fe834d444730786662fc09a70 Mon Sep 17 00:00:00 2001 From: Benjamen Keroack Date: Fri, 2 Oct 2026 11:09:10 -0600 Subject: [PATCH] stack, appliance: admit LAN clients to the TLS JSON-RPC listener (#887) Since #817, -rpcallowip gates the TLS JSON-RPC listener once the list has an entry. The stack template's list named only loopback and the stack subnet, which were meant for the plain listener, while its TLS listener binds 0.0.0.0 and is published. So every client on the LAN was dropped before the handshake: on the appliance all four boot tests fail "JSON-RPC is reachable over TLS" with an unexpected EOF, and the satd log reads "RPC TLS connection rejected: source IP not in the listener's allowlist". satd-init now writes rpcallowip=0.0.0.0/0. The plain listener is kept off the LAN by the network instead, as Electrum, Esplora and metrics already were: it is never published directly, and the appliance's nftables admits only the stack subnet to 8332. The subnet no longer reaches bitcoin.conf, so satd-init stops reading SATD_STACK_SUBNET and the appliance's render path stops passing it. compose.yml keeps passing it (and keeps its subnet fixed) because an older SATD_IMAGE's satd-init still uses it; the StartOS package does the same for its pinned image and can drop it when the pin moves. satd-init-test.sh checks that the rendered TLS listener admits a client outside the stack subnet; it fails with the old two lines put back. Co-authored-by: Claude Opus 5.5 (1M context) (cherry picked from commit 492054529e43e8de3f03ba5d27ec2ce24942169c) --- CHANGELOG.md | 1 + contrib/appliance/bin/satd-appliance | 1 - contrib/appliance/files/compose.appliance.yml | 5 ++--- contrib/appliance/files/configure-network | 1 - contrib/stack/.env.example | 3 +-- contrib/stack/compose.yml | 6 ++++-- contrib/stack/satd/satd-init | 2 -- contrib/stack/satd/satd.conf.tmpl | 15 ++++++++++----- contrib/stack/tests/satd-init-test.sh | 14 +++++++++++++- docs/release-notes/0.6.0-pre.md | 8 ++++++++ 10 files changed, 39 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a353cf7..ab9e26d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ item below is (or will be) written up in full in the in-development ### Changed +- `satd-init` (the reference stack, the appliance, Umbrel and StartOS) writes `rpcallowip=0.0.0.0/0`. The allowlist now gates the published TLS JSON-RPC listener too, and the stack-subnet list it wrote refused every client on the LAN before the handshake. The plain listener is kept off the LAN by the network, as the other plain listeners already were. - `satd-init` (the reference stack, Umbrel and StartOS) writes `upgradechainstate=1` for a satd that has the flag, so an app-store node rebuilds its chainstate from the block files by itself on the first start after a release that changes the format, and restarts a rebuild a power cut interrupted. - sat-tui draws the mempool size and UTXO age histograms as log-scaled bar charts, four rows tall with each bucket's count on its bar, instead of one-row linear sparklines that showed the largest bucket and flattened the rest. Both tails of each distribution are now visible. The age chart marks its young buckets as estimates while the node is still building its exact counts. - **Storage: every index is keyed on a dense transaction ordinal instead of a 32-byte txid.** Four schema steps, 3 → 7, and **one** `-reindex-chainstate` for all of them; the block files are untouched and nothing is re-downloaded. A fully indexed mainnet chainstate is projected to drop from ~826 GB to ~449 GB. Pruned datadirs must resync. The four bullets below are the individual steps. diff --git a/contrib/appliance/bin/satd-appliance b/contrib/appliance/bin/satd-appliance index 11f5d3a81..692ae67ec 100755 --- a/contrib/appliance/bin/satd-appliance +++ b/contrib/appliance/bin/satd-appliance @@ -112,7 +112,6 @@ run_compose() { # into /etc/default/satd, which is the only place satd will actually read it # from: a `signet=1` line in a config file is accepted and then ignored. render_config() { - SATD_STACK_SUBNET="$(stack_subnet)" \ SATD_TLS_HOSTNAME="$(hostname)" \ "$LIB/configure-network" "$1" } diff --git a/contrib/appliance/files/compose.appliance.yml b/contrib/appliance/files/compose.appliance.yml index 5b4151191..11c9b5c8d 100644 --- a/contrib/appliance/files/compose.appliance.yml +++ b/contrib/appliance/files/compose.appliance.yml @@ -21,9 +21,8 @@ # SATD_HOST=10.77.0.1 docker compose \ # -f compose.appliance.yml -f compose.lightning.yml up -d # -# satd itself binds 0.0.0.0 so the gateway reaches it. bitcoin.conf's -# rpcallowip limits that to this subnet, and nftables keeps every plain -# listener off the LAN entirely. +# satd itself binds 0.0.0.0 so the gateway reaches it, and nftables keeps +# every plain listener off the LAN: only this subnet reaches them. name: satd-appliance diff --git a/contrib/appliance/files/configure-network b/contrib/appliance/files/configure-network index 1abd20a94..dfbdee591 100755 --- a/contrib/appliance/files/configure-network +++ b/contrib/appliance/files/configure-network @@ -41,7 +41,6 @@ SATD_MKCA="$LIB/mkca.sh" \ NETWORK="$NETWORK" \ SATD_MCP=1 \ SATD_STATUSPAGE=1 \ -SATD_STACK_SUBNET="${SATD_STACK_SUBNET:-10.77.0.0/24}" \ SATD_TLS_HOSTNAME="${SATD_TLS_HOSTNAME:-$(hostname)}" \ SATD_CA_EXPORT_HINT="satd-appliance tls export-ca" \ "$LIB/satd-init" diff --git a/contrib/stack/.env.example b/contrib/stack/.env.example index dea53607a..d1c20f7e4 100644 --- a/contrib/stack/.env.example +++ b/contrib/stack/.env.example @@ -56,6 +56,5 @@ SATD_MCP=0 # echo "RTL_PASSWORD=$(openssl rand -hex 24)" >> .env # --- internals -------------------------------------------------------------- -# The compose network. Change only if it collides with an existing network; -# bitcoin.conf's rpcallowip is derived from it. +# The compose network. Change only if it collides with an existing network. SATD_STACK_SUBNET=10.77.0.0/24 diff --git a/contrib/stack/compose.yml b/contrib/stack/compose.yml index 76413f517..230972ded 100644 --- a/contrib/stack/compose.yml +++ b/contrib/stack/compose.yml @@ -32,6 +32,8 @@ services: environment: NETWORK: ${NETWORK:-signet} SATD_MCP: ${SATD_MCP:-0} + # Read by the satd-init in images up to 0.6.0-rc1, which put the + # subnet in rpcallowip; later images ignore it. SATD_STACK_SUBNET: ${SATD_STACK_SUBNET:-10.77.0.0/24} SATD_TLS_HOSTNAME: ${SATD_TLS_HOSTNAME:-satd} SATD_P2P_PORT: ${SATD_P2P_PORT:-38333} @@ -104,8 +106,8 @@ volumes: networks: satd: - # A fixed subnet so bitcoin.conf's rpcallowip can name it exactly rather - # than opening RPC to every RFC1918 address. + # A fixed subnet: satd images up to 0.6.0-rc1 name it in rpcallowip + # (see satd-init above). ipam: config: - subnet: ${SATD_STACK_SUBNET:-10.77.0.0/24} diff --git a/contrib/stack/satd/satd-init b/contrib/stack/satd/satd-init index a0b1426ed..3184cea83 100755 --- a/contrib/stack/satd/satd-init +++ b/contrib/stack/satd/satd-init @@ -26,7 +26,6 @@ TEMPLATE="${SATD_CONF_TEMPLATE:-/etc/satd/satd.conf.tmpl}" MKCA="${SATD_MKCA:-/usr/local/bin/satd-mkca}" NETWORK="${NETWORK:-signet}" -SUBNET="${SATD_STACK_SUBNET:-10.77.0.0/24}" TLS_HOSTNAME="${SATD_TLS_HOSTNAME:-$(hostname)}" MCP_ENABLED="${SATD_MCP:-0}" # Further names to put in MCP's Host allowlist, comma-separated. The names @@ -107,7 +106,6 @@ render() { -e "s|@ESPLORA_TLS_PORT@|$ESPLORA_TLS_PORT|g" \ -e "s|@METRICS_PORT@|$METRICS_PORT|g" \ -e "s|@ZMQ_PORT@|$ZMQ_PORT|g" \ - -e "s|@SUBNET@|$SUBNET|g" \ -e "s|@TLS_DIR@|$TLS_DIR|g" \ "$TEMPLATE" } diff --git a/contrib/stack/satd/satd.conf.tmpl b/contrib/stack/satd/satd.conf.tmpl index 715d12c82..1d11f42dc 100644 --- a/contrib/stack/satd/satd.conf.tmpl +++ b/contrib/stack/satd/satd.conf.tmpl @@ -13,8 +13,8 @@ # Plain listeners bind the compose network only and are NOT published to # the host: RPC, Electrum, Esplora and the metrics endpoint are how the # overlay containers (LND, NBXplorer, RTL, the mint) talk to satd, and -# those clients have no way to trust a private CA. `rpcallowip` narrows -# the RPC surface to this stack's own subnet. +# those clients have no way to trust a private CA. What keeps them inside +# is where they are reachable from, not `rpcallowip` (see JSON-RPC below). # # TLS listeners bind 0.0.0.0 and ARE published. Everything that leaves the # host is TLS, presenting the per-install certificate from tls/mkca.sh. @@ -55,9 +55,14 @@ peerblockfilters=1 server=1 rpcbind=0.0.0.0 rpcport=@RPC_PORT@ -# The stack's own subnet, plus loopback for `docker exec satd sat-cli`. -rpcallowip=127.0.0.1 -rpcallowip=@SUBNET@ +# `rpcallowip` gates the TLS listener too, and that one is published: its +# clients are wherever the operator's wallets are, so the list admits every +# address. A narrower list refuses them before the TLS handshake. The plain +# listener is kept off the LAN by the network instead, as the other plain +# listeners already are: it is never published directly (StartOS fronts it +# with its own TLS proxy), and the appliance, which runs satd on the host, +# opens 8332 at nftables to the stack subnet only. +rpcallowip=0.0.0.0/0 rpctlsbind=0.0.0.0:@RPC_TLS_PORT@ rpctlscert=@TLS_DIR@/fullchain.crt rpctlskey=@TLS_DIR@/leaf.key diff --git a/contrib/stack/tests/satd-init-test.sh b/contrib/stack/tests/satd-init-test.sh index fc6c73486..cf10fa77e 100755 --- a/contrib/stack/tests/satd-init-test.sh +++ b/contrib/stack/tests/satd-init-test.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # satd-init's guarded keys (status page, metrics over TLS, chainstate -# upgrades), against stub binaries. +# upgrades) and the JSON-RPC allowlist, against stub binaries. # # Each must be written for a satd that has the key and not for one that does # not: satd refuses an unknown config key, so the second case is the @@ -79,6 +79,18 @@ else ok "writes nothing for a satd without it" fi +echo "== satd-init: JSON-RPC over TLS from the LAN ==" +# rpcallowip gates the published TLS listener as well as the plain one, so a +# list naming only the stack subnet refuses every wallet on the LAN before the +# handshake. The appliance boot test sees that as an `unexpected eof`. +render "$WORK/new/satd" "$WORK/d" SATD_STACK_SUBNET=10.99.0.0/24 +if grep -q '^rpctlsbind=0\.0\.0\.0:' "$WORK/d/bitcoin.conf" \ + && grep -qx 'rpcallowip=0.0.0.0/0' "$WORK/d/bitcoin.conf"; then + ok "the TLS listener admits a client outside the stack subnet" +else + bad "TLS RPC refuses the LAN: $(grep -E '^rpc(allowip|tlsbind)' "$WORK/d/bitcoin.conf" | tr '\n' ' ')" +fi + echo [[ "$fail" -eq 0 ]] || { echo "satd-init-test.sh: FAILED"; exit 1; } echo "satd-init-test.sh: all checks passed" diff --git a/docs/release-notes/0.6.0-pre.md b/docs/release-notes/0.6.0-pre.md index 0f7c4f2b6..5d060b762 100644 --- a/docs/release-notes/0.6.0-pre.md +++ b/docs/release-notes/0.6.0-pre.md @@ -1195,6 +1195,14 @@ only the clients of your plain-HTTP bind, add the TLS clients' addresses to the same list before upgrading, or they will be refused. The same goes for `-rpcreadonlytlsbind` and `-rpcreadonlyallowip`. +The reference stack, the appliance, and the Umbrel and StartOS packages were +that configuration: their TLS listener is published to the LAN, and their +allowlist named only the stack's own subnet. `satd-init` now writes +`rpcallowip=0.0.0.0/0`, and the plain listener is kept off the LAN by the +network instead (it is never published directly, and the appliance's +nftables admits only the stack subnet to it), as their other plain listeners +already were. + On a dual-stack bind (`[::]`), an IPv4 client is now matched as the IPv4 address it is. It reached the listener as `::ffff:a.b.c.d`, which matched no IPv4 entry and missed the loopback exemption, so a plain `[::]` bind