mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
* Add a BTCPay Server SSO harness to the docker fixture
BTCPay bundles RTL and runs it in single-sign-on mode, reached over an entry
path the standalone login never exercises: no password, a rotating cookie file,
an unregistered /rtl/api/authenticate/cookie URL that falls through to the
catch-all in server/utils/app.ts, and a reverse proxy in front. Regressions on
that path have previously gone unnoticed until they reached BTCPay users.
Adds an "sso" compose profile, so a plain `docker compose up -d` is unchanged:
- rtl-sso, a second RTL running with RTL_SSO=1, RTL_COOKIE_PATH and
LOGOUT_REDIRECT_LINK -- the environment block lifted verbatim from BTCPay's
own compose fragment, so this exercises the env-driven SSO path BTCPay
actually uses. A second container is required because RTL selects one
authentication mode at startup, so SSO and password login cannot coexist in
one instance.
- rtl-sso-config-init, staging rtl/RTL-Config.sso.json into a volume -- the
same copy-into-a-volume dance the standalone RTL already needs, because RTL
rewrites its config on startup.
- rtl-sso-proxy, nginx standing in for BTCPay's traefik, routing only /rtl
and /rtl/* exactly as BTCPay's router rule does. There is no prefix
stripping anywhere: RTL is built with <base href="/rtl/"> and mounts every
route under baseHref '/rtl', so the prefix is passed through unmodified.
Everything outside /rtl 404s, so a request escaping the prefix surfaces as
a failure rather than being quietly served.
scripts/verify-sso.sh asserts the whole flow in 11 checks -- prefix routing,
CSRF token minting on the catch-all, the sha256 access-key handshake, an
authenticated node call, cookie rotation on login, and rejection of a wrong key
-- and exits non-zero so it can gate a change. bin/sso-url prints the link
BTCPay renders on its Services page. RTL_IMAGE overrides both RTL containers at
once, so a branch build gets tested through both entry paths.
BTCPay itself (postgres, nbxplorer, btcpayserver) is deliberately not included;
the README documents what that leaves untested and how to run against BTCPay's
own regtest stack when the question is BTCPay's behaviour rather than RTL's.
Also bumps the fixture's default RTL image from v0.15.8 to v0.15.10.
* Document the SSO harness in the rtl-docker-fixture skill
* Point CLAUDE.md at the BTCPay SSO harness
* Note that no CI runs on an open PR
400 lines
16 KiB
YAML
400 lines
16 KiB
YAML
# Regtest dev fixture for RTL: bitcoind + 3 LND nodes + RTL.
|
|
#
|
|
# NOT suitable for production. All credentials are throwaway.
|
|
#
|
|
# Topology is alice -> bob -> carol, so bob forwards payments and RTL's
|
|
# routing/forwarding screens have data in them. See README.md.
|
|
#
|
|
# Node images come from Polar (https://lightningpolar.com), which publishes
|
|
# multi-arch (amd64 + arm64) builds. Nothing is built locally.
|
|
|
|
volumes:
|
|
bitcoind_data:
|
|
alice_data:
|
|
bob_data:
|
|
carol_data:
|
|
cln_data:
|
|
eclair_data:
|
|
rtl_db:
|
|
rtl_config:
|
|
rtl_sso_db:
|
|
rtl_sso_config:
|
|
rtl_sso_cookie:
|
|
|
|
x-lnd: &lnd
|
|
image: polarlightning/lnd:0.20.0-beta
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- bitcoind
|
|
|
|
services:
|
|
bitcoind:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_bitcoind
|
|
image: polarlightning/bitcoind:30.0
|
|
restart: unless-stopped
|
|
command:
|
|
- bitcoind
|
|
- -server=1
|
|
- -regtest=1
|
|
# rpcauth hash for ${BITCOIN_RPC_USER}/${BITCOIN_RPC_PASSWORD}. '$$' escapes
|
|
# compose interpolation and reaches bitcoind as a single '$'.
|
|
- -rpcauth=rtldev:8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e$$010df4b32c5e9a556cba1857eb5865990c983d8a56dadc0fdbf457cf90073c6c
|
|
- -zmqpubrawblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT}
|
|
- -zmqpubrawtx=tcp://0.0.0.0:${BITCOIN_ZMQ_TX_PORT}
|
|
# eclair's zmqblock consumes the hashblock topic, not rawblock (LND uses
|
|
# rawblock/rawtx above); without this endpoint eclair never sees new
|
|
# blocks and channels stay in WAIT_FOR_FUNDING_CONFIRMED forever.
|
|
- -zmqpubhashblock=tcp://0.0.0.0:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
|
|
- -txindex=1
|
|
- -dnsseed=0
|
|
- -rpcbind=0.0.0.0
|
|
- -rpcallowip=0.0.0.0/0
|
|
- -rpcport=${BITCOIN_RPC_PORT}
|
|
- -listen=1
|
|
- -listenonion=0
|
|
- -fallbackfee=0.0002
|
|
ports:
|
|
- "${BITCOIN_RPC_PORT}:${BITCOIN_RPC_PORT}"
|
|
volumes:
|
|
- bitcoind_data:/home/bitcoin/.bitcoin
|
|
|
|
# --alias / --externalip / --tlsextradomain are per-node on purpose: the alias
|
|
# is what RTL displays, and the extradomain must match the hostname RTL dials
|
|
# (https://alice:8080) or TLS validation fails.
|
|
alice:
|
|
<<: *lnd
|
|
container_name: ${COMPOSE_PROJECT_NAME}_alice
|
|
command:
|
|
- lnd
|
|
- --noseedbackup
|
|
- --trickledelay=5000
|
|
- --alias=alice
|
|
- --externalip=alice
|
|
- --tlsextradomain=alice
|
|
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_alice
|
|
- --tlsextradomain=host.docker.internal
|
|
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
|
|
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
|
|
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
|
|
- --bitcoin.active
|
|
- --bitcoin.regtest
|
|
- --bitcoin.node=bitcoind
|
|
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
|
|
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
|
|
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
|
|
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
|
|
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
|
|
- --accept-keysend
|
|
- --accept-amp
|
|
ports:
|
|
- "${ALICE_REST_PORT}:${LIGHTNING_REST_PORT}"
|
|
volumes:
|
|
- alice_data:/home/lnd/.lnd
|
|
|
|
bob:
|
|
<<: *lnd
|
|
container_name: ${COMPOSE_PROJECT_NAME}_bob
|
|
command:
|
|
- lnd
|
|
- --noseedbackup
|
|
- --trickledelay=5000
|
|
- --alias=bob
|
|
- --externalip=bob
|
|
- --tlsextradomain=bob
|
|
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_bob
|
|
- --tlsextradomain=host.docker.internal
|
|
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
|
|
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
|
|
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
|
|
- --bitcoin.active
|
|
- --bitcoin.regtest
|
|
- --bitcoin.node=bitcoind
|
|
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
|
|
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
|
|
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
|
|
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
|
|
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
|
|
- --accept-keysend
|
|
- --accept-amp
|
|
ports:
|
|
- "${BOB_REST_PORT}:${LIGHTNING_REST_PORT}"
|
|
volumes:
|
|
- bob_data:/home/lnd/.lnd
|
|
|
|
carol:
|
|
<<: *lnd
|
|
container_name: ${COMPOSE_PROJECT_NAME}_carol
|
|
command:
|
|
- lnd
|
|
- --noseedbackup
|
|
- --trickledelay=5000
|
|
- --alias=carol
|
|
- --externalip=carol
|
|
- --tlsextradomain=carol
|
|
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_carol
|
|
- --tlsextradomain=host.docker.internal
|
|
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
|
|
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
|
|
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
|
|
- --bitcoin.active
|
|
- --bitcoin.regtest
|
|
- --bitcoin.node=bitcoind
|
|
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
|
|
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
|
|
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
|
|
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
|
|
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
|
|
- --accept-keysend
|
|
- --accept-amp
|
|
ports:
|
|
- "${CAROL_REST_PORT}:${LIGHTNING_REST_PORT}"
|
|
volumes:
|
|
- carol_data:/home/lnd/.lnd
|
|
|
|
# Core Lightning node. Unlike the LND nodes it talks to RTL over clnrest (the
|
|
# built-in REST plugin) using rune auth, so it needs --clnrest-* options and a
|
|
# rune written where RTL can read it. create-rune.sh (run from the healthcheck)
|
|
# creates the rune and writes it to /root/.lightning/rtl.rune (RTL mounts that
|
|
# read-only); the healthcheck is unhealthy until it exists, so rtl waits for it.
|
|
# --clnrest-host=0.0.0.0 is required so the rtl container can reach it; the default
|
|
# 127.0.0.1 would only be reachable from inside this container. Protocol stays https
|
|
# (clnrest default, self-signed) — RTL connects with rejectUnauthorized:false.
|
|
cln:
|
|
image: elementsproject/lightningd:v25.09
|
|
container_name: ${COMPOSE_PROJECT_NAME}_cln
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- bitcoind
|
|
environment:
|
|
LIGHTNINGD_NETWORK: regtest
|
|
command:
|
|
- --alias=cln
|
|
- --bitcoin-rpcconnect=${BITCOIN_HOST}
|
|
- --bitcoin-rpcport=${BITCOIN_RPC_PORT}
|
|
- --bitcoin-rpcuser=${BITCOIN_RPC_USER}
|
|
- --bitcoin-rpcpassword=${BITCOIN_RPC_PASSWORD}
|
|
- --bitcoin-retry-timeout=3600
|
|
- --addr=0.0.0.0:9735
|
|
- --announce-addr=cln:9735
|
|
- --large-channels
|
|
- --clnrest-host=0.0.0.0
|
|
- --clnrest-port=3010
|
|
ports:
|
|
- "${CLN_REST_PORT:-3010}:3010"
|
|
volumes:
|
|
- cln_data:/root/.lightning
|
|
- ./cln/create-rune.sh:/opt/create-rune.sh:ro
|
|
healthcheck:
|
|
# create-rune.sh ensures the rune exists (idempotent, one quick attempt) and
|
|
# this reports healthy only once it does. Driving it from the healthcheck — which
|
|
# retries on its interval — means a transient RPC-startup race self-heals instead
|
|
# of a one-shot script permanently wedging the stack. rtl waits on this via
|
|
# depends_on: condition: service_healthy before reading the rune at startup.
|
|
test: ["CMD-SHELL", "sh /opt/create-rune.sh && test -f /root/.lightning/rtl.rune"]
|
|
interval: 5s
|
|
timeout: 10s
|
|
retries: 40
|
|
|
|
# Eclair has no on-chain wallet of its own -- it drives a bitcoind wallet over
|
|
# RPC. Without a dedicated wallet it would grab "the default loaded wallet",
|
|
# which here is the rtldev mining wallet, and eclair's on-chain balance would
|
|
# show the miner's coins. This init container creates (or reloads) a wallet
|
|
# named "eclair" before the eclair node starts; load_on_startup survives
|
|
# bitcoind restarts.
|
|
eclair-wallet-init:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_eclair_wallet_init
|
|
image: polarlightning/bitcoind:30.0
|
|
depends_on:
|
|
- bitcoind
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
bcli() { bitcoin-cli -regtest -rpcconnect=${BITCOIN_HOST} -rpcport=${BITCOIN_RPC_PORT} -rpcuser=${BITCOIN_RPC_USER} -rpcpassword=${BITCOIN_RPC_PASSWORD} "$$@"; }
|
|
i=0
|
|
until bcli getblockchaininfo >/dev/null 2>&1; do
|
|
i=$$((i+1)); [ "$$i" -ge 60 ] && echo "bitcoind never came up" && exit 1
|
|
sleep 1
|
|
done
|
|
bcli -named createwallet wallet_name=eclair load_on_startup=true >/dev/null 2>&1 \
|
|
|| bcli loadwallet eclair true >/dev/null 2>&1 \
|
|
|| true
|
|
bcli -rpcwallet=eclair getwalletinfo >/dev/null
|
|
echo "eclair wallet ready"
|
|
|
|
# Eclair node. Talks to RTL over its HTTP API with basic auth (the
|
|
# lnApiPassword in RTL's config). The polarlightning image is used because
|
|
# acinq/eclair on Docker Hub is amd64-only and its newest versioned tag is
|
|
# years stale; Polar builds the same ACINQ source multi-arch (amd64 + arm64).
|
|
# The image entrypoint translates each --key=value into -Declair.key=value.
|
|
# The entrypoint also overrides server.public-ips.0 with the container IP, but
|
|
# the arg must still be present for that substitution to happen.
|
|
eclair:
|
|
image: polarlightning/eclair:0.13.1
|
|
container_name: ${COMPOSE_PROJECT_NAME}_eclair
|
|
restart: unless-stopped
|
|
depends_on:
|
|
bitcoind:
|
|
condition: service_started
|
|
eclair-wallet-init:
|
|
condition: service_completed_successfully
|
|
command:
|
|
- polar-eclair
|
|
- --node-alias=eclair
|
|
- --server.public-ips.0=eclair
|
|
- --server.port=9735
|
|
- --api.enabled=true
|
|
- --api.binding-ip=0.0.0.0
|
|
- --api.port=8080
|
|
- --api.password=${ECLAIR_API_PASSWORD:-rtldev}
|
|
- --chain=regtest
|
|
- --bitcoind.host=${BITCOIN_HOST}
|
|
- --bitcoind.rpcport=${BITCOIN_RPC_PORT}
|
|
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
|
|
- --bitcoind.rpcpassword=${BITCOIN_RPC_PASSWORD}
|
|
# zmqblock must be bitcoind's *hashblock* endpoint (eclair subscribes to
|
|
# that topic); pointing it at the rawblock endpoint the LND nodes use
|
|
# leaves eclair blind to new blocks.
|
|
- --bitcoind.zmqblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
|
|
- --bitcoind.zmqtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
|
|
- --bitcoind.wallet=eclair
|
|
- --datadir=/home/eclair/.eclair
|
|
- --printToConsole=true
|
|
# Regtest feerates are far from mainnet estimates; without a wide
|
|
# tolerance eclair closes channels over feerate disagreements.
|
|
- --on-chain-fees.feerate-tolerance.ratio-low=0.00001
|
|
- --on-chain-fees.feerate-tolerance.ratio-high=10000.0
|
|
ports:
|
|
- "${ECLAIR_REST_PORT:-8281}:8080"
|
|
volumes:
|
|
- eclair_data:/home/eclair
|
|
|
|
# RTL rewrites its config file on startup, so it cannot be given the tracked
|
|
# template directly: a bind mount would either be read-only (RTL exits with
|
|
# EROFS) or would let RTL scribble into a version-controlled file. Instead the
|
|
# template is copied into a volume that 'down -v' discards, which keeps the
|
|
# source pristine and every run starting from identical config.
|
|
rtl-config-init:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_rtl_config_init
|
|
image: busybox:1.36
|
|
command: >
|
|
sh -c "cp /template/RTL-Config.regtest.json /config/RTL-Config.json &&
|
|
chmod 644 /config/RTL-Config.json &&
|
|
echo 'config staged'"
|
|
volumes:
|
|
- ./rtl/RTL-Config.regtest.json:/template/RTL-Config.regtest.json:ro
|
|
- rtl_config:/config
|
|
|
|
rtl:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_rtl
|
|
# Defaults to the published image; override with RTL_IMAGE (e.g. a locally built
|
|
# branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up.
|
|
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
rtl-config-init:
|
|
condition: service_completed_successfully
|
|
alice:
|
|
condition: service_started
|
|
bob:
|
|
condition: service_started
|
|
carol:
|
|
condition: service_started
|
|
cln:
|
|
condition: service_healthy
|
|
eclair:
|
|
condition: service_started
|
|
ports:
|
|
- "${RTL_PORT}:${RTL_PORT}"
|
|
environment:
|
|
RTL_CONFIG_PATH: /RTL/config
|
|
volumes:
|
|
- rtl_config:/RTL/config
|
|
- alice_data:/lnd/alice:ro
|
|
- bob_data:/lnd/bob:ro
|
|
- carol_data:/lnd/carol:ro
|
|
- cln_data:/cln:ro
|
|
- rtl_db:/RTL/database
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BTCPay Server SSO harness -- profile "sso", so a plain 'up' does not start it:
|
|
#
|
|
# docker compose --profile sso up -d
|
|
# bin/sso-url
|
|
#
|
|
# BTCPay bundles RTL as a service and runs it in single-sign-on mode. RTL
|
|
# writes a random cookie to RTL_COOKIE_PATH; BTCPay reads that file and renders
|
|
# a link to /rtl/api/authenticate/cookie?access-key=<cookie> on its Services
|
|
# page. That path is not a registered route -- it falls through to RTL's
|
|
# catch-all, which mints the XSRF-TOKEN cookie and serves index.html, and the
|
|
# SPA then reads access-key from the query string and posts it as a password
|
|
# login. Reproducing that entry path is the whole point of this harness: it is
|
|
# where RTL's auth, CSRF and static-serving behaviour meet an external caller,
|
|
# and it has broken before without the standalone flow noticing.
|
|
#
|
|
# BTCPay itself (postgres + nbxplorer + btcpayserver) is deliberately not here.
|
|
# See README.md, "BTCPay SSO harness", for what that leaves untested and how to
|
|
# run against a real BTCPay when it matters.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Same copy-into-a-volume dance as rtl-config-init above, and for the same
|
|
# reason: RTL rewrites its config on startup.
|
|
rtl-sso-config-init:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_config_init
|
|
profiles: ["sso"]
|
|
image: busybox:1.36
|
|
command: >
|
|
sh -c "cp /template/RTL-Config.sso.json /config/RTL-Config.json &&
|
|
chmod 644 /config/RTL-Config.json &&
|
|
echo 'sso config staged'"
|
|
volumes:
|
|
- ./rtl/RTL-Config.sso.json:/template/RTL-Config.sso.json:ro
|
|
- rtl_sso_config:/config
|
|
|
|
# A second RTL rather than a flag on the first: RTL picks one authentication
|
|
# mode at startup, so SSO and the password login cannot coexist in one
|
|
# instance. Only alice is wired up, matching BTCPay's one-node-per-RTL layout.
|
|
#
|
|
# The environment block is copied from BTCPay's own compose fragment
|
|
# (docker-compose-generator/docker-fragments/bitcoin-lnd.yml in
|
|
# btcpayserver-docker), so this exercises the env-driven SSO path BTCPay
|
|
# actually uses rather than the config-file equivalent -- which is why
|
|
# RTL-Config.sso.json leaves its SSO block zeroed and carries no multiPass.
|
|
#
|
|
# Only 'expose'd, never published: all access goes through the proxy, as it
|
|
# does under BTCPay.
|
|
rtl-sso:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso
|
|
profiles: ["sso"]
|
|
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
rtl-sso-config-init:
|
|
condition: service_completed_successfully
|
|
alice:
|
|
condition: service_started
|
|
environment:
|
|
RTL_CONFIG_PATH: /RTL/config
|
|
RTL_SSO: 1
|
|
RTL_COOKIE_PATH: /RTL/cookie/.cookie
|
|
LOGOUT_REDIRECT_LINK: /server/services
|
|
expose:
|
|
- "3000"
|
|
volumes:
|
|
- rtl_sso_config:/RTL/config
|
|
- rtl_sso_cookie:/RTL/cookie
|
|
- alice_data:/lnd/alice:ro
|
|
- rtl_sso_db:/RTL/database
|
|
|
|
# Stands in for BTCPay's traefik. Routes only /rtl and /rtl/*, mirroring the
|
|
# label BTCPay puts on its RTL container; see nginx/rtl-sso.conf.
|
|
rtl-sso-proxy:
|
|
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_proxy
|
|
profiles: ["sso"]
|
|
image: nginx:1.27-alpine
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- rtl-sso
|
|
ports:
|
|
- "${RTL_SSO_PORT:-3001}:80"
|
|
volumes:
|
|
- ./nginx/rtl-sso.conf:/etc/nginx/conf.d/default.conf:ro
|