Skip to content

Latest commit

 

History

History
459 lines (363 loc) · 14.4 KB

README.md

File metadata and controls

459 lines (363 loc) · 14.4 KB

lndinit: a wallet initializer utility for lnd

This repository contains the source for the lndinit command. The main purpose of lndinit is to help automate the lnd wallet initialization, including seed and password generation.

Requirements

Most commands of this tool operate independently of lnd and therefore don't require a specific version to be installed.

The commands wait-ready and init-wallet only work with lnd v0.14.2-beta and later though.

A recent version of Kubernetes is needed when interacting with secrets stored in k8s. Any version >= v1.8 should work.


Subcommands

Most commands work without lnd running, as they are designed to do some provisioning work before lnd is started.

gen-password

gen-password generates a random password (no lnd needed)

gen-seed

gen-seed generates a random seed phrase

No lnd needed, but seed will be in lnd-specific aezeed format

load-secret

load-secret interacts with kubernetes to read from secrets (no lnd needed)

store-secret

store-secret interacts with kubernetes to write to secrets (no lnd needed)

store-configmap

store-configmap interacts with kubernetes to write to configmaps (no lnd needed)

init-wallet

init-wallet has two modes:

  • --init-type=file creates an lnd specific wallet.db file
    • Only works if lnd is NOT running yet
  • --init-type=rpc calls the lnd RPC to create a wallet
    • Use this mode if you are using a remote database as lnd's storage backend instead of bolt DB based file databases
    • Needs lnd to be running and no wallet to exist

wait-ready

wait-ready waits for lnd to be ready by connecting to lnd's status RPC

  • Needs lnd to run, eventually

Example Usage

Example use case 1: Basic setup

This is a very basic example that shows the purpose of the different sub commands of the lndinit binary. In this example, all secrets are stored in files. This is normally not a good security practice as potentially other users or processes on a system can read those secrets if the permissions aren't set correctly. It is advised to store secrets in dedicated secret storage services like Kubernetes Secrets or HashiCorp Vault.

1. Generate a seed without a seed passphrase

Create a new seed if one does not exist yet.

$ if [[ ! -f /safe/location/seed.txt ]]; then
    lndinit gen-seed > /safe/location/seed.txt
  fi

2. Generate a wallet password

Create a new wallet password if one does not exist yet.

$ if [[ ! -f /safe/location/walletpassword.txt ]]; then
    lndinit gen-password > /safe/location/walletpassword.txt
  fi

3. Initialize the wallet

Create the wallet database with the given seed and password files. If the wallet already exists, we make sure we can actually unlock it with the given password file. This will take a few seconds in any case.

$ lndinit -v init-wallet \
    --secret-source=file \
    --file.seed=/safe/location/seed.txt \
    --file.wallet-password=/safe/location/walletpassword.txt \
    --init-file.output-wallet-dir=$HOME/.lnd/data/chain/bitcoin/mainnet \
    --init-file.validate-password

4. Start and auto unlock lnd

With everything prepared, we can now start lnd and instruct it to auto unlock itself with the password in the file we prepared.

$ lnd \
    --bitcoin.active \
    ...
    --wallet-unlock-password-file=/safe/location/walletpassword.txt

Example use case 2: Kubernetes

This example shows how Kubernetes (k8s) Secrets can be used to store the wallet seed and password. The pod running those commands must be provisioned with a service account that has permissions to read/create/modify secrets in a given namespace.

Here's an example of a service account, role provision and pod definition:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: lnd-provision-account


---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: lnd-update-secrets-role
  namespace: default
rules:
  - apiGroups: [ "" ]
    resources: [ "secrets" ]
    verbs: [ "get", "list", "create", "watch", "update", "patch" ]


---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: lnd-update-secrets-role-binding
  namespace: default
roleRef:
  kind: Role
  name: lnd-update-secrets-role
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: lnd-provision-account
    namespace: default


---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: lnd-pod
spec:
  strategy:
    type: Recreate
  replicas: 1
  template:
    spec:
      # We use the special service account created, so the init script is able
      # to update the secret as expected.
      serviceAccountName: lnd-provision-account

      containers:
        # The main lnd container
        - name: lnd
          
          # The lndinit image is an image based on the main lnd image that just
          # adds the lndinit binary to it. The tag name is simply:
          #   <lndinit-version>-lnd-<lnd-version>
          image: lightninglabs/lndinit:v0.1.0-lnd-v0.14.2-beta
          env:
            - name: WALLET_SECRET_NAME
              value: lnd-wallet-secret
            - name: WALLET_DIR
              value: /root/.lnd/data/chain/bitcoin/mainnet
            - name: CERT_DIR
              value: /root/.lnd
            - name: UPLOAD_RPC_SECRETS
              value: '1'
            - name: RPC_SECRETS_NAME
              value: lnd-rpc-secrets
          command: [ '/init-wallet-k8s.sh' ]
          args: [
              '--bitcoin.mainnet',
              '...',
              '--wallet-unlock-password-file=/tmp/wallet-password',
          ]

The /init-wallet-k8s.sh script that is invoked in the example above can be found in this repository: example-init-wallet-k8s.sh The script executes the steps described in this example and also uploads the RPC secrets (tls.cert and all *.macaroon files) to another secret so apps using the lnd node can access those secrets.

1. Generate a seed passphrase (optional)

Generate a new seed passphrase. If an entry with the key already exists in the k8s secret, it is not overwritten, and the operation is a no-op.

$ lndinit gen-password \
    | lndinit -v store-secret \
    --target=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.secret-key-name=seed-passphrase

2. Generate a seed using the passphrase

Generate a new seed with the passphrase created before. If an entry with that key already exists in the k8s secret, it is not overwritten, and the operation is a no-op.

$ lndinit -v gen-seed \
    --passphrase-k8s.secret-name=lnd-secrets \
    --passphrase-k8s.secret-key-name=seed-passphrase \
    | lndinit -v store-secret \
    --target=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.secret-key-name=seed

3. Generate a wallet password

Generate a new wallet password. If an entry with that key already exists in the k8s secret, it is not overwritten, and the operation is a no-op.

$ lndinit gen-password \
    | lndinit -v store-secret \
    --target=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.secret-key-name=wallet-password

4. Initialize the wallet, attempting a test unlock with the password

Create the wallet database with the given seed, seed passphrase and wallet password loaded from a k8s secret. If the wallet already exists, we make sure we can actually unlock it with the given password file. This will take a few seconds in any case.

$ lndinit -v init-wallet \
    --secret-source=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.seed-key-name=seed \
    --k8s.seed-passphrase-key-name=seed-passphrase \
    --k8s.wallet-password-key-name=wallet-password \
    --init-file.output-wallet-dir=$HOME/.lnd/data/chain/bitcoin/mainnet \
    --init-file.validate-password

The above is an example for a file/bbolt based node. For such a node creating the wallet directly as a file is the most secure option, since it doesn't require the node to spin up the wallet unlocker RPC (which doesn't use macaroons and is therefore un-authenticated).

But in setups where the wallet isn't a file (since all state is in a remote database such as etcd or Postgres), this method cannot be used. Instead, the wallet needs to be initialized through RPC, as shown in the next example:

$ lndinit -v init-wallet \
    --secret-source=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.seed-key-name=seed \
    --k8s.seed-passphrase-key-name=seed-passphrase \
    --k8s.wallet-password-key-name=wallet-password \
    --init-type=rpc \
    --init-rpc.server=localhost:10009 \
    --init-rpc.tls-cert-path=$HOME/.lnd/tls.cert

NOTE: If this is used in combination with the --wallet-unlock-password-file= flag in lnd for automatic unlocking, then the --wallet-unlock-allow-create flag also needs to be set. Otherwise, lnd won't be starting the wallet unlocking RPC that is used for initializing the wallet.

The following example shows how to use the lndinit init-wallet command to create a watch-only wallet from a previously exported accounts JSON file:

$ lndinit -v init-wallet \
    --secret-source=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.seed-key-name=seed \
    --k8s.seed-passphrase-key-name=seed-passphrase \
    --k8s.wallet-password-key-name=wallet-password \
    --init-type=rpc \
    --init-rpc.server=localhost:10009 \
    --init-rpc.tls-cert-path=$HOME/.lnd/tls.cert \
    --init-rpc.watch-only \
    --init-rpc.accounts-file=/tmp/accounts.json

5. Store the wallet password in a file

Because we now only have the wallet password as a value in a k8s secret, we need to retrieve it and store it in a file that lnd can read to auto unlock.

$ lndinit -v load-secret \
    --source=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.secret-key-name=wallet-password > /safe/location/walletpassword.txt

Security notice:

Any process or user that has access to the file system of the container can potentially read the password if it's stored as a plain file. For an extra bump in security, a named pipe can be used instead of a file. That way the password can only be read exactly once from the pipe during lnd's startup.

# Create a FIFO pipe first. This will behave like a file except that writes to
# it will only occur once there's a reader on the other end.
$ mkfifo /tmp/wallet-password

# Read the secret from Kubernetes and write it to the pipe. This will only
# return once lnd is actually reading from the pipe. Therefore we need to run
# the command as a background process (using the ampersand notation).
$ lndinit load-secret \
    --source=k8s \
    --k8s.secret-name=lnd-secrets \
    --k8s.secret-key-name=wallet-password > /tmp/wallet-password &

# Now run lnd and point it to the named pipe.
$ lnd \
    --bitcoin.active \
    ...
    --wallet-unlock-password-file=/tmp/wallet-password

6. Start and auto unlock lnd

With everything prepared, we can now start lnd and instruct it to auto unlock itself with the password in the file we prepared.

$ lnd \
    --bitcoin.active \
    ...
    --wallet-unlock-password-file=/safe/location/walletpassword.txt

Logging and idempotent operations

By default, lndinit aborts and exits with a zero return code if the desired result is already achieved (e.g. a secret key or a wallet database already exist). This can make it hard to follow exactly what is happening when debugging the initialization. To assist with debugging, the following two flags can be used:

  • --verbose (-v): Log debug information to stderr.
  • --error-on-existing (-e): Exit with a non-zero return code (128) if the result of an operation already exists. See example below.

Example:

# Treat every non-zero return code as abort condition (default for k8s container
# commands).
$ set -e

# Run the command and catch any non-zero return code in the ret variable. The
# logical OR is required to not fail because of above setting.
$ ret=0
$ lndinit --error-on-existing init-wallet ... || ret=$?
$ if [[ $ret -eq 0 ]]; then
    echo "Successfully initialized wallet."
  elif [[ $ret -eq 128 ]]; then
    echo "Wallet already exists, skipping initialization."
  else
    echo "Failed to initialize wallet!"
    exit 1
  fi

Release Process

This project is updated less often than lnd, so there are two main aspects to the release process. When a new lnd is released, and it's compatible with existing lndinit binary, only the container image needs to be built.

Binary release

When binary changes are required (either for dependency upgrades, or to maintain compatibility with a new lnd release) the lndinit binary must be rebuilt:

  1. Apply the necessary code changes.
  2. Adjust the relevant version constant(s) in version.go.
    • Usually this will be incrementing AppPatch.
  3. Open PR and have it merged.
  4. A maintainer must push a git-tag with the new version:
    • git checkout main && git pull
    • TAG=v0.<minor>.<patch>-beta (e.g.: TAG=v0.1.15-beta)
    • git tag $TAG && git push $TAG

Then proceed to the container image release process.

Container image release

When a new version of lnd is released, a new lndinit container image build is triggered by pushing a tag with the format: docker/<lndinit_version>-lnd-<lnd_version>

For example, to build an image based on lnd v0.16.4-beta, which includes lndinit v0.1.15-beta:

LNDINIT_VERSION=v0.1.15-beta
LND_VERSION=v0.16.4-beta

git checkout $LNDINIT_VERSION
git tag docker/${LNDINIT_VERSION}-lnd-${LND_VERSION}
git push docker/${LNDINIT_VERSION}-lnd-${LND_VERSION}

If lnd v0.16.5-beta is released and does not require additional lndinit binary changes, the desired image can be built by re-running the previous command with the lnd version adjusted. In this case, there's no need to modify any code in this repo.

For more detail, refer to the docker.yml Github workflow.