From 43830aa20b006c021b5290f8c8f989fcd18034eb Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 13 Aug 2025 09:57:42 +0200 Subject: [PATCH] multi: add Vault support --- cmd_init_wallet.go | 76 ++++++++++-- cmd_load_secret.go | 31 ++++- cmd_store_secret.go | 52 +++++++- example-init-wallet-vault.sh | 75 ++++++++++++ go.mod | 36 ++++-- go.sum | 85 +++++++++---- main.go | 5 +- vault.go | 230 +++++++++++++++++++++++++++++++++++ 8 files changed, 534 insertions(+), 56 deletions(-) create mode 100644 example-init-wallet-vault.sh create mode 100644 vault.go diff --git a/cmd_init_wallet.go b/cmd_init_wallet.go index 1965445..ece7b63 100644 --- a/cmd_init_wallet.go +++ b/cmd_init_wallet.go @@ -52,6 +52,15 @@ type secretSourceK8s struct { Base64 bool `long:"base64" description:"Encode as base64 when storing and decode as base64 when reading"` } +type secretSourceVault struct { + AuthTokenPath string `long:"auth-token-path" description:"The full path to the token file that should be used to authenticate against HashiCorp Vault"` + AuthRole string `long:"auth-role" description:"The role to acquire when logging into HashiCorp Vault"` + SecretName string `long:"secret-name" description:"The name of the Kubernetes secret"` + SeedKeyName string `long:"seed-key-name" description:"The name of the entry within the secret that contains the seed"` + SeedPassphraseEntryName string `long:"seed-passphrase-entry-name" description:"The name of the entry within the secret that contains the seed passphrase"` + WalletPasswordEntryName string `long:"wallet-password-entry-name" description:"The name of the entry within the secret that contains the wallet password"` +} + type initTypeFile struct { OutputWalletDir string `long:"output-wallet-dir" description:"The directory in which the wallet.db file should be initialized"` 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 { } type initWalletCommand struct { - 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"` - SecretSource string `long:"secret-source" description:"Where to read the secrets from to initialize the wallet with" choice:"file" choice:"k8s"` - File *secretSourceFile `group:"Flags for reading the secrets from files (use when --secret-source=file)" namespace:"file"` - K8s *secretSourceK8s `group:"Flags for reading the secrets from Kubernetes (use when --secret-source=k8s)" namespace:"k8s"` - InitType string `long:"init-type" description:"How to initialize the wallet" choice:"file" choice:"rpc"` - InitFile *initTypeFile `group:"Flags for initializing the wallet as a file (use when --init-type=file)" namespace:"init-file"` - InitRpc *initTypeRpc `group:"Flags for initializing the wallet through RPC (use when --init-type=rpc)" namespace:"init-rpc"` + 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"` + SecretSource string `long:"secret-source" description:"Where to read the secrets from to initialize the wallet with" choice:"file" choice:"k8s"` + File *secretSourceFile `group:"Flags for reading the secrets from files (use when --secret-source=file)" namespace:"file"` + K8s *secretSourceK8s `group:"Flags for reading the secrets from Kubernetes (use when --secret-source=k8s)" namespace:"k8s"` + Vault *secretSourceVault `group:"Flags for reading the secrets from HashiCorp Vault (use when --secret-source=vault)" namespace:"vault"` + InitType string `long:"init-type" description:"How to initialize the wallet" choice:"file" choice:"rpc"` + InitFile *initTypeFile `group:"Flags for initializing the wallet as a file (use when --init-type=file)" namespace:"init-file"` + InitRpc *initTypeRpc `group:"Flags for initializing the wallet through RPC (use when --init-type=rpc)" namespace:"init-rpc"` } func newInitWalletCommand() *initWalletCommand { @@ -82,6 +92,9 @@ func newInitWalletCommand() *initWalletCommand { K8s: &secretSourceK8s{ Namespace: defaultK8sNamespace, }, + Vault: &secretSourceVault{ + AuthTokenPath: defaultK8sServiceAccountTokenPath, + }, InitType: typeFile, InitFile: &initTypeFile{}, InitRpc: &initTypeRpc{ @@ -248,8 +261,9 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, } if requireSeed { - logger.Infof("Reading seed from k8s secret %s (namespace %s)", - x.K8s.SecretName, x.K8s.Namespace) + logger.Infof("Reading seed from k8s secret %s "+ + "(namespace %s)", x.K8s.SecretName, + x.K8s.Namespace) seed, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err @@ -258,8 +272,8 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, // The seed passphrase is optional. if x.K8s.SeedPassphraseKeyName != "" { - logger.Infof("Reading seed passphrase from k8s secret %s "+ - "(namespace %s)", x.K8s.SecretName, + logger.Infof("Reading seed passphrase from k8s secret "+ + "%s (namespace %s)", x.K8s.SecretName, x.K8s.Namespace) k8sSecret.KeyName = x.K8s.SeedPassphraseKeyName seedPassPhrase, _, err = readK8s(k8sSecret) @@ -268,13 +282,49 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, } } - logger.Infof("Reading wallet password from k8s secret %s (namespace %s)", - x.K8s.SecretName, x.K8s.Namespace) + logger.Infof("Reading wallet password from k8s secret %s "+ + "(namespace %s)", x.K8s.SecretName, x.K8s.Namespace) k8sSecret.KeyName = x.K8s.WalletPasswordKeyName walletPassword, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err } + + // Read passphrase from HashiCorp Vault secret. + case storageVault: + vaultSecret := &vaultSecretOptions{ + AuthTokenPath: x.Vault.AuthTokenPath, + AuthRole: x.Vault.AuthRole, + SecretName: x.Vault.SecretName, + } + vaultSecret.SecretKeyName = x.Vault.SeedKeyName + + logger.Infof("Reading seed from vault secret %s", + x.Vault.SecretName) + seed, _, err = readVault(vaultSecret) + if err != nil { + return "", "", "", err + } + + // The seed passphrase is optional. + if x.Vault.SeedPassphraseEntryName != "" { + logger.Infof("Reading seed passphrase from vault "+ + "secret %s", x.Vault.SecretName) + vaultSecret.SecretKeyName = + x.Vault.SeedPassphraseEntryName + seedPassPhrase, _, err = readVault(vaultSecret) + if err != nil { + return "", "", "", err + } + } + + logger.Infof("Reading wallet password from vault secret %s", + x.Vault.SecretName) + vaultSecret.SecretKeyName = x.Vault.WalletPasswordEntryName + walletPassword, _, err = readVault(vaultSecret) + if err != nil { + return "", "", "", err + } } // The seed, its passphrase and the wallet password should all never diff --git a/cmd_load_secret.go b/cmd_load_secret.go index db018c9..0be69f2 100644 --- a/cmd_load_secret.go +++ b/cmd_load_secret.go @@ -7,9 +7,10 @@ import ( ) type loadSecretCommand struct { - Source string `long:"source" short:"s" description:"Secret storage source" choice:"k8s"` - K8s *k8sSecretOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"` - Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"` + Source string `long:"source" short:"s" description:"Secret storage source" choice:"k8s"` + K8s *k8sSecretOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"` + Vault *vaultSecretOptions `group:"Flags for looking up the secret as a value inside a HashiCorp Vault (use when --source=vault)" namespace:"vault"` + Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"` } func newLoadSecretCommand() *loadSecretCommand { @@ -18,6 +19,9 @@ func newLoadSecretCommand() *loadSecretCommand { K8s: &k8sSecretOptions{ Namespace: defaultK8sNamespace, }, + Vault: &vaultSecretOptions{ + AuthTokenPath: defaultK8sServiceAccountTokenPath, + }, Output: outputFormatRaw, } } @@ -70,6 +74,27 @@ func (x *loadSecretCommand) Execute(_ []string) error { return nil + case storageVault: + content, secret, err := readVault(x.Vault) + if err != nil { + return fmt.Errorf("error reading secret %s from "+ + "vault: %v", x.Vault.SecretName, err) + } + + if x.Output == outputFormatJSON { + content, err = asJSON(&struct { + *jsonVaultObject `json:",inline"` + Value string `json:"value"` + }{ + jsonVaultObject: secret, + Value: content, + }) + } + + fmt.Printf("%s\n", content) + + return nil + default: return fmt.Errorf("invalid secret storage source %s", x.Source) } diff --git a/cmd_store_secret.go b/cmd_store_secret.go index d20ded5..cad0cf3 100644 --- a/cmd_store_secret.go +++ b/cmd_store_secret.go @@ -22,10 +22,11 @@ type entry struct { } type storeSecretCommand struct { - 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"` - Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"` - Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"` - K8s *targetK8sSecret `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"` + 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"` + Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"` + Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"` + K8s *targetK8sSecret `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"` + Vault *vaultSecretOptions `group:"Flags for storing the secret as a value inside a HashiCorp Vault (use when --target=vault)" namespace:"vault"` } func newStoreSecretCommand() *storeSecretCommand { @@ -39,6 +40,9 @@ func newStoreSecretCommand() *storeSecretCommand { ResourcePolicy: defaultK8sResourcePolicy, }, }, + Vault: &vaultSecretOptions{ + AuthTokenPath: defaultK8sServiceAccountTokenPath, + }, } } @@ -101,6 +105,15 @@ func (x *storeSecretCommand) Execute(args []string) error { return storeSecretsK8s(entries, x.K8s, x.Overwrite) + case storageVault: + // Take the actual entry name from the options if we aren't in + // batch mode. + if len(entries) == 1 && entries[0].key == "" { + entries[0].key = x.Vault.SecretKeyName + } + + return storeSecretsVault(entries, x.Vault, x.Overwrite) + default: return fmt.Errorf("invalid secret storage target %s", x.Target) } @@ -138,3 +151,34 @@ func storeSecretsK8s(entries []*entry, opts *targetK8sSecret, return nil } + +func storeSecretsVault(entries []*entry, opts *vaultSecretOptions, + overwrite bool) error { + + if opts.SecretName == "" { + return fmt.Errorf("secret name is required") + } + + for _, entry := range entries { + if entry.key == "" { + return fmt.Errorf("secret entry key is required") + } + + entryOpts := &vaultSecretOptions{ + AuthTokenPath: opts.AuthTokenPath, + AuthRole: opts.AuthRole, + SecretName: opts.SecretName, + SecretKeyName: entry.key, + } + + logger.Infof("Storing entry with name %s to secret %s in vault", + entryOpts.SecretKeyName, entryOpts.SecretName) + err := saveVault(entry.value, entryOpts, overwrite) + if err != nil { + return fmt.Errorf("error storing secret %s entry %s: "+ + "%v", opts.SecretName, entry.key, err) + } + } + + return nil +} diff --git a/example-init-wallet-vault.sh b/example-init-wallet-vault.sh new file mode 100644 index 0000000..dcd69a1 --- /dev/null +++ b/example-init-wallet-vault.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -e + +VAULT_AUTH_ROLE=${VAULT_AUTH_ROLE:-lnd-role} +WALLET_SECRET_NAME=${WALLET_SECRET_NAME:-lnd-wallet-secret} +WALLET_DIR=${WALLET_DIR:-~/.lnd/data/chain/bitcoin/mainnet} +WALLET_PASSWORD_FILE=${WALLET_PASSWORD_FILE:-/tmp/wallet-password} +CERT_DIR=${CERT_DIR:-~/.lnd} +UPLOAD_RPC_SECRETS=${UPLOAD_RPC_SECRETS:-0} +RPC_SECRETS_NAME=${RPC_SECRETS_NAME:-lnd-rpc-secret} + +echo "[STARTUP] Asserting wallet password exists in secret ${WALLET_SECRET_NAME}" +lndinit gen-password \ + | lndinit -v store-secret \ + --target=vault \ + --vault.auth-role="${VAULT_AUTH_ROLE}" \ + --vault.secret-name="${WALLET_SECRET_NAME}" \ + --vault.secret-key-name=wallet-password + +echo "" +echo "[STARTUP] Asserting seed exists in secret ${WALLET_SECRET_NAME}" +lndinit gen-seed \ + | lndinit -v store-secret \ + --target=vault \ + --vault.auth-role="${VAULT_AUTH_ROLE}" \ + --vault.secret-name="${WALLET_SECRET_NAME}" \ + --vault.secret-key-name=seed + +echo "" +echo "[STARTUP] Asserting wallet is created with values from secret ${WALLET_SECRET_NAME}" +lndinit -v init-wallet \ + --secret-source=vault \ + --vault.auth-role="${VAULT_AUTH_ROLE}" \ + --vault.secret-name="${WALLET_SECRET_NAME}" \ + --vault.seed-key-name=seed \ + --vault.wallet-password-entry-name=wallet-password \ + --output-wallet-dir="${WALLET_DIR}" \ + --validate-password + +echo "" +echo "[STARTUP] Preparing lnd auto unlock file" + +# To make sure the password can be read exactly once (by lnd itself), we create +# a named pipe. Because we can only write to such a pipe if there's a reader on +# the other end, we need to run this in a sub process in the background. +mkfifo "${WALLET_PASSWORD_FILE}" +lndinit -v load-secret \ + --source=vault \ + --vault.auth-role="${VAULT_AUTH_ROLE}" \ + --vault.secret-name="${WALLET_SECRET_NAME}" \ + --vault.secret-key-name=wallet-password > "${WALLET_PASSWORD_FILE}" & + +# In case we want to upload the TLS certificate and macaroons to k8s secrets as +# well once lnd is ready, we can use the wait-ready and store-secret commands in +# combination to wait until lnd is ready and then batch upload the files to k8s. +if [[ "${UPLOAD_RPC_SECRETS}" == "1" ]]; then + echo "" + echo "[STARTUP] Starting RPC secret uploader process in background" + lndinit -v wait-ready \ + && lndinit -v store-secret \ + --batch \ + --overwrite \ + --target=vault \ + --vault.auth-role="${VAULT_AUTH_ROLE}" \ + --vault.secret-name="${RPC_SECRETS_NAME}" \ + "${CERT_DIR}/tls.cert" \ + "${WALLET_DIR}"/*.macaroon & +fi + +# And finally start lnd. We need to use "exec" here to make sure all signals are +# forwarded correctly. +echo "" +echo "[STARTUP] Starting lnd with flags: $@" +exec lnd "$@" diff --git a/go.mod b/go.mod index 2f1b5d9..bc9bd6c 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display github.com/lightningnetwork/lnd v0.19.2-beta github.com/lightningnetwork/lnd/kvdb v1.4.16 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 go.etcd.io/bbolt v1.3.11 google.golang.org/grpc v1.59.0 k8s.io/api v0.18.3 @@ -20,8 +20,6 @@ require ( k8s.io/client-go v0.18.3 ) -require github.com/lightningnetwork/lnd/healthcheck v1.2.6 - require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect @@ -41,7 +39,7 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect @@ -56,6 +54,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect @@ -75,8 +74,16 @@ require ( github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/vault/api v1.20.0 github.com/imdario/mergo v0.3.12 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect @@ -107,6 +114,7 @@ require ( github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.8 // indirect + github.com/lightningnetwork/lnd/healthcheck v1.2.6 github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.9 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect @@ -116,7 +124,8 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/miekg/dns v1.1.43 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect @@ -133,6 +142,7 @@ require ( github.com/prometheus/procfs v0.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.9.2 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -163,17 +173,17 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect - golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.37.0 // indirect golang.org/x/oauth2 v0.14.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect diff --git a/go.sum b/go.sum index 918d453..100afa2 100644 --- a/go.sum +++ b/go.sum @@ -38,10 +38,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -90,8 +92,8 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -154,6 +156,9 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0= github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= @@ -164,6 +169,8 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -183,6 +190,8 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= @@ -259,12 +268,32 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9K github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= +github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -421,9 +450,13 @@ github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTO github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -433,8 +466,13 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -486,6 +524,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -515,6 +554,9 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -541,8 +583,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= @@ -630,8 +672,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -644,8 +686,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -669,8 +711,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -686,9 +728,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -723,12 +766,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -737,8 +780,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -760,8 +803,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index c4c2cf3..09ef985 100644 --- a/main.go +++ b/main.go @@ -21,8 +21,9 @@ const ( outputFormatRaw = "raw" outputFormatJSON = "json" - storageFile = "file" - storageK8s = "k8s" + storageFile = "file" + storageK8s = "k8s" + storageVault = "vault" errTargetExists = "target exists error" errInputMissing = "input missing error" diff --git a/vault.go b/vault.go new file mode 100644 index 0000000..b81f7cf --- /dev/null +++ b/vault.go @@ -0,0 +1,230 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/hashicorp/vault/api" +) + +const ( + defaultK8sServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/" + + "serviceaccount/token" + + vaultApiPathK8sAuth = "/v1/auth/kubernetes/login" +) + +type jsonAuthResponse struct { + Auth struct { + ClientToken string `json:"client_token"` + Accessor string `json:"accessor"` + Policies []string `json:"policies"` + LeaseDuration int `json:"lease_duration"` + Renewable bool `json:"renewable"` + Metadata struct { + Role string `json:"role"` + ServiceAccountName string `json:"service_account_name"` + ServiceAccountNamespace string `json:"service_account_namespace"` + ServiceAccountSecretName string `json:"service_account_secret_name"` + ServiceAccountUID string `json:"service_account_uid"` + } `json:"metadata"` + } `json:"auth"` +} + +type jsonK8sAuthConfig struct { + Role string `json:"role"` + JWT string `json:"jwt"` +} + +type vaultSecretOptions struct { + AuthTokenPath string `long:"auth-token-path" description:"The full path to the token file that should be used to authenticate against HashiCorp Vault"` + AuthRole string `long:"auth-role" description:"The role to acquire when logging into HashiCorp Vault"` + SecretName string `long:"secret-name" description:"The name of the Vault secret"` + SecretKeyName string `long:"secret-key-name" description:"The name of the key/entry within the secret"` +} + +type jsonVaultObject struct { + RequestID string `json:"request_id"` + LeaseID string `json:"lease_id"` + LeaseDuration int `json:"lease_duration"` + Renewable bool `json:"renewable"` +} + +func saveVault(content string, opts *vaultSecretOptions, overwrite bool) error { + client, err := getClientVault(opts) + if err != nil { + return err + } + + secret, exists, err := getSecretVault(client, opts.SecretName) + if err != nil { + return err + } + + secretData := make(map[string]interface{}) + if exists { + secretData = secret.Data + } + + return storeSecretVault(client, secretData, opts, overwrite, content) +} + +func readVault(opts *vaultSecretOptions) (string, *jsonVaultObject, error) { + client, err := getClientVault(opts) + if err != nil { + return "", nil, err + } + + secret, exists, err := getSecretVault(client, opts.SecretName) + if err != nil { + return "", nil, err + } + + if !exists { + return "", nil, fmt.Errorf("secret %s does not exist in vault", + opts.SecretName) + } + + if len(secret.Data) == 0 { + return "", nil, fmt.Errorf("secret %s exists but contains no "+ + "data", opts.SecretName) + } + + entry := secret.Data[opts.SecretKeyName] + stringEntry, isString := entry.(string) + if entry == nil || (isString && len(stringEntry) == 0) { + return "", nil, fmt.Errorf("secret %s exists but does not "+ + "contain the entry %s", opts.SecretName, + opts.SecretKeyName) + } + + // Remove any newlines at the end of the file. We won't ever write a + // newline ourselves but maybe the file was provisioned by another + // process or user. + content := strings.TrimRight(stringEntry, "\r\n") + + return content, &jsonVaultObject{ + RequestID: secret.RequestID, + LeaseID: secret.LeaseID, + LeaseDuration: secret.LeaseDuration, + Renewable: secret.Renewable, + }, nil +} + +func getClientVault(opts *vaultSecretOptions) (*api.Client, error) { + logger.Infof("Creating vault config from environment") + cfg := api.DefaultConfig() + if cfg.Error != nil { + return nil, fmt.Errorf("error reading vault config from env: "+ + "%v", cfg.Error) + } + + logger.Infof("Creating vault client") + client, err := api.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("error creating vault client: %v", err) + } + + logger.Infof("Reading auth token %s", opts.AuthTokenPath) + token, err := readFile(opts.AuthTokenPath) + if err != nil { + return nil, fmt.Errorf("error reading auth token %s: %v", + opts.AuthTokenPath, err) + } + + if len(token) == 0 { + return nil, fmt.Errorf("token %s is empty", opts.AuthTokenPath) + } + if len(opts.AuthRole) == 0 { + return nil, fmt.Errorf("the --auth-role flag must be set") + } + + logger.Infof("Authenticating with auth token") + conf := &jsonK8sAuthConfig{ + JWT: token, + Role: opts.AuthRole, + } + req := client.NewRequest("POST", vaultApiPathK8sAuth) + if err := req.SetJSONBody(conf); err != nil { + return nil, fmt.Errorf("error marshalling auth config: %v", err) + } + + res, err := client.RawRequest(req) + if err != nil { + return nil, fmt.Errorf("error sending auth request: %v", err) + } + defer func() { + _ = res.Body.Close() + }() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error response from vault server: "+ + "%d - %s", res.StatusCode, res.Status) + } + + authResponse := &jsonAuthResponse{} + respBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + if err = json.Unmarshal(respBody, authResponse); err != nil { + return nil, fmt.Errorf("error parsing auth response: %v", err) + } + + logger.Infof("Authenticated successfully with vault server, acquired "+ + "policies %v", authResponse.Auth.Policies) + client.SetToken(authResponse.Auth.ClientToken) + + logger.Infof("Vault client created successfully") + return client, nil +} + +func getSecretVault(client *api.Client, name string) (*api.Secret, bool, + error) { + + logger.Infof("Attempting to load secret %s from vault", name) + secret, err := client.Logical().Read(name) + + switch { + case err == nil && secret != nil: + logger.Infof("Secret %s loaded successfully", name) + return secret, true, nil + + case secret == nil: + logger.Infof("Secret %s not found in vault, got error %v", name, + err) + return nil, false, nil + + default: + return nil, false, fmt.Errorf("error querying secret "+ + "existence: %v", err) + } +} + +func storeSecretVault(client *api.Client, secretData map[string]interface{}, + opts *vaultSecretOptions, overwrite bool, content string) error { + + if secretData[opts.SecretKeyName] != nil && !overwrite { + return fmt.Errorf("entry %s in secret %s already exists: %v", + opts.SecretKeyName, opts.SecretName, + errTargetExists) + } + + secretData[opts.SecretKeyName] = content + + logger.Infof("Attempting to update entry %s of secret %s in vault", + opts.SecretKeyName, opts.SecretName) + + _, err := client.Logical().Write(opts.SecretName, secretData) + if err != nil { + return fmt.Errorf("error updating secret %s in vault: %v", + opts.SecretName, err) + } + + logger.Infof("Updated secret %s", opts.SecretName) + + return nil +}