Skip to content

Commit aa2d60f

Browse files
authored
Merge pull request #276 from benthecarman/macaroons
Replace HMAC Auth with scoped macaroons
2 parents db34578 + ff9ed6e commit aa2d60f

61 files changed

Lines changed: 4541 additions & 479 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: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ 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:
27+
28+
```bash
29+
cargo test --manifest-path e2e-tests/Cargo.toml -- --test-threads=4
2430
```
2531

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

5569
## Configuration
5670

‎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 this RPC method and `GetPermissions` |
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+
`GetPermissions` ignores method restrictions, but still requires a valid, unexpired macaroon.
63+
64+
Derive a restricted copy without contacting the server:
65+
66+
```bash
67+
ldk-server-cli derive-macaroon "$MACAROON" \
68+
--caveat 'permissions = node:read' \
69+
--caveat 'method = GetNodeInfo' \
70+
--caveat "time-before = $EXPIRY_UNIX_SECONDS"
71+
```
72+
73+
The command prints a hex token. Rust clients can use `macaroon::derive_macaroon`.
74+
Give the copy to the application and keep the original private.
75+
76+
### Create and revoke tokens
77+
78+
Use `CreateMacaroon` to give each client a token you can revoke separately. New tokens keep
79+
all the caller's restrictions. Revoking the caller's token does not revoke these new tokens.
80+
81+
Copies made with `derive-macaroon` share the original token's ID. Revoking that ID blocks
82+
all those copies. The server cannot list copies made locally.
83+
84+
Revocation and expiry block new requests. Existing event streams stay open until the client
85+
disconnects or the server stops. Reconnecting requires a valid token.
86+
87+
See [Macaroon Management](#macaroon-management) for the RPCs and
88+
[Operations](operations.md#macaroons) for storage and recovery.
89+
90+
### Macaroon Permissions
91+
92+
Choose the permissions each client needs, or use `admin` by itself for full access.
93+
The CLI also has `readonly`, `invoice`, and `admin` presets.
94+
RPCs with no permission mapping return `UNIMPLEMENTED`, even for admin tokens.
95+
96+
| Permission | Access |
97+
| ---------- | ------ |
98+
| `node:read` | Node information, balances, and pathfinding scores |
99+
| `onchain:receive` | Create on-chain receive addresses |
100+
| `onchain:send` | Send on-chain funds |
101+
| `invoices:create` | Create BOLT11/BOLT12 invoices and incoming refund requests |
102+
| `payments:read` | Read payments, forwarded payments, and forwarding statistics |
103+
| `payments:claim` | Claim or fail held BOLT11 payments |
104+
| `payments:send` | Send BOLT11, BOLT12, spontaneous, unified, and refund payments; splice out |
105+
| `channels:read` | List channels |
106+
| `channels:manage` | Open, configure, cooperatively close, or splice funds into channels |
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

@@ -237,6 +321,18 @@ Use events as notifications. After reconnecting, reconcile recoverable state wit
237321
`GetPaymentDetails`, `ListPayments`, `ListForwardedPayments`, and `ListChannels`. Some event fields
238322
cannot be recovered through these APIs.
239323

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

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

‎docs/configuration.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ cooldown_secs = 3600
115115

116116
Where local ldk-server data is stored. Defaults to `~/.ldk-server/` on Linux and
117117
`~/Library/Application Support/ldk-server/` on macOS. This directory is still used for
118-
the node mnemonic, API key, TLS material, and logs when LDK Node state and history
118+
the node mnemonic, macaroon credentials, TLS material, and logs when LDK Node state and history
119119
use PostgreSQL.
120120

121121
### `[storage.postgres]`
@@ -257,7 +257,11 @@ Two resolution methods are supported via the `mode` field:
257257
tls.crt # TLS certificate (PEM)
258258
tls.key # TLS private key (PEM)
259259
<network>/ # e.g., bitcoin/, regtest/, signet/
260-
api_key # API key
260+
macaroons/
261+
admin.macaroon # Admin token (0400)
262+
roots/ # Private root keys (0700)
263+
admin.toml # Admin root key and permissions (0400)
264+
<id>.toml # Root key and permissions from CreateMacaroon (0400)
261265
ldk-server.log # Log file
262266
ldk_node_postgres.lock # Present when LDK Node state uses PostgreSQL
263267
ldk_node_data.sqlite # LDK Node state, payments, and forwarding history
@@ -271,4 +275,4 @@ guidance.
271275

272276
When `[storage.postgres]` is configured, LDK Node wallet state, channel state, payment
273277
history, and forwarding history are stored in PostgreSQL instead of `ldk_node_data.sqlite`.
274-
The storage directory remains required for the mnemonic, API key, TLS material, and logs.
278+
The storage directory remains required for the mnemonic, macaroon credentials, TLS material, and logs.

‎docs/getting-started.md‎

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

74-
Two files are auto-generated on first run:
74+
The server creates these files on first run:
7575

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

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

84-
### Reading the API Key
84+
### Client Macaroons
8585

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

8891
```bash
89-
xxd -p -c 64 ~/.ldk-server/bitcoin/api_key
92+
ldk-server-cli create-macaroon my-app --preset readonly
93+
ldk-server-cli create-macaroon invoice-app --preset invoice
9094
```
9195

9296
## First Commands
9397

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

97101
```bash
98102
# Check the node is running
@@ -111,7 +115,7 @@ details explicitly:
111115
```bash
112116
ldk-server-cli \
113117
--base-url localhost:3536 \
114-
--api-key <hex_api_key> \
118+
--macaroon <hex_macaroon> \
115119
--tls-cert /path/to/tls.crt \
116120
get-node-info
117121
```

‎docs/operations.md‎

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

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

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

71-
### API Key
71+
### Macaroons
7272

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

79105
### TLS
80106

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

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

0 commit comments

Comments
 (0)