Skip to content

Commit 1e9e322

Browse files
committed
Replace HMAC Auth with scoped macaroons
We initially implemented HMAC auth but it doesn't allow for scoped permissions without creating a new token for each use case. Macaroons support this natively and allows for users to do more complex things without us having to build out tons of support. This implements macaroons from scratch and replaces our existing HMAC Auth. AI assistance: OpenAI Codex; reviews by GPT-6 Astra and Claude Fable 5.1.
1 parent f9bf766 commit 1e9e322

62 files changed

Lines changed: 4508 additions & 482 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ cargo run --bin ldk-server ./contrib/ldk-server-config.toml
1919
## Testing
2020

2121
```bash
22-
cargo test # Run all tests
23-
cargo test --all-features # Run tests with all features
22+
cargo test # Run workspace tests
23+
cargo test --all-features # Run workspace tests with all features
24+
```
25+
26+
Run the end-to-end tests from their separate workspace. Raw gRPC tests require `curl` with
27+
HTTP/2 support (`curl --version` must list `HTTP2`):
28+
29+
```bash
30+
cargo test --manifest-path e2e-tests/Cargo.toml -- --test-threads=4
2431
```
2532

2633
## Code Quality
@@ -50,7 +57,15 @@ cargo fmt --all
5057
2. Regenerate protos (see above)
5158
3. Create handler in `ldk-server/src/api/` (follow existing patterns)
5259
4. Add route in `ldk-server/src/service.rs`
53-
5. Add CLI command in `ldk-server-cli/src/main.rs`
60+
5. Map the RPC to its required permission in `method_authorization` in `ldk-server/src/macaroons/authorization.rs`.
61+
Unmapped methods return `UNIMPLEMENTED`, even for admin tokens.
62+
6. Add CLI command in `ldk-server-cli/src/main.rs`
63+
7. For a unary RPC, add the MCP tool in `ldk-server-mcp/src/tools/` and update the tool list test
64+
in `ldk-server-mcp/tests/integration.rs`. Add a live test in `e2e-tests/tests/mcp.rs` if applicable.
65+
8. Test allowed and denied requests, including admin access.
66+
67+
If the RPC needs a new permission, add it to `ldk-server-grpc/src/permissions.rs` and
68+
`ALL_PERMISSIONS`. Update the presets that need it and add it to `docs/api-guide.md`.
5469

5570
## Configuration
5671

Cargo.lock

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["ldk-server-cli", "ldk-server-client", "ldk-server-grpc", "ldk-server", "ldk-server-mcp"]
3+
members = ["ldk-server-cli", "ldk-server-client", "ldk-server-grpc", "ldk-server", "ldk-server-mcp", "ldk-server-macaroons"]
44
exclude = ["e2e-tests"]
55

66
[profile.release]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ a Lightning node while exposing a robust, language-agnostic API via [Protocol Bu
1919
- `ldk-server-cli`: CLI client for the server API
2020
- `ldk-server-client`: Rust client library for authenticated TLS gRPC calls
2121
- `ldk-server-grpc`: generated protobuf and shared gRPC types
22+
- `ldk-server-macaroons`: shared token parsing, signing, derivation, and request binding
2223
- `ldk-server-mcp`: stdio MCP bridge exposing unary `ldk-server` RPCs as MCP tools
2324

2425
### Features

docs/api-guide.md

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,107 @@ underlying LDK Node documentation.
1515

1616
## Authentication
1717

18-
Every gRPC request must include an `x-auth` metadata header with an HMAC-SHA256 signature:
18+
Each request needs a hex-encoded v2 macaroon in the `macaroon` header:
1919

20+
```text
21+
macaroon: <hex-encoded-request-macaroon>
2022
```
21-
x-auth: HMAC <unix_timestamp>:<hmac_hex>
23+
24+
Keep your original macaroon private. The client uses it to make a token for each request,
25+
as described below. All requests use TLS.
26+
27+
You can add caveats to make a restricted copy of your macaroon without contacting the server.
28+
A caveat limits its permissions, allowed methods, or expiry time. Added caveats can only reduce
29+
access. See [Restrictions](#restrictions) for examples.
30+
31+
### Request binding
32+
33+
The Rust client, CLI, and MCP handle request binding automatically. They keep your macaroon
34+
private and send a copy tied to the request's method, body, and time.
35+
36+
Custom clients must add one final caveat:
37+
38+
```text
39+
request = <unix-seconds> <RpcMethod> <body-sha256>
2240
```
2341

24-
Where:
42+
Use Unix time in seconds, a method name such as `OnchainSend`, and the lowercase SHA-256 hash
43+
of the exact gRPC body, including its five-byte frame header. Rust clients can use
44+
`macaroon::bind_macaroon_to_request`.
45+
46+
Client and server clocks must be within 60 seconds. The token cannot authorize a different
47+
request, but the same request can still be replayed while the token is valid.
48+
49+
See [Request proof format](request-binding.md) for exact encoding rules and an example.
50+
51+
### Restrictions
2552

26-
- `unix_timestamp` is the current time in seconds since the Unix epoch
27-
- `hmac_hex` is the hex-encoded result of
28-
`HMAC-SHA256(api_key_bytes, timestamp_be_bytes || grpc_request_body_bytes)`
29-
- `api_key_bytes` is the API key string encoded as UTF-8 bytes
30-
- `timestamp_be_bytes` is the timestamp as a big-endian 8-byte unsigned integer
31-
- `grpc_request_body_bytes` is the raw gRPC request body sent over HTTP/2, including
32-
the 5-byte gRPC message frame
53+
A caveat is a condition that limits what a macaroon can do. All caveats must pass:
3354

34-
The server rejects requests where the timestamp differs from the server's clock by more than
35-
**60 seconds**.
55+
| Caveat | Meaning |
56+
|--------|---------|
57+
| `permissions = node:read,payments:read` | Allow only these permissions |
58+
| `method = GetNodeInfo` | Allow only this RPC method |
59+
| `time-before = 1800000000` | Expire at this Unix time in seconds |
60+
61+
Added caveats can only reduce access. They cannot restore permissions or extend the expiry time.
62+
63+
Derive a restricted copy without contacting the server:
64+
65+
```bash
66+
ldk-server-cli derive-macaroon "$MACAROON" \
67+
--caveat 'permissions = node:read' \
68+
--caveat 'method = GetNodeInfo' \
69+
--caveat "time-before = $EXPIRY_UNIX_SECONDS"
70+
```
71+
72+
The command prints a hex token. Rust clients can use `macaroon::derive_macaroon`.
73+
Give the copy to the application and keep the original private.
74+
75+
### Create and revoke tokens
76+
77+
Use `CreateMacaroon` to give each client a token you can revoke separately. New tokens keep
78+
all the caller's restrictions. Revoking the caller's token does not revoke these new tokens.
79+
80+
Copies made with `derive-macaroon` share the original token's ID. Revoking that ID blocks
81+
all those copies. The server cannot list copies made locally.
82+
83+
Revocation and expiry block new requests. Existing event streams stay open until the client
84+
disconnects or the server stops. Reconnecting requires a valid token.
85+
86+
See [Macaroon Management](#macaroon-management) for the RPCs and
87+
[Operations](operations.md#macaroons) for storage and recovery.
88+
89+
### Macaroon Permissions
90+
91+
Choose the permissions each client needs, or use `admin` by itself for full access.
92+
The CLI also has `readonly`, `invoice`, and `admin` presets.
93+
RPCs with no permission mapping return `UNIMPLEMENTED`, even for admin tokens.
94+
95+
| Permission | Access |
96+
| ---------- | ------ |
97+
| `node:read` | Node information, balances, and pathfinding scores |
98+
| `onchain:receive` | Create on-chain receive addresses |
99+
| `onchain:send` | Send on-chain funds |
100+
| `invoices:create` | Create BOLT11/BOLT12 invoices and incoming refund requests |
101+
| `payments:read` | Read payments and forwarded payments |
102+
| `payments:claim` | Claim or fail held BOLT11 payments |
103+
| `payments:send` | Send BOLT11, BOLT12, spontaneous, unified, and refund payments |
104+
| `channels:read` | List channels |
105+
| `channels:manage` | Open, configure, or cooperatively close channels |
106+
| `channels:splice` | Splice funds in or out, including to an external address |
107+
| `channels:force_close` | Force-close channels |
108+
| `peers:read` | List peers |
109+
| `peers:manage` | Connect or disconnect peers |
110+
| `messages:sign` | Sign messages and create BOLT12 payer proofs |
111+
| `messages:verify` | Verify message signatures |
112+
| `graph:read` | Read network graph data |
113+
| `utilities:read` | Decode invoices and offers |
114+
| `events:read` | Subscribe to the event stream |
115+
| `macaroons:manage` | Create, list, and revoke macaroons within your permissions |
116+
117+
MCP provides token management through `create_macaroon`, `list_macaroons`, `revoke_macaroon`,
118+
and `get_permissions`.
36119

37120
## TLS
38121

@@ -67,9 +150,10 @@ Errors are returned as standard gRPC status codes:
67150
| gRPC Code | Meaning |
68151
|---------------------------|------------------------------------------------------------------|
69152
| `INVALID_ARGUMENT` (3) | Malformed request or invalid parameters |
153+
| `PERMISSION_DENIED` (7) | Missing permission or a caveat that does not pass |
70154
| `FAILED_PRECONDITION` (9) | Lightning operation error (e.g., insufficient balance, no route) |
71155
| `INTERNAL` (13) | Server-side bug |
72-
| `UNAUTHENTICATED` (16) | Missing or invalid `x-auth` header |
156+
| `UNAUTHENTICATED` (16) | Missing, invalid, or revoked macaroon |
73157

74158
The `grpc-message` trailer contains a human-readable error description.
75159

@@ -232,6 +316,18 @@ Use events as notifications. After reconnecting, reconcile recoverable state wit
232316
`GetPaymentDetails`, `ListPayments`, `ListForwardedPayments`, and `ListChannels`. Some event fields
233317
cannot be recovered through these APIs.
234318

319+
### Macaroon Management
320+
321+
| RPC | Description |
322+
|-----|-------------|
323+
| `CreateMacaroon` | Create a macaroon and return its private hex token in `token` |
324+
| `ListMacaroons` | List IDs, names, permissions, and caveats, without secrets |
325+
| `RevokeMacaroon` | Revoke a macaroon by ID |
326+
| `GetPermissions` | Show the caller's ID, name, usable permissions, and caveats |
327+
328+
The first three RPCs require `macaroons:manage` or `admin`. You can only create or revoke
329+
tokens whose permissions you have. The last unrestricted admin token cannot be revoked.
330+
235331
### Metrics
236332

237333
Metrics are served as a plain HTTP GET endpoint (not gRPC):

docs/configuration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,11 @@ Two resolution methods are supported via the `mode` field:
207207
tls.crt # TLS certificate (PEM)
208208
tls.key # TLS private key (PEM)
209209
<network>/ # e.g., bitcoin/, regtest/, signet/
210-
api_key # API key
210+
macaroons/
211+
admin.macaroon # Admin token (0400)
212+
roots/ # Private root keys (0700)
213+
admin.toml # Admin root key and permissions (0400)
214+
<id>.toml # Root key and permissions from CreateMacaroon (0400)
211215
ldk-server.log # Log file
212216
ldk_node_data.sqlite # LDK Node state (channels, wallet, payments)
213217
ldk_server_data.sqlite # Forwarded-payment history

docs/getting-started.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,32 @@ gRPC service listening on 127.0.0.1:3536
7373
NODE_URI: <node_id>@<address>
7474
```
7575

76-
Two files are auto-generated on first run:
76+
The server creates these files on first run:
7777

78-
| File | Location | Purpose |
79-
|-----------------|-----------------------------------|------------------------------------------|
80-
| API key | `<storage_dir>/<network>/api_key` | 32-byte random key (stored as raw bytes) |
81-
| TLS certificate | `<storage_dir>/tls.crt` | Self-signed ECDSA P-256 certificate |
78+
| File | Location | Purpose |
79+
|------|----------|---------|
80+
| Admin macaroon | `<storage_dir>/<network>/macaroons/admin.macaroon` | Full API access |
81+
| TLS certificate | `<storage_dir>/tls.crt` | Secure client connections |
8282

8383
The default storage directory is `~/.ldk-server/` on Linux and
8484
`~/Library/Application Support/ldk-server/` on macOS.
8585

86-
### Reading the API Key
86+
### Client Macaroons
8787

88-
The API key file contains raw bytes. To get the hex string the CLI and client library expect:
88+
The CLI reads `admin.macaroon` automatically. This file contains a hex token.
89+
Keep it private, and never give clients files from `macaroons/roots/`.
90+
91+
Create a restricted token for each application:
8992

9093
```bash
91-
xxd -p -c 64 ~/.ldk-server/bitcoin/api_key
94+
ldk-server-cli create-macaroon my-app --preset readonly
95+
ldk-server-cli create-macaroon invoice-app --preset invoice
9296
```
9397

9498
## First Commands
9599

96100
If the CLI and server share the same machine and use the default storage directory, the CLI
97-
auto-discovers the API key and TLS certificate, so no flags are needed:
101+
auto-discovers the macaroon and TLS certificate, so no flags are needed:
98102

99103
```bash
100104
# Check the node is running
@@ -113,7 +117,7 @@ details explicitly:
113117
```bash
114118
ldk-server-cli \
115119
--base-url localhost:3536 \
116-
--api-key <hex_api_key> \
120+
--macaroon <hex_macaroon> \
117121
--tls-cert /path/to/tls.crt \
118122
get-node-info
119123
```

docs/operations.md

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to ma
6060

6161
- Network graph data (re-synced from gossip or RGS)
6262
- Fee rate cache (re-fetched from the chain backend)
63-
- The API key (can be regenerated, but clients will need the new one)
63+
- Macaroon credentials (can be replaced, but clients will need new tokens)
6464
- The TLS certificate (can be regenerated, but clients will need the new one)
6565

6666
> **Warning:** Do not restore a backup onto two running nodes simultaneously. Running the
@@ -69,13 +69,39 @@ the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to ma
6969
7070
## Security
7171

72-
### API Key
72+
### Macaroons
7373

74-
- Auto-generated as 32 random bytes on first startup
75-
- Stored at `<network_dir>/api_key` with `0400` permissions (read-only for owner)
76-
- The hex-encoded form of this key is used for HMAC authentication
77-
- Treat it as a secret: anyone with the API key and network access to the gRPC port can
78-
control the node
74+
Keep `<network_dir>/macaroons/` private. It contains the default admin token in `admin.macaroon`
75+
and the server's root keys in `roots/`. Give clients tokens, never root keys.
76+
77+
Use `create-macaroon` to give each client a token you can revoke separately. Use `derive-macaroon`
78+
to make a restricted copy. Give each client only the permissions it needs.
79+
See the [API guide](api-guide.md#authentication) for restrictions and request binding.
80+
81+
To replace an admin token, create and save a new admin token, then revoke the old ID.
82+
Replace `admin.macaroon` with the new token or pass it with `--macaroon`.
83+
The API prevents revocation of the last unrestricted admin token.
84+
85+
#### Recovery
86+
87+
Back up `roots/` and `admin.macaroon` together. Old root files can restore revoked access.
88+
Deleting all roots invalidates every token; the server creates a new admin token on restart.
89+
90+
At startup, the server repairs a missing or invalid `admin.macaroon` from `roots/admin.toml`
91+
and logs the change. It keeps valid tokens, even if restricted or expired. It warns if the
92+
file holds a request token; replace that file with a reusable token.
93+
94+
If the original admin root is gone but other roots remain, the server warns instead of replacing
95+
it. Use another admin token to create a replacement and save it as `admin.macaroon`.
96+
97+
Duplicate root names or IDs stop startup. Move conflicting files out of `roots/` and restart.
98+
Files ending in `.tmp` are ignored.
99+
100+
Root-file caveat edits take effect after restart. `GetPermissions` shows them, and newly issued
101+
tokens inherit them. Tokens issued earlier have separate roots and do not change.
102+
103+
The server logs successful token creation and revocation, including who made the change and
104+
which token it affects. Logs contain no tokens or root secrets.
79105

80106
### TLS
81107

@@ -188,7 +214,7 @@ To allow clients to connect from other machines:
188214
(e.g., `0.0.0.0:3536`).
189215
3. **Distribute the TLS certificate:** Copy `<storage_dir>/tls.crt` to each client machine.
190216
Clients must pin this certificate since it is self-signed.
191-
4. **Share the API key:** Provide the hex-encoded API key to authorized clients.
217+
4. **Share the macaroon:** Provide the hex-encoded macaroon to authorized clients.
192218

193219
If you regenerate the TLS certificate (by deleting `tls.crt` and `tls.key` and restarting),
194220
all clients will need the new certificate.

0 commit comments

Comments
 (0)