Skip to content

lnaddress: add a clnrest backend authenticated with a restricted rune - #17

Open
nGoline wants to merge 9 commits into
mainfrom
feat/clnrest
Open

nGoline wants to merge 9 commits into
mainfrom
feat/clnrest

Conversation

@nGoline

@nGoline nGoline commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Stacked on #16 and #15. Merge those first. The diff then shrinks to the 4 commits from 1a7848c onward, and I'll rebase onto main.

Why

cln-grpc can't restrict a client. In v26.06.9 the plugin configures TLS with client_ca_root(ca) and no auth interceptor, so any certificate signed by the node's CA gets every RPC method, including pay and withdraw. Its source says so (plugins/grpc-plugin/src/tls.rs): "the server will accept any client that is signed by the CA. In the future, we might add runes". Minting a separate client cert doesn't help. clnrest checks a rune on every call, against the method and its parameters.

What

  • LIGHTNING__BACKEND=clnrest → new ClnRestService: POST /v1/{invoice,listinvoices,getinfo} with the Rune header. Env: CLNREST__URL, CLNREST__RUNE, CLNREST__CACERT (the node's ca.pem; it can be omitted only behind a publicly trusted proxy). Behaves the same as the gRPC backend: description hash, comment in label, verify limited to lnaddress- invoices.
  • LIGHTNING__BACKEND=cln (gRPC) still works unchanged, and is documented as legacy.
  • PEM reading, CA validation and invoice labels are moved into Helpers/ and shared by both transports.

The recommended rune

lightning-cli createrune restrictions='[["method=getinfo","method=invoice","method=listinvoices"],["method/invoice","pnamelabel^lnaddress-"],["method/listinvoices","pnamepayment_hash/"]]'
Call Result on v26.06.9
getinfo; invoice with an lnaddress- label; listinvoices by payment_hash ✅ allowed
invoice with any other label ❌ 401
listinvoices with no filter or by label (can't dump other invoices) ❌ 401
pay, withdraw, listfunds, createrune, … ❌ 401

Docs

  • The CLN section now leads with clnrest: enabling it, the rune and what it allows, revoking it with showrunes/blacklistrune, and env vars.
  • The "don't expose it publicly" warning is kept and now covers both transports. The rune can't spend, but it can still create invoices, and the clnrest port accepts any other rune you've issued. Keep it on a private network and proxy only the two LNURL paths.
  • cln-grpc moves to a Legacy subsection with a stronger caution, plus migration steps. CLNREST__CACERT takes the same ca.pem as before, and labels are unchanged, so verify keeps working for old invoices.

Tests

  • Unit (56, +15): request shape (URL, Rune header, amount_msat/label/description/deschashonly), comment in label, paid/unpaid/expired, foreign and unknown invoices → null, sync warnings, rune rejection → error, config validation.
  • Integration (23, on CLN v26.06.9): the regtest node now runs clnrest too, and the fixture creates the README's rune. The same 8 backend tests run over both gRPC and clnrest (shared base class). Another 6 confirm the rune rejects listfunds, withdraw, pay, createrune, unfiltered listinvoices and invoices with other labels. One test checks that the README documents exactly the rune the tests use.
  • End to end: the Docker image with LIGHTNING__BACKEND=clnrest on a private network with the regtest node. Healthy; description_hash == sha256(metadata) per lightning-cli decode; comment in label; real payment settles and verify returns the preimage; foreign invoice → Not found; no /health log noise.
  • ci: add GitHub Actions for CI, releases and Dependabot #14 is updated so the CI CLN job runs both ClnGrpcIntegrationTest and ClnRestIntegrationTest.

Deployment note

Nothing changes unless you switch. To move the ms02 deployment: add clnrest-port=3010 and clnrest-host=<container IP or 0.0.0.0 on the private network> to lightningd, create the rune, then replace LIGHTNING__BACKEND=cln + CLN__* with LIGHTNING__BACKEND=clnrest, CLNREST__URL=https://lightningd:3010, CLNREST__RUNE and CLNREST__CACERT (the same ca.pem). Then drop client.pem/client-key.pem from LnAddress's env.

🤖 Generated with Claude Code

nGoline and others added 9 commits October 22, 2026 09:42
The image listens on 8080 and ships wget but not curl, so the compose
healthcheck always failed and the docker run examples mapped the wrong
port. Fix both, along with the LND certificate instructions, and note
that the reverse proxy must terminate TLS since callback URLs are
always https.

Document the release binaries, the HTTP endpoints, and how to build and
run the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cln-grpc node.proto is unchanged between v26.06.8 and v26.06.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LUD-06 requires the invoice's description hash to equal sha256 of the
metadata served at /.well-known/lnurlp. Invoices carried a plain
description instead, so wallets that check it (Zeus, Phoenix, Breez)
rejected them.

Build the metadata in one place and use it for both the well-known
response and the invoice: CLN gets it as the description with
deschashonly, LND as the description hash. The LUD-12 comment no longer
goes in the description; CLN keeps it in the invoice label and LND
drops it.

The metadata now includes the LUD-16 text/identifier entry and is
serialized as JSON instead of interpolated. Usernames outside the LUD-16
charset return 404 and the rest are lowercased.

On CLN, verify only answers for invoices labeled lnaddress-, so it no
longer exposes other invoices on the node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a HEALTHCHECK that polls /health with busybox wget, so deployments
no longer need their own, and drop it from the example compose file.

Ship an appsettings.json that logs Microsoft.AspNetCore at Warning, so
the healthcheck doesn't log six lines every 30 seconds.

Document that cln-grpc client certificates grant full control of the
node and how to limit their exposure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move PEM reading, CA chain validation and invoice labels out of
ClnService so a second Core Lightning transport can share them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cln-grpc accepts any client certificate signed by the node's CA and
can't restrict what it may call, so the certificate LnAddress needs
grants full control of the node, funds included.

LIGHTNING__BACKEND=clnrest talks to the clnrest plugin instead, with a
rune that can be restricted to getinfo, invoice with an lnaddress-
label, and listinvoices by payment_hash. clnrest's certificate is
validated against the node's CA, like cln-grpc's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document enabling clnrest, the rune to create and how to revoke it,
and how to move from cln-grpc. cln-grpc stays as a legacy option with
its warning, and the advice to keep either port off the public network
now covers both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enable clnrest on the regtest node, have the fixture create the rune
the README recommends, and run the same tests over cln-grpc and
clnrest. Check that the rune rejects every other call, and that the
README documents the same rune.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant