The half that landed correctly (LND, clean 401) made the half that did not harder to notice, because the first check an operator would run returns a pass. Records the probes, the three-fact root cause, and the pass condition for re-probing a node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.5 KiB
The Bitcoin RPC proxy that stayed open after it was fixed
Status: code fix committed (f6b5245b); on-node verification recorded below.
Found: 2026-08-02, archi-dev-box, while verifying a05956c4 instead of assuming it.
Severity: critical on any affected node — unauthenticated control of Bitcoin Core RPC
through a proxy that injects the node's own credentials.
Why this document exists
a05956c4 closed two unauthenticated endpoints on the wallet UI ports. Its commit message
stated:
The nginx template is
include_str!'d and re-rendered on every reconcile pass, so this ships atomically with the binary.
That is true for most nodes and false for a specific, silent, and not-rare state. The half
that landed correctly (LND) made the half that did not (Bitcoin RPC) harder to notice,
because a spot check of the LND endpoint returns a clean 401 and reads as "patched".
What was observed
Node running the fixed binary (installed 17:21, contains the new template — auth_request
present in the binary at 4 occurrences). All probes from the node's own LAN address, no
cookies, no credentials:
| Probe | Result |
|---|---|
GET http://192.168.63.240:18083/lnd-connect-info |
401, 24 bytes, {"error":"Unauthorized"} — closed |
POST http://192.168.63.240:8334/bitcoin-rpc/ (getblockcount) |
200 — {"result":960774,"error":null} — OPEN |
OPTIONS http://192.168.63.240:8334/bitcoin-rpc/ |
204 with Access-Control-Allow-Origin: * — OPEN |
The rendered config on disk, /var/lib/archipelago/bitcoin-ui/nginx.conf, was dated
2026-06-30 — the pre-fix version, with no auth_request and with the wildcard CORS
header the fix removes.
Root cause
Three facts have to be true at once, and on this node they were:
bitcoin-uiis listed in the node's durableuser-uninstalledmarker (/var/lib/archipelago/user-uninstalled.json).reconcile_appreturns on that marker (prod_orchestrator.rs:1956) before reachingrun_pre_start_hooks, which is the only thing that renders the nginx config.- The container keeps running anyway, because it is owned by systemd via a Quadlet
unit —
archy-bitcoin-ui.service,active, restarted 17:25 after the daemon restart — not by the reconciler that is refusing to touch it.
So: a container systemd keeps alive, that the orchestrator has stopped reconciling, never receives a config fix shipped inside the binary. The marker means "must stay removed", but nothing enforces removal against systemd, and the orchestrator treats the marker as permission to stop looking.
This is not a one-app accident. On the same node archy-electrs-ui is in the identical
state (uninstalled marker + active Quadlet unit + Up 10 days). It serves only a static
page with no credential-injecting proxy, so its exposure is low — but it would miss any
future config fix the same way.
Why it matters beyond this node
An OTA carrying a05956c4 would have closed the LND leak everywhere and silently failed to
close the Bitcoin RPC proxy on every node in this state — while making those nodes look
patched to exactly the check an operator would run first. That is the most misleading
possible outcome of shipping a security fix.
The fix
f6b5245b: a container that is actually running is a live attack surface whatever a marker
says about it, so its security-relevant config is reconciled even behind the marker, and the
container is restarted so nginx loads it.
Deliberately narrow:
- Nothing is created, pulled, built, started or resurrected. The "must stay removed" contract can only weaken for a container that is already running, which by definition means it was never removed.
- A hook error is swallowed, not propagated — an app the user uninstalled must not be able to fail the reconcile pass for every app after it.
- The pre-existing marker test passes unchanged; that is what proves the removal contract survived. A new regression test pins the whole chain: stale conf in, gate present out, container restarted, nothing created.
Operator note
Deploying the fix rewrites the config and restarts archy-bitcoin-ui (a brief Bitcoin UI
interruption, nothing else). Any node that ever had bitcoin-ui uninstalled while its
Quadlet unit stayed active should be re-probed with the POST /bitcoin-rpc/ check above —
a 401 is the pass condition. Treat the Bitcoin RPC password on any node that answered
200 as known to anyone who could reach that port, and rotate it after the fix is
deployed, never before.