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.
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.
Most commands work without lnd
running, as they are designed to do some provisioning work before lnd
is started.
gen-password
generates a random password (no lnd
needed)
gen-seed
generates a random seed phrase
No lnd
needed, but seed will be in lnd
-specific aezeed
format
load-secret
interacts with kubernetes to read from secrets (no lnd
needed)
store-secret
interacts with kubernetes to write to secrets (no lnd
needed)
store-configmap
interacts with kubernetes to write to configmaps (no lnd
needed)
init-wallet
has two modes:
--init-type=file
creates anlnd
specificwallet.db
file- Only works if
lnd
is NOT running yet
- Only works if
--init-type=rpc
calls thelnd
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
- Use this mode if you are using a remote database as
wait-ready
waits for lnd
to be ready by connecting to lnd
's status RPC
- Needs
lnd
to run, eventually
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.
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
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
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
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
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.
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
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
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
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
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
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
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 tostderr
.--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
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.
When binary changes are required (either for dependency upgrades, or to maintain
compatibility with a new lnd
release) the lndinit
binary must be rebuilt:
- Apply the necessary code changes.
- Adjust the relevant version constant(s) in
version.go
.- Usually this will be incrementing
AppPatch
.
- Usually this will be incrementing
- Open PR and have it merged.
- 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.
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.