Browse Source

loopd: add macaroon authentication to the daemon's server connection

To secure access to loop's RPC server, we add a macaroon authentication
service and its gRPC interceptors to the daemon's server connection.
pull/281/head
Oliver Gugger 9 months ago
parent
commit
751c70e6c7
No known key found for this signature in database GPG Key ID: 8E4256593F177720
4 changed files with 244 additions and 9 deletions
  1. +1
    -0
      go.mod
  2. +22
    -4
      loopd/config.go
  3. +29
    -5
      loopd/daemon.go
  4. +192
    -0
      loopd/macaroons.go

+ 1
- 0
go.mod View File

@ -23,6 +23,7 @@ require (
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c
google.golang.org/grpc v1.24.0
gopkg.in/macaroon-bakery.v2 v2.0.1
gopkg.in/macaroon.v2 v2.1.0
)

+ 22
- 4
loopd/config.go View File

@ -57,6 +57,16 @@ var (
DefaultTLSKeyPath = filepath.Join(
LoopDirBase, DefaultNetwork, DefaultTLSKeyFilename,
)
// DefaultMacaroonFilename is the default file name for the
// autogenerated loop macaroon.
DefaultMacaroonFilename = "loop.macaroon"
// DefaultMacaroonPath is the default full path of the base loop
// macaroon.
DefaultMacaroonPath = filepath.Join(
LoopDirBase, DefaultNetwork, DefaultMacaroonFilename,
)
)
type lndConfig struct {
@ -82,7 +92,7 @@ type Config struct {
RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"`
CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."`
LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath and --tlskeypath."`
LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath, --tlskeypath and --macaroonpath."`
ConfigFile string `long:"configfile" description:"Path to configuration file."`
DataDir string `long:"datadir" description:"Directory for loopdb."`
@ -93,6 +103,8 @@ type Config struct {
TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed."`
TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set."`
MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for loop's RPC and REST services if it doesn't exist."`
LogDir string `long:"logdir" description:"Directory to log output."`
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)."`
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."`
@ -133,6 +145,7 @@ func DefaultConfig() Config {
DebugLevel: defaultLogLevel,
TLSCertPath: DefaultTLSCertPath,
TLSKeyPath: DefaultTLSKeyPath,
MacaroonPath: DefaultMacaroonPath,
MaxLSATCost: lsat.DefaultMaxCostSats,
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
LoopOutMaxParts: defaultLoopOutMaxParts,
@ -193,9 +206,9 @@ func Validate(cfg *Config) error {
cfg.DataDir = filepath.Join(cfg.DataDir, cfg.Network)
cfg.LogDir = filepath.Join(cfg.LogDir, cfg.Network)
// We want the TLS files to also be in the "namespaced" sub directory.
// Replace the default values with actual values in case the user
// specified either loopdir or datadir.
// We want the TLS and macaroon files to also be in the "namespaced" sub
// directory. Replace the default values with actual values in case the
// user specified either loopdir or datadir.
if cfg.TLSCertPath == DefaultTLSCertPath {
cfg.TLSCertPath = filepath.Join(
cfg.DataDir, DefaultTLSCertFilename,
@ -206,6 +219,11 @@ func Validate(cfg *Config) error {
cfg.DataDir, DefaultTLSKeyFilename,
)
}
if cfg.MacaroonPath == DefaultMacaroonPath {
cfg.MacaroonPath = filepath.Join(
cfg.DataDir, DefaultMacaroonFilename,
)
}
// If either of these directories do not exist, create them.
if err := os.MkdirAll(cfg.DataDir, os.ModePerm); err != nil {

+ 29
- 5
loopd/daemon.go View File

@ -16,6 +16,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
)
@ -79,6 +80,8 @@ type Daemon struct {
restServer *http.Server
restListener net.Listener
restCtxCancel func()
macaroonService *macaroons.Service
}
// New creates a new instance of the loop client daemon.
@ -172,8 +175,9 @@ func (d *Daemon) startWebServers() error {
var err error
// With our client created, let's now finish setting up and start our
// RPC server.
serverOpts := []grpc.ServerOption{}
// RPC server. First we add the security interceptor to our gRPC server
// options that checks the macaroons for validity.
serverOpts := d.macaroonInterceptor()
d.grpcServer = grpc.NewServer(serverOpts...)
looprpc.RegisterSwapClientServer(d.grpcServer, d)
@ -322,6 +326,17 @@ func (d *Daemon) initialize() error {
// stop on main context cancel. So we create it early and pass it down.
d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
// Start the macaroon service and let it create its default macaroon in
// case it doesn't exist yet.
err = d.startMacaroonService()
if err != nil {
// The client is the only thing we started yet, so if we clean
// up its connection now, nothing else needs to be shut down at
// this point.
clientCleanup()
return err
}
// Now finally fully initialize the swap client RPC server instance.
d.swapClientServer = swapClientServer{
impl: swapclient,
@ -336,9 +351,13 @@ func (d *Daemon) initialize() error {
// Retrieve all currently existing swaps from the database.
swapsList, err := d.impl.FetchSwaps()
if err != nil {
// The client is the only thing we started yet, so if we clean
// up its connection now, nothing else needs to be shut down at
// this point.
// The client and the macaroon service are the only things we
// started yet, so if we clean that up now, nothing else needs
// to be shut down at this point.
if err := d.stopMacaroonService(); err != nil {
log.Errorf("Error shutting down macaroon service: %v",
err)
}
clientCleanup()
return err
}
@ -443,6 +462,11 @@ func (d *Daemon) stop() {
d.restCtxCancel()
}
err := d.macaroonService.Close()
if err != nil {
log.Errorf("Error stopping macaroon service: %v", err)
}
// Next, shut down the connections to lnd and the swap server.
if d.lnd != nil {
d.lnd.Close()

+ 192
- 0
loopd/macaroons.go View File

@ -0,0 +1,192 @@
package loopd
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
const (
// loopMacaroonLocation is the value we use for the loopd macaroons'
// "Location" field when baking them.
loopMacaroonLocation = "loop"
)
var (
// RequiredPermissions is a map of all loop RPC methods and their
// required macaroon permissions to access loopd.
RequiredPermissions = map[string][]bakery.Op{
"/looprpc.SwapClient/LoopOut": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/LoopIn": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/Monitor": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/ListSwaps": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/SwapInfo": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/LoopOutTerms": {{
Entity: "terms",
Action: "read",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/LoopOutQuote": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/GetLoopInTerms": {{
Entity: "terms",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetLoopInQuote": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetLsatTokens": {{
Entity: "auth",
Action: "read",
}},
}
// allPermissions is the list of all existing permissions that exist
// for loopd's RPC. The default macaroon that is created on startup
// contains all these permissions and is therefore equivalent to lnd's
// admin.macaroon but for loop.
allPermissions = []bakery.Op{{
Entity: "loop",
Action: "out",
}, {
Entity: "loop",
Action: "in",
}, {
Entity: "swap",
Action: "execute",
}, {
Entity: "swap",
Action: "read",
}, {
Entity: "terms",
Action: "read",
}, {
Entity: "auth",
Action: "read",
}}
// macDbDefaultPw is the default encryption password used to encrypt the
// loop macaroon database. The macaroon service requires us to set a
// non-nil password so we set it to an empty string. This will cause the
// keys to be encrypted on disk but won't provide any security at all as
// the password is known to anyone.
//
// TODO(guggero): Allow the password to be specified by the user. Needs
// create/unlock calls in the RPC. Using a password should be optional
// though.
macDbDefaultPw = []byte("")
)
// startMacaroonService starts the macaroon validation service, creates or
// unlocks the macaroon database and creates the default macaroon if it doesn't
// exist yet. If macaroons are disabled in general in the configuration, none of
// these actions are taken.
func (d *Daemon) startMacaroonService() error {
// Create the macaroon authentication/authorization service.
var err error
d.macaroonService, err = macaroons.NewService(
d.cfg.DataDir, loopMacaroonLocation, macaroons.IPLockChecker,
)
if err != nil {
return fmt.Errorf("unable to set up macaroon authentication: "+
"%v", err)
}
// Try to unlock the macaroon store with the private password.
err = d.macaroonService.CreateUnlock(&macDbDefaultPw)
if err != nil {
return fmt.Errorf("unable to unlock macaroon DB: %v", err)
}
// Create macaroon files for loop CLI to use if they don't exist.
if !lnrpc.FileExists(d.cfg.MacaroonPath) {
ctx := context.Background()
// We only generate one default macaroon that contains all
// existing permissions (equivalent to the admin.macaroon in
// lnd). Custom macaroons can be created through the bakery
// RPC.
loopMac, err := d.macaroonService.NewMacaroon(
ctx, macaroons.DefaultRootKeyID,
allPermissions...,
)
if err != nil {
return err
}
loopMacBytes, err := loopMac.M().MarshalBinary()
if err != nil {
return err
}
err = ioutil.WriteFile(d.cfg.MacaroonPath, loopMacBytes, 0644)
if err != nil {
if err := os.Remove(d.cfg.MacaroonPath); err != nil {
log.Errorf("Unable to remove %s: %v",
d.cfg.MacaroonPath, err)
}
return err
}
}
return nil
}
// stopMacaroonService closes the macaroon database.
func (d *Daemon) stopMacaroonService() error {
return d.macaroonService.Close()
}
// macaroonInterceptor creates gRPC server options with the macaroon security
// interceptors.
func (d *Daemon) macaroonInterceptor() []grpc.ServerOption {
unaryInterceptor := d.macaroonService.UnaryServerInterceptor(
RequiredPermissions,
)
streamInterceptor := d.macaroonService.StreamServerInterceptor(
RequiredPermissions,
)
return []grpc.ServerOption{
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
}
}

Loading…
Cancel
Save