Merge pull request #9546 from hieblmi/macaroon-ip-cidr-constraint

macaroons: ip range constraint
This commit is contained in:
Oliver Gugger authored and GitHub committed 2025-03-14 10:22:43 -06:00
commit 053d63e110
5 files changed
+143 -20

No files matched your search

+17
View File
@@ -30,6 +30,10 @@ var (
Name: "ip_address", Name: "ip_address",
Usage: "the IP address the macaroon will be bound to", Usage: "the IP address the macaroon will be bound to",
} }
macIPRangeFlag = cli.StringFlag{
Name: "ip_range",
Usage: "the IP range the macaroon will be bound to",
}
macCustomCaveatNameFlag = cli.StringFlag{ macCustomCaveatNameFlag = cli.StringFlag{
Name: "custom_caveat_name", Name: "custom_caveat_name",
Usage: "the name of the custom caveat to add", Usage: "the name of the custom caveat to add",
@@ -557,6 +561,19 @@ func applyMacaroonConstraints(ctx *cli.Context,
) )
} }
if ctx.IsSet(macIPRangeFlag.Name) {
_, net, err := net.ParseCIDR(ctx.String(macIPRangeFlag.Name))
if err != nil {
return nil, fmt.Errorf("unable to parse ip_range "+
"%s: %w", ctx.String("ip_range"), err)
}
macConstraints = append(
macConstraints,
macaroons.IPLockConstraint(net.String()),
)
}
if ctx.IsSet(macCustomCaveatNameFlag.Name) { if ctx.IsSet(macCustomCaveatNameFlag.Name) {
customCaveatName := ctx.String(macCustomCaveatNameFlag.Name) customCaveatName := ctx.String(macCustomCaveatNameFlag.Name)
if containsWhiteSpace(customCaveatName) { if containsWhiteSpace(customCaveatName) {
+1 -1
View File
@@ -472,7 +472,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
} }
macaroonService, err = macaroons.NewService( macaroonService, err = macaroons.NewService(
rootKeyStore, "lnd", walletInitParams.StatelessInit, rootKeyStore, "lnd", walletInitParams.StatelessInit,
macaroons.IPLockChecker, macaroons.IPLockChecker, macaroons.IPRangeLockChecker,
macaroons.CustomChecker(interceptorChain), macaroons.CustomChecker(interceptorChain),
) )
if err != nil { if err != nil {
@@ -166,6 +166,10 @@
on the channel. LND will disable the channel for new HTLCs and kick off the on the channel. LND will disable the channel for new HTLCs and kick off the
cooperative close flow automatically when the channel has no HTLCs left. cooperative close flow automatically when the channel has no HTLCs left.
* [A new macaroon constraint](https://github.com/lightningnetwork/lnd/pull/9546)
to allow for restriction of access based on an IP range. Prior to this only
specific IPs could be allowed or denied.
# Improvements # Improvements
## Functional Updates ## Functional Updates
+56 -16
View File
@@ -37,9 +37,8 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
name string name string
run func(ctxt context.Context, t *testing.T) run func(ctxt context.Context, t *testing.T)
}{{ }{{
// First test: Make sure we get an error if we use no macaroons // Make sure we get an error if we use no macaroons but try to
// but try to connect to a node that has macaroon authentication // connect to a node that has macaroon authentication enabled.
// enabled.
name: "no macaroon", name: "no macaroon",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
conn, err := testNode.ConnectRPCWithMacaroon(nil) conn, err := testNode.ConnectRPCWithMacaroon(nil)
@@ -51,8 +50,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "expected 1 macaroon") require.Contains(t, err.Error(), "expected 1 macaroon")
}, },
}, { }, {
// Second test: Ensure that an invalid macaroon also triggers an // Ensure that an invalid macaroon also triggers an error.
// error.
name: "invalid macaroon", name: "invalid macaroon",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
invalidMac, _ := macaroon.New( invalidMac, _ := macaroon.New(
@@ -68,8 +66,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "invalid ID") require.Contains(t, err.Error(), "invalid ID")
}, },
}, { }, {
// Third test: Try to access a write method with read-only // Try to access a write method with read-only macaroon.
// macaroon.
name: "read only macaroon", name: "read only macaroon",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
readonlyMac, err := testNode.ReadMacaroon( readonlyMac, err := testNode.ReadMacaroon(
@@ -85,8 +82,8 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "permission denied") require.Contains(t, err.Error(), "permission denied")
}, },
}, { }, {
// Fourth test: Check first-party caveat with timeout that // Check first-party caveat with timeout that expired 30 seconds
// expired 30 seconds ago. // ago.
name: "expired macaroon", name: "expired macaroon",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
readonlyMac, err := testNode.ReadMacaroon( readonlyMac, err := testNode.ReadMacaroon(
@@ -106,7 +103,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "macaroon has expired") require.Contains(t, err.Error(), "macaroon has expired")
}, },
}, { }, {
// Fifth test: Check first-party caveat with invalid IP address. // Check first-party caveat with invalid IP address.
name: "invalid IP macaroon", name: "invalid IP macaroon",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
readonlyMac, err := testNode.ReadMacaroon( readonlyMac, err := testNode.ReadMacaroon(
@@ -128,7 +125,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "different IP address") require.Contains(t, err.Error(), "different IP address")
}, },
}, { }, {
// Sixth test: Make sure that if we do everything correct and // Make sure that if we do everything correct and
// send the admin macaroon with first-party caveats that we can // send the admin macaroon with first-party caveats that we can
// satisfy, we get a correct answer. // satisfy, we get a correct answer.
name: "correct macaroon", name: "correct macaroon",
@@ -149,8 +146,51 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
assert.Contains(t, res.Address, "bcrt1") assert.Contains(t, res.Address, "bcrt1")
}, },
}, { }, {
// Seventh test: Bake a macaroon that can only access exactly // Check first-party caveat with invalid IP range.
// two RPCs and make sure it works as expected. name: "invalid IP range macaroon",
run: func(ctxt context.Context, t *testing.T) {
readonlyMac, err := testNode.ReadMacaroon(
testNode.Cfg.ReadMacPath, defaultTimeout,
)
require.NoError(t, err)
invalidIPRangeMac, err := macaroons.AddConstraints(
readonlyMac, macaroons.IPRangeLockConstraint(
"1.1.1.1/32",
),
)
require.NoError(t, err)
cleanup, client := macaroonClient(
t, testNode, invalidIPRangeMac,
)
defer cleanup()
_, err = client.GetInfo(ctxt, infoReq)
require.Error(t, err)
require.Contains(t, err.Error(), "different IP range")
},
}, {
// Make sure that if we do everything correct and send the admin
// macaroon with first-party caveats that we can satisfy, we get
// a correct answer.
name: "correct macaroon",
run: func(ctxt context.Context, t *testing.T) {
adminMac, err := testNode.ReadMacaroon(
testNode.Cfg.AdminMacPath, defaultTimeout,
)
require.NoError(t, err)
adminMac, err = macaroons.AddConstraints(
adminMac, macaroons.TimeoutConstraint(30),
macaroons.IPRangeLockConstraint("127.0.0.0/8"),
)
require.NoError(t, err)
cleanup, client := macaroonClient(t, testNode, adminMac)
defer cleanup()
res, err := client.NewAddress(ctxt, newAddrReq)
require.NoError(t, err, "get new address")
assert.Contains(t, res.Address, "bcrt1")
},
}, {
// Bake a macaroon that can only access exactly two RPCs and
// make sure it works as expected.
name: "custom URI permissions", name: "custom URI permissions",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
entity := macaroons.PermissionEntityCustomURI entity := macaroons.PermissionEntityCustomURI
@@ -199,9 +239,9 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Contains(t, err.Error(), "permission denied") require.Contains(t, err.Error(), "permission denied")
}, },
}, { }, {
// Eighth test: check that with the CheckMacaroonPermissions // Check that with the CheckMacaroonPermissions RPC, we can
// RPC, we can check that a macaroon follows (or doesn't) // check that a macaroon follows (or doesn't) permissions and
// permissions and constraints. // constraints.
name: "unknown permissions", name: "unknown permissions",
run: func(ctxt context.Context, t *testing.T) { run: func(ctxt context.Context, t *testing.T) {
// A test macaroon created with permissions from pool, // A test macaroon created with permissions from pool,
+65 -3
View File
@@ -3,6 +3,7 @@ package macaroons
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@@ -21,6 +22,10 @@ const (
// in the serialized macaroon. We choose a single space as the delimiter // in the serialized macaroon. We choose a single space as the delimiter
// between the because that is also used by the macaroon bakery library. // between the because that is also used by the macaroon bakery library.
CondLndCustom = "lnd-custom" CondLndCustom = "lnd-custom"
// CondIPRange is the caveat condition name that is used for tying an IP
// range to a macaroon.
CondIPRange = "iprange"
) )
// CustomCaveatAcceptor is an interface that contains a single method for // CustomCaveatAcceptor is an interface that contains a single method for
@@ -80,9 +85,9 @@ func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
} }
} }
// IPLockConstraint locks macaroon to a specific IP address. // IPLockConstraint locks a macaroon to a specific IP address. If ipAddr is an
// If address is an empty string, this constraint does nothing to // empty string, this constraint does nothing to accommodate default value's
// accommodate default value's desired behavior. // desired behavior.
func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
return func(mac *macaroon.Macaroon) error { return func(mac *macaroon.Macaroon) error {
if ipAddr != "" { if ipAddr != "" {
@@ -93,8 +98,32 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
} }
caveat := checkers.Condition("ipaddr", caveat := checkers.Condition("ipaddr",
macaroonIPAddr.String()) macaroonIPAddr.String())
return mac.AddFirstPartyCaveat([]byte(caveat)) return mac.AddFirstPartyCaveat([]byte(caveat))
} }
return nil
}
}
// IPRangeLockConstraint locks a macaroon to a specific IP address range. If
// ipRange is an empty string, this constraint does nothing to accommodate
// default value's desired behavior.
func IPRangeLockConstraint(ipRange string) func(*macaroon.Macaroon) error {
return func(mac *macaroon.Macaroon) error {
if ipRange != "" {
_, parsedNet, err := net.ParseCIDR(ipRange)
if err != nil {
return fmt.Errorf("incorrect macaroon IP "+
"range: %w", err)
}
caveat := checkers.Condition(
CondIPRange, parsedNet.String(),
)
return mac.AddFirstPartyCaveat([]byte(caveat))
}
return nil return nil
} }
} }
@@ -122,6 +151,39 @@ func IPLockChecker() (string, checkers.Func) {
} }
} }
// IPRangeLockChecker accepts client IP range from the validation context and
// compares it with the IP range locked in the macaroon. It is of the `Checker`
// type.
func IPRangeLockChecker() (string, checkers.Func) {
return CondIPRange, func(ctx context.Context, cond, arg string) error {
// Get peer info and extract IP range from it for macaroon
// check.
pr, ok := peer.FromContext(ctx)
if !ok {
return errors.New("unable to get peer info from " +
"context")
}
peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
if err != nil {
return fmt.Errorf("unable to parse peer address: %w",
err)
}
_, ipNet, err := net.ParseCIDR(arg)
if err != nil {
return fmt.Errorf("unable to parse macaroon IP "+
"range: %w", err)
}
if !ipNet.Contains(net.ParseIP(peerAddr)) {
return errors.New("macaroon locked to different " +
"IP range")
}
return nil
}
}
// CustomConstraint returns a function that adds a custom caveat condition to // CustomConstraint returns a function that adds a custom caveat condition to
// a macaroon. // a macaroon.
func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error { func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {