Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

walletunlocker+lnd: add command line flag to allow passing admin macaroon after wallet creation #1288

Merged
merged 12 commits into from
Nov 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 117 additions & 4 deletions cmd/lncli/cmd_walletunlocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@ import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"

"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/urfave/cli"
)

var (
statelessInitFlag = cli.BoolFlag{
Name: "stateless_init",
Usage: "do not create any macaroon files in the file " +
"system of the daemon",
}
saveToFlag = cli.StringFlag{
Name: "save_to",
Usage: "save returned admin macaroon to this file",
}
)

var createCommand = cli.Command{
Name: "create",
Category: "Startup",
Expand All @@ -37,6 +52,14 @@ var createCommand = cli.Command{
to potentially recover all on-chain funds, and most off-chain funds as
well.

If the --stateless_init flag is set, no macaroon files are created by
the daemon. Instead, the binary serialized admin macaroon is returned
in the answer. This answer MUST be stored somewhere, otherwise all
access to the RPC server will be lost and the wallet must be recreated
to re-gain access.
If the --save_to parameter is set, the macaroon is saved to this file,
otherwise it is printed to standard out.

Finally, it's also possible to use this command and a set of static
channel backups to trigger a recover attempt for the provided Static
Channel Backups. Only one of the three parameters will be accepted. See
Expand All @@ -58,6 +81,8 @@ var createCommand = cli.Command{
Name: "multi_file",
Usage: "the path to a multi-channel back up file",
},
statelessInitFlag,
saveToFlag,
},
Action: actionDecorator(create),
}
Expand Down Expand Up @@ -171,7 +196,15 @@ func create(ctx *cli.Context) error {
}
}
}
}

// Should the daemon be initialized stateless? Then we expect an answer
// with the admin macaroon later. Because the --save_to is related to
// stateless init, it doesn't make sense to be set on its own.
statelessInit := ctx.Bool(statelessInitFlag.Name)
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
return fmt.Errorf("cannot set save_to parameter without " +
"stateless_init")
}

walletPassword, err := capturePassword(
Expand Down Expand Up @@ -349,13 +382,19 @@ mnemonicCheck:
AezeedPassphrase: aezeedPass,
RecoveryWindow: recoveryWindow,
ChannelBackups: chanBackups,
StatelessInit: statelessInit,
}
if _, err := client.InitWallet(ctxb, req); err != nil {
response, err := client.InitWallet(ctxb, req)
if err != nil {
return err
}

fmt.Println("\nlnd successfully initialized!")

if statelessInit {
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
}

return nil
}

Expand Down Expand Up @@ -410,6 +449,12 @@ var unlockCommand = cli.Command{
start up. This command MUST be run after booting up lnd before it's
able to carry out its duties. An exception is if a user is running with
--noseedbackup, then a default passphrase will be used.

If the --stateless_init flag is set, no macaroon files are created by
the daemon. This should be set for every unlock if the daemon was
initially initialized stateless. Otherwise the daemon will create
unencrypted macaroon files which could leak information to the system
that the daemon runs on.
`,
Flags: []cli.Flag{
cli.IntFlag{
Expand All @@ -430,6 +475,7 @@ var unlockCommand = cli.Command{
"combination with some sort of password " +
"manager or secrets vault.",
},
statelessInitFlag,
},
Action: actionDecorator(unlock),
}
Expand Down Expand Up @@ -485,6 +531,7 @@ func unlock(ctx *cli.Context) error {
req := &lnrpc.UnlockWalletRequest{
WalletPassword: pw,
RecoveryWindow: recoveryWindow,
StatelessInit: ctx.Bool(statelessInitFlag.Name),
}
_, err = client.UnlockWallet(ctxb, req)
if err != nil {
Expand All @@ -511,7 +558,35 @@ var changePasswordCommand = cli.Command{
--noseedbackup), one must restart their daemon without
--noseedbackup and use this command. The "current password" field
should be left empty.

If the daemon was originally initialized stateless, then the
--stateless_init flag needs to be set for the change password request
as well! Otherwise the daemon will generate unencrypted macaroon files
in its file system again and possibly leak sensitive information.
Changing the password will by default not change the macaroon root key
(just re-encrypt the macaroon database with the new password). So all
macaroons will still be valid.
If one wants to make sure that all previously created macaroons are
invalidated, a new macaroon root key can be generated by using the
--new_mac_root_key flag.

After a successful password change with the --stateless_init flag set,
the current or new admin macaroon is returned binary serialized in the
answer. This answer MUST then be stored somewhere, otherwise
all access to the RPC server will be lost and the wallet must be re-
created to re-gain access. If the --save_to parameter is set, the
macaroon is saved to this file, otherwise it is printed to standard out.
`,
Flags: []cli.Flag{
statelessInitFlag,
saveToFlag,
cli.BoolFlag{
Name: "new_mac_root_key",
Usage: "rotate the macaroon root key resulting in " +
"all previously created macaroons to be " +
"invalidated",
},
},
Action: actionDecorator(changePassword),
}

Expand Down Expand Up @@ -539,15 +614,53 @@ func changePassword(ctx *cli.Context) error {
return fmt.Errorf("passwords don't match")
}

// Should the daemon be initialized stateless? Then we expect an answer
// with the admin macaroon later. Because the --save_to is related to
// stateless init, it doesn't make sense to be set on its own.
statelessInit := ctx.Bool(statelessInitFlag.Name)
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
return fmt.Errorf("cannot set save_to parameter without " +
"stateless_init")
}

req := &lnrpc.ChangePasswordRequest{
CurrentPassword: currentPw,
NewPassword: newPw,
CurrentPassword: currentPw,
NewPassword: newPw,
StatelessInit: statelessInit,
NewMacaroonRootKey: ctx.Bool("new_mac_root_key"),
}

_, err = client.ChangePassword(ctxb, req)
response, err := client.ChangePassword(ctxb, req)
if err != nil {
return err
}

if statelessInit {
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
}

return nil
}

// storeOrPrintAdminMac either stores the admin macaroon to a file specified or
// prints it to standard out, depending on the user flags set.
func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error {
// The user specified the optional --save_to parameter. We'll save the
// macaroon to that file.
if ctx.IsSet("save_to") {
macSavePath := lncfg.CleanAndExpandPath(ctx.String("save_to"))
err := ioutil.WriteFile(macSavePath, adminMac, 0644)
if err != nil {
_ = os.Remove(macSavePath)
return err
}
fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
return nil
}

// Otherwise we just print it. The user MUST store this macaroon
// somewhere so we either save it to a provided file path or just print
// it to standard output.
fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac))
return nil
}
43 changes: 43 additions & 0 deletions docs/macaroons.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,49 @@ timeout can be changed with the `--macaroontimeout` option; this can be
increased for making RPC calls between systems whose clocks are more than 60s
apart.

## Stateless initialization

As mentioned above, by default `lnd` creates several macaroon files in its
directory. These are unencrypted and in case of the `admin.macaroon` provide
full access to the daemon. This can be seen as quite a big security risk if
the `lnd` daemon runs in an environment that is not fully trusted.

The macaroon files are the only files with highly sensitive information that
are not encrypted (unlike the wallet file and the macaroon database file that
contains the [root key](../macaroons/README.md), these are always encrypted,
even if no password is used).

To avoid leaking the macaroon information, `lnd` supports the so called
`stateless initialization` mode:
* The three startup commands `create`, `unlock` and `changepassword` of `lncli`
all have a flag called `--stateless_init` that instructs the daemon **not**
to create `*.macaroon` files.
* The two operations `create` and `changepassword` that actually create/update
the macaroon database will return the admin macaroon in the RPC call.
Assuming the daemon and the `lncli` are not used on the same machine, this
will leave no unencrypted information on the machine where `lnd` runs on.
* To be more precise: By default, when using the `changepassword` command, the
macaroon root key in the macaroon DB is just re-encrypted with the new
password. But the key remains the same and therefore the macaroons issued
before the `changepassword` command still remain valid. If a user wants to
invalidate all previously created macaroons, the `--new_mac_root_key` flag
of the `changepassword` command should be used!
* An user of `lncli` will see the returned admin macaroon printed to the screen
or saved to a file if the parameter `--save_to=some_file.macaroon` is used.
* **Important:** By default, `lnd` will create the macaroon files during the
`unlock` phase, if the `--stateless_init` flag is not used. So to avoid
leakage of the macaroon information, use the stateless initialization flag
for all three startup commands of the wallet unlocker service!

Examples:

* Create a new wallet stateless (first run):
* `lncli create --stateless_init --save_to=/safe/location/admin.macaroon`
* Unlock a wallet that has previously been initialized stateless:
* `lncli unlock --stateless_init`
* Use the created macaroon:
* `lncli --macaroonpath=/safe/location/admin.macaroon getinfo`

## Using Macaroons with GRPC clients

When interacting with `lnd` using the GRPC interface, the macaroons are encoded
Expand Down
Loading