Skip to content

Commit 43830aa

Browse files
committed
multi: add Vault support
1 parent e741dfe commit 43830aa

File tree

8 files changed

+534
-56
lines changed

8 files changed

+534
-56
lines changed

cmd_init_wallet.go

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ type secretSourceK8s struct {
5252
Base64 bool `long:"base64" description:"Encode as base64 when storing and decode as base64 when reading"`
5353
}
5454

55+
type secretSourceVault struct {
56+
AuthTokenPath string `long:"auth-token-path" description:"The full path to the token file that should be used to authenticate against HashiCorp Vault"`
57+
AuthRole string `long:"auth-role" description:"The role to acquire when logging into HashiCorp Vault"`
58+
SecretName string `long:"secret-name" description:"The name of the Kubernetes secret"`
59+
SeedKeyName string `long:"seed-key-name" description:"The name of the entry within the secret that contains the seed"`
60+
SeedPassphraseEntryName string `long:"seed-passphrase-entry-name" description:"The name of the entry within the secret that contains the seed passphrase"`
61+
WalletPasswordEntryName string `long:"wallet-password-entry-name" description:"The name of the entry within the secret that contains the wallet password"`
62+
}
63+
5564
type initTypeFile struct {
5665
OutputWalletDir string `long:"output-wallet-dir" description:"The directory in which the wallet.db file should be initialized"`
5766
ValidatePassword bool `long:"validate-password" description:"If a wallet file already exists in the output wallet directory, validate that it can be unlocked with the given password; this will try to decrypt the wallet and will take several seconds to complete"`
@@ -65,13 +74,14 @@ type initTypeRpc struct {
6574
}
6675

6776
type initWalletCommand struct {
68-
Network string `long:"network" description:"The Bitcoin network to initialize the wallet for, required for wallet internals" choice:"mainnet" choice:"testnet" choice:"testnet3" choice:"regtest" choice:"simnet"`
69-
SecretSource string `long:"secret-source" description:"Where to read the secrets from to initialize the wallet with" choice:"file" choice:"k8s"`
70-
File *secretSourceFile `group:"Flags for reading the secrets from files (use when --secret-source=file)" namespace:"file"`
71-
K8s *secretSourceK8s `group:"Flags for reading the secrets from Kubernetes (use when --secret-source=k8s)" namespace:"k8s"`
72-
InitType string `long:"init-type" description:"How to initialize the wallet" choice:"file" choice:"rpc"`
73-
InitFile *initTypeFile `group:"Flags for initializing the wallet as a file (use when --init-type=file)" namespace:"init-file"`
74-
InitRpc *initTypeRpc `group:"Flags for initializing the wallet through RPC (use when --init-type=rpc)" namespace:"init-rpc"`
77+
Network string `long:"network" description:"The Bitcoin network to initialize the wallet for, required for wallet internals" choice:"mainnet" choice:"testnet" choice:"testnet3" choice:"regtest" choice:"simnet"`
78+
SecretSource string `long:"secret-source" description:"Where to read the secrets from to initialize the wallet with" choice:"file" choice:"k8s"`
79+
File *secretSourceFile `group:"Flags for reading the secrets from files (use when --secret-source=file)" namespace:"file"`
80+
K8s *secretSourceK8s `group:"Flags for reading the secrets from Kubernetes (use when --secret-source=k8s)" namespace:"k8s"`
81+
Vault *secretSourceVault `group:"Flags for reading the secrets from HashiCorp Vault (use when --secret-source=vault)" namespace:"vault"`
82+
InitType string `long:"init-type" description:"How to initialize the wallet" choice:"file" choice:"rpc"`
83+
InitFile *initTypeFile `group:"Flags for initializing the wallet as a file (use when --init-type=file)" namespace:"init-file"`
84+
InitRpc *initTypeRpc `group:"Flags for initializing the wallet through RPC (use when --init-type=rpc)" namespace:"init-rpc"`
7585
}
7686

7787
func newInitWalletCommand() *initWalletCommand {
@@ -82,6 +92,9 @@ func newInitWalletCommand() *initWalletCommand {
8292
K8s: &secretSourceK8s{
8393
Namespace: defaultK8sNamespace,
8494
},
95+
Vault: &secretSourceVault{
96+
AuthTokenPath: defaultK8sServiceAccountTokenPath,
97+
},
8598
InitType: typeFile,
8699
InitFile: &initTypeFile{},
87100
InitRpc: &initTypeRpc{
@@ -248,8 +261,9 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string,
248261
}
249262

250263
if requireSeed {
251-
logger.Infof("Reading seed from k8s secret %s (namespace %s)",
252-
x.K8s.SecretName, x.K8s.Namespace)
264+
logger.Infof("Reading seed from k8s secret %s "+
265+
"(namespace %s)", x.K8s.SecretName,
266+
x.K8s.Namespace)
253267
seed, _, err = readK8s(k8sSecret)
254268
if err != nil {
255269
return "", "", "", err
@@ -258,8 +272,8 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string,
258272

259273
// The seed passphrase is optional.
260274
if x.K8s.SeedPassphraseKeyName != "" {
261-
logger.Infof("Reading seed passphrase from k8s secret %s "+
262-
"(namespace %s)", x.K8s.SecretName,
275+
logger.Infof("Reading seed passphrase from k8s secret "+
276+
"%s (namespace %s)", x.K8s.SecretName,
263277
x.K8s.Namespace)
264278
k8sSecret.KeyName = x.K8s.SeedPassphraseKeyName
265279
seedPassPhrase, _, err = readK8s(k8sSecret)
@@ -268,13 +282,49 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string,
268282
}
269283
}
270284

271-
logger.Infof("Reading wallet password from k8s secret %s (namespace %s)",
272-
x.K8s.SecretName, x.K8s.Namespace)
285+
logger.Infof("Reading wallet password from k8s secret %s "+
286+
"(namespace %s)", x.K8s.SecretName, x.K8s.Namespace)
273287
k8sSecret.KeyName = x.K8s.WalletPasswordKeyName
274288
walletPassword, _, err = readK8s(k8sSecret)
275289
if err != nil {
276290
return "", "", "", err
277291
}
292+
293+
// Read passphrase from HashiCorp Vault secret.
294+
case storageVault:
295+
vaultSecret := &vaultSecretOptions{
296+
AuthTokenPath: x.Vault.AuthTokenPath,
297+
AuthRole: x.Vault.AuthRole,
298+
SecretName: x.Vault.SecretName,
299+
}
300+
vaultSecret.SecretKeyName = x.Vault.SeedKeyName
301+
302+
logger.Infof("Reading seed from vault secret %s",
303+
x.Vault.SecretName)
304+
seed, _, err = readVault(vaultSecret)
305+
if err != nil {
306+
return "", "", "", err
307+
}
308+
309+
// The seed passphrase is optional.
310+
if x.Vault.SeedPassphraseEntryName != "" {
311+
logger.Infof("Reading seed passphrase from vault "+
312+
"secret %s", x.Vault.SecretName)
313+
vaultSecret.SecretKeyName =
314+
x.Vault.SeedPassphraseEntryName
315+
seedPassPhrase, _, err = readVault(vaultSecret)
316+
if err != nil {
317+
return "", "", "", err
318+
}
319+
}
320+
321+
logger.Infof("Reading wallet password from vault secret %s",
322+
x.Vault.SecretName)
323+
vaultSecret.SecretKeyName = x.Vault.WalletPasswordEntryName
324+
walletPassword, _, err = readVault(vaultSecret)
325+
if err != nil {
326+
return "", "", "", err
327+
}
278328
}
279329

280330
// The seed, its passphrase and the wallet password should all never

cmd_load_secret.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
)
88

99
type loadSecretCommand struct {
10-
Source string `long:"source" short:"s" description:"Secret storage source" choice:"k8s"`
11-
K8s *k8sSecretOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"`
12-
Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"`
10+
Source string `long:"source" short:"s" description:"Secret storage source" choice:"k8s"`
11+
K8s *k8sSecretOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"`
12+
Vault *vaultSecretOptions `group:"Flags for looking up the secret as a value inside a HashiCorp Vault (use when --source=vault)" namespace:"vault"`
13+
Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"`
1314
}
1415

1516
func newLoadSecretCommand() *loadSecretCommand {
@@ -18,6 +19,9 @@ func newLoadSecretCommand() *loadSecretCommand {
1819
K8s: &k8sSecretOptions{
1920
Namespace: defaultK8sNamespace,
2021
},
22+
Vault: &vaultSecretOptions{
23+
AuthTokenPath: defaultK8sServiceAccountTokenPath,
24+
},
2125
Output: outputFormatRaw,
2226
}
2327
}
@@ -70,6 +74,27 @@ func (x *loadSecretCommand) Execute(_ []string) error {
7074

7175
return nil
7276

77+
case storageVault:
78+
content, secret, err := readVault(x.Vault)
79+
if err != nil {
80+
return fmt.Errorf("error reading secret %s from "+
81+
"vault: %v", x.Vault.SecretName, err)
82+
}
83+
84+
if x.Output == outputFormatJSON {
85+
content, err = asJSON(&struct {
86+
*jsonVaultObject `json:",inline"`
87+
Value string `json:"value"`
88+
}{
89+
jsonVaultObject: secret,
90+
Value: content,
91+
})
92+
}
93+
94+
fmt.Printf("%s\n", content)
95+
96+
return nil
97+
7398
default:
7499
return fmt.Errorf("invalid secret storage source %s", x.Source)
75100
}

cmd_store_secret.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ type entry struct {
2222
}
2323

2424
type storeSecretCommand struct {
25-
Batch bool `long:"batch" description:"Instead of reading one secret from stdin, read all files of the argument list and store them as entries in the secret"`
26-
Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"`
27-
Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"`
28-
K8s *targetK8sSecret `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"`
25+
Batch bool `long:"batch" description:"Instead of reading one secret from stdin, read all files of the argument list and store them as entries in the secret"`
26+
Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"`
27+
Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"`
28+
K8s *targetK8sSecret `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"`
29+
Vault *vaultSecretOptions `group:"Flags for storing the secret as a value inside a HashiCorp Vault (use when --target=vault)" namespace:"vault"`
2930
}
3031

3132
func newStoreSecretCommand() *storeSecretCommand {
@@ -39,6 +40,9 @@ func newStoreSecretCommand() *storeSecretCommand {
3940
ResourcePolicy: defaultK8sResourcePolicy,
4041
},
4142
},
43+
Vault: &vaultSecretOptions{
44+
AuthTokenPath: defaultK8sServiceAccountTokenPath,
45+
},
4246
}
4347
}
4448

@@ -101,6 +105,15 @@ func (x *storeSecretCommand) Execute(args []string) error {
101105

102106
return storeSecretsK8s(entries, x.K8s, x.Overwrite)
103107

108+
case storageVault:
109+
// Take the actual entry name from the options if we aren't in
110+
// batch mode.
111+
if len(entries) == 1 && entries[0].key == "" {
112+
entries[0].key = x.Vault.SecretKeyName
113+
}
114+
115+
return storeSecretsVault(entries, x.Vault, x.Overwrite)
116+
104117
default:
105118
return fmt.Errorf("invalid secret storage target %s", x.Target)
106119
}
@@ -138,3 +151,34 @@ func storeSecretsK8s(entries []*entry, opts *targetK8sSecret,
138151

139152
return nil
140153
}
154+
155+
func storeSecretsVault(entries []*entry, opts *vaultSecretOptions,
156+
overwrite bool) error {
157+
158+
if opts.SecretName == "" {
159+
return fmt.Errorf("secret name is required")
160+
}
161+
162+
for _, entry := range entries {
163+
if entry.key == "" {
164+
return fmt.Errorf("secret entry key is required")
165+
}
166+
167+
entryOpts := &vaultSecretOptions{
168+
AuthTokenPath: opts.AuthTokenPath,
169+
AuthRole: opts.AuthRole,
170+
SecretName: opts.SecretName,
171+
SecretKeyName: entry.key,
172+
}
173+
174+
logger.Infof("Storing entry with name %s to secret %s in vault",
175+
entryOpts.SecretKeyName, entryOpts.SecretName)
176+
err := saveVault(entry.value, entryOpts, overwrite)
177+
if err != nil {
178+
return fmt.Errorf("error storing secret %s entry %s: "+
179+
"%v", opts.SecretName, entry.key, err)
180+
}
181+
}
182+
183+
return nil
184+
}

example-init-wallet-vault.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
VAULT_AUTH_ROLE=${VAULT_AUTH_ROLE:-lnd-role}
6+
WALLET_SECRET_NAME=${WALLET_SECRET_NAME:-lnd-wallet-secret}
7+
WALLET_DIR=${WALLET_DIR:-~/.lnd/data/chain/bitcoin/mainnet}
8+
WALLET_PASSWORD_FILE=${WALLET_PASSWORD_FILE:-/tmp/wallet-password}
9+
CERT_DIR=${CERT_DIR:-~/.lnd}
10+
UPLOAD_RPC_SECRETS=${UPLOAD_RPC_SECRETS:-0}
11+
RPC_SECRETS_NAME=${RPC_SECRETS_NAME:-lnd-rpc-secret}
12+
13+
echo "[STARTUP] Asserting wallet password exists in secret ${WALLET_SECRET_NAME}"
14+
lndinit gen-password \
15+
| lndinit -v store-secret \
16+
--target=vault \
17+
--vault.auth-role="${VAULT_AUTH_ROLE}" \
18+
--vault.secret-name="${WALLET_SECRET_NAME}" \
19+
--vault.secret-key-name=wallet-password
20+
21+
echo ""
22+
echo "[STARTUP] Asserting seed exists in secret ${WALLET_SECRET_NAME}"
23+
lndinit gen-seed \
24+
| lndinit -v store-secret \
25+
--target=vault \
26+
--vault.auth-role="${VAULT_AUTH_ROLE}" \
27+
--vault.secret-name="${WALLET_SECRET_NAME}" \
28+
--vault.secret-key-name=seed
29+
30+
echo ""
31+
echo "[STARTUP] Asserting wallet is created with values from secret ${WALLET_SECRET_NAME}"
32+
lndinit -v init-wallet \
33+
--secret-source=vault \
34+
--vault.auth-role="${VAULT_AUTH_ROLE}" \
35+
--vault.secret-name="${WALLET_SECRET_NAME}" \
36+
--vault.seed-key-name=seed \
37+
--vault.wallet-password-entry-name=wallet-password \
38+
--output-wallet-dir="${WALLET_DIR}" \
39+
--validate-password
40+
41+
echo ""
42+
echo "[STARTUP] Preparing lnd auto unlock file"
43+
44+
# To make sure the password can be read exactly once (by lnd itself), we create
45+
# a named pipe. Because we can only write to such a pipe if there's a reader on
46+
# the other end, we need to run this in a sub process in the background.
47+
mkfifo "${WALLET_PASSWORD_FILE}"
48+
lndinit -v load-secret \
49+
--source=vault \
50+
--vault.auth-role="${VAULT_AUTH_ROLE}" \
51+
--vault.secret-name="${WALLET_SECRET_NAME}" \
52+
--vault.secret-key-name=wallet-password > "${WALLET_PASSWORD_FILE}" &
53+
54+
# In case we want to upload the TLS certificate and macaroons to k8s secrets as
55+
# well once lnd is ready, we can use the wait-ready and store-secret commands in
56+
# combination to wait until lnd is ready and then batch upload the files to k8s.
57+
if [[ "${UPLOAD_RPC_SECRETS}" == "1" ]]; then
58+
echo ""
59+
echo "[STARTUP] Starting RPC secret uploader process in background"
60+
lndinit -v wait-ready \
61+
&& lndinit -v store-secret \
62+
--batch \
63+
--overwrite \
64+
--target=vault \
65+
--vault.auth-role="${VAULT_AUTH_ROLE}" \
66+
--vault.secret-name="${RPC_SECRETS_NAME}" \
67+
"${CERT_DIR}/tls.cert" \
68+
"${WALLET_DIR}"/*.macaroon &
69+
fi
70+
71+
# And finally start lnd. We need to use "exec" here to make sure all signals are
72+
# forwarded correctly.
73+
echo ""
74+
echo "[STARTUP] Starting lnd with flags: $@"
75+
exec lnd "$@"

0 commit comments

Comments
 (0)