Merge pull request #9546 from hieblmi/macaroon-ip-cidr-constraint
macaroons: ip range constraint
This commit is contained in:
5 files changed
+143
-20
No files matched your search
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user