macaroons+rpcperms: add protector caveats to restrict RPC request fields - #11117
macaroons+rpcperms: add protector caveats to restrict RPC request fields#11117GeorgeTsagk wants to merge 5 commits into
Conversation
Add a new first-party caveat condition, "protector <profile-name>", that references a named, compiled-in protector profile. A protector profile restricts which request fields may be set on the RPC methods it covers, allowing a macaroon to grant access to a method while denying specific dangerous parameters, for example fields that could redirect channel funds to a third party. The checker added here is the bakery level half of the mechanism: it verifies that the referenced profile name is well formed and known to this lnd instance, so a macaroon referencing an unknown profile is rejected as a whole. That makes future profile names, and older lnd versions that know no profiles at all, fail closed rather than validate without the intended restrictions. The field level enforcement itself runs in the RPC interceptor chain, where the request message is available. Caveats are decoded with checkers.ParseCaveat, the same parser the bakery uses for checking, so bake time encoding and enforcement time decoding cannot drift apart. Caveats that are almost, but not exactly, the canonical "protector <name>" encoding (wrong letter case, or a separator other than the single space) fail closed instead of being silently ignored, while genuinely different conditions that merely share the prefix are left alone. Fuzz targets assert that the parser never panics, never returns an invalid profile name without an error and never silently drops a well formed protector caveat.
Add a pair of interceptors that enforce the field rules of a macaroon's protector caveats against the request message, and register the protector checker with the macaroon service. The interceptors are placed after (inside of) both the macaroon and the middleware interceptors. After the macaroon interceptor because enforcement assumes an already validated macaroon, and after the middleware interceptors because a registered RPC middleware may replace the request message: enforcing earlier would judge the original request while the handler executes the replacement. For the same reason the protector stream wrapper wraps the middleware stream wrapper, so per-message enforcement sees the message after any replacement. Enforcement deliberately does not run inside the validator dispatch of checkMacaroon, so an external macaroon validator that knows nothing about protector caveats cannot be used to bypass the field rules by accepting the macaroon. As a consequence the credential is resolved independently, and any ambiguity there fails closed: a request carrying more than one macaroon metadata value, an unparseable macaroon, a malformed protector caveat or an unknown profile name are all rejected. This strict contract is documented on RegisterExternalValidator. OpenChannel and CloseChannel are server streaming, so their request message only exists after stream open. Streams whose macaroon carries a protector caveat covering the called method get their RecvMsg wrapped; all other streams are passed through unwrapped, and the profiles are resolved once at wrap time, so there is no per-message macaroon parsing and no cost at all on normal traffic. Field rules fail closed by construction: a populated field that a rule table does not explicitly classify as allowed, denied or nested is rejected, a request whose proto type does not match the rule set is rejected, and a covered method whose request cannot be inspected as a proto message is rejected. Denials carry the PermissionDenied gRPC status code. Methods not covered by any of the macaroon's profiles pass without inspection, so externally registered services whose request types lnd cannot inspect keep working. This commit also adds the first profile, channel-management-v1: channel management without the ability to redirect value to a third party. It denies push_sat, close_address and funding_shim on OpenChannel, OpenChannelSync and BatchOpenChannel (per batched channel), and delivery_address on CloseChannel. UpdateChannelPolicy is covered with all fields vetted as safe, which documents the review and reserves the slot for future tightening. Profile rules are code owned: rules under an existing profile name may only ever be tightened, never loosened. A unit test walks the proto descriptors of every covered message and requires every field to be classified as exactly one of allowed, denied or nested, so a request field added in a future release cannot ship unclassified. The same test verifies that every rule references an existing field, catching typos that would otherwise silently disable a denial, and that every method URI is bound to the correct request message type.
The flag can be specified multiple times and attaches one protector caveat per given profile name. Like the other constraint flags it is applied client side, so it also works for offline baking with --root_key and for tightening existing macaroons through constrainmacaroon.
Cover the protector caveat end to end against a real node: the deny matrix for push_sat, close_address and delivery_address on both unary and streaming methods, batch open enforcement inside the repeated channels field, fail closed rejection of unknown profile names, a control case proving the same request passes with an unconstrained macaroon, and the positive path where a protected macaroon opens a channel, updates the channel policy and cooperatively closes it. The test also registers a real request rewriting RPC middleware and asserts that the rewritten request is the one judged by the field rules, which pins the interceptor ordering on the real server rather than in a hand built chain.
🔴 PR Severity: CRITICAL
🟠 High (2 files)
🟡 Medium (4 files)
🟢 Low (7 files)
AnalysisThe highest individually-classified tier is HIGH, driven by This PR introduces a substantial new macaroon/RPC-permission "protector" mechanism spanning both the To override, add a |
|
Extra notes for reviewer context, from a security-focused pass over this
|
Add a section to the macaroon documentation describing the caveat, the channel-management-v1 profile and the tighten-only versioning contract, plus an explicit limitations subsection: only the methods a profile covers are constrained (so the caveat must be paired with uri: scoped permissions), a protector caveat must never be combined with macaroon:generate because such a macaroon can bake itself a replacement without the caveat, the guarantee is about redirection rather than value loss in general (fee rate fields and force closes stay available), and profiles place no restriction on counterparties. The release notes entry explains the mechanism (name in the caveat, rules in lnd, tighten-only, fail closed on unknown profiles), how to attach the caveat, and the same limitations.
c1511c6 to
63253e0
Compare
|
This is great 🙏, thanks for working on this. This PR should close my feature request here: #10336 |
Change Description
This PR adds a first-party macaroon caveat,
protector <profile-name>, thatrestricts which request fields may be set on the RPC methods covered by a
named, compiled-in profile. Permissions decide which methods a macaroon may
call; protector caveats add a finer layer inside those methods.
Motivation: delegating channel management to a third party (a liquidity or
rebalancing service, an operator's own tooling, an LSP agent) without also
handing over the ability to redirect channel funds. Today a macaroon that can
call
OpenChannelcan also setpush_satorclose_address, and one that cancall
CloseChannelcan setdelivery_address, so "may manage channels"implies "may move value to an arbitrary destination".
Design
makes the guarantee upgradeable: a release can tighten a profile (for example
when a new value-carrying field is added to a request) and every macaroon
already issued benefits on upgrade.
never loosened. New semantics require a new name (
channel-management-v2).a whole, and an older lnd rejects the caveat as unrecognized. Both directions
fail closed.
lncli bakemacaroon --protector) or appended offline toan existing macaroon (
lncli constrainmacaroon --protector).First profile:
channel-management-v1Guarantee: the covered methods cannot redirect value to a third party.
OpenChannel,OpenChannelSyncpush_sat,close_address,funding_shimBatchOpenChannelpush_sat,close_address(per batched channel)CloseChanneldelivery_addressUpdateChannelPolicyNote the open/close pairing:
close_addresssets the upfront shutdown script alater cooperative close pays to, so denying
delivery_addressis onlymeaningful if
close_addressis denied at open time too. Out of scope for v1:fee-based value burn and peer restrictions.
Enforcement, and the two parts worth reviewer scrutiny
Enforcement is a dedicated pair of interceptors placed after (inside of) both
the macaroon and the middleware interceptors:
may replace the request message. Enforcing at validation time would judge
the original request while the handler executes the replacement. The
protector stream wrapper wraps the middleware stream wrapper for the same
reason.
ExternalValidatorthatknows nothing about protector caveats cannot be used to accept a macaroon
and thereby skip the field rules. Because the credential is then resolved
independently, ambiguity there fails closed: more than one
macaroonmetadata value, an unparseable macaroon, a malformed caveat or an unknown
profile are all rejected.
OpenChannelandCloseChannelare server-streaming, so the request onlyexists after stream open. Streams whose macaroon carries a covering profile get
RecvMsgwrapped, with profiles resolved once at wrap time; all other streamspass through unwrapped, so there is no cost on normal traffic.
Rules fail closed by construction: a populated field not explicitly classified
as allowed, denied or nested is rejected, as is a request of the wrong proto
type or one that cannot be inspected on a covered method. Denials use
codes.PermissionDenied. A unit test additionally walks the proto descriptorsof every covered message and requires every field to be classified, so a field
added in a later release fails CI until someone vets it rather than silently
falling outside a deny-list.
Scoping
A profile constrains only the methods it lists; uncovered methods are
unaffected. Narrowing the callable set stays the job of permissions, which also
lets several profiles combine on one macaroon. In practice the caveat is meant
to be paired with
uri:ops:lncli bakemacaroon --protector channel-management-v1 --timeout 7776000 \ uri:/lnrpc.Lightning/GetInfo uri:/lnrpc.Lightning/ListChannels \ uri:/lnrpc.Lightning/ListPeers uri:/lnrpc.Lightning/ConnectPeer \ uri:/lnrpc.Lightning/OpenChannel uri:/lnrpc.Lightning/OpenChannelSync \ uri:/lnrpc.Lightning/BatchOpenChannel uri:/lnrpc.Lightning/CloseChannel \ uri:/lnrpc.Lightning/UpdateChannelPolicyWith broad entity permissions instead, uncovered methods such as
SendCoinsremain fully usable. This is stated in the flag help,
docs/macaroons.mdandthe release notes.
Compatibility
Additive: no proto or database changes, and no behavior change for macaroons
without a protector caveat. One narrowing worth flagging: on non-whitelisted
methods, a present
macaroonmetadata value must now contain exactly one hexencoded, parseable macaroon. This closes a fail-open path where a lenient
external validator could accept a request whose credential enforcement could
not resolve. Documented on
RegisterExternalValidator.Follow-up, not in this PR: a
listprotectors-style RPC so a profile's rules canbe read from a running node, since
printmacaroonshows the name but not itsmeaning.
Steps to Test
The itest covers the deny matrix on unary and streaming methods, batch open,
unknown profiles, a control case with an unconstrained macaroon, a full
open/policy-update/coop-close lifecycle through a protected macaroon, and a
registered request-rewriting middleware asserting the replaced request is the
one judged.
Manually, against a regtest node with a funded wallet and a peer: bake as
above, then
I ran this on a local regtest, including a control showing the identical
--push_amt 2000000request succeeds with an unconstrained macaroon and reallydoes move 2,000,000 sats to the peer, which is the transfer the caveat blocks.
Design questions for reviewers
does not cover, leaving method scoping to
uri:ops. The alternative is adefault of "deny any write-capable method the profile has not vetted",
derived from the permission map: stricter and self-defending against an
over-broad bake, at the cost of coupling profiles to the entity/action model.
Happy to switch if preferred.
the field list in the caveat. I chose the former so profiles tighten on
upgrade and
printmacaroonoutput stays auditable, at the cost of holdersnot being able to invent their own field restrictions offline.
Pull Request Checklist
Testing
Code Style and Documentation