diff --git a/config.go b/config.go index 5cf54deceb..f9aec0284d 100644 --- a/config.go +++ b/config.go @@ -1779,6 +1779,11 @@ func parseRPCParams(cConfig *lncfg.Chain, nodeConfig interface{}, var daemonName, confDir, confFile, confFileBase string switch conf := nodeConfig.(type) { case *lncfg.Btcd: + // Resolves environment variable references in RPCUser and + // RPCPass fields. + conf.RPCUser = supplyEnvValue(conf.RPCUser) + conf.RPCPass = supplyEnvValue(conf.RPCPass) + // If both RPCUser and RPCPass are set, we assume those // credentials are good to use. if conf.RPCUser != "" && conf.RPCPass != "" { @@ -1824,6 +1829,11 @@ func parseRPCParams(cConfig *lncfg.Chain, nodeConfig interface{}, confFile = conf.ConfigPath confFileBase = BitcoinChainName + // Resolves environment variable references in RPCUser + // and RPCPass fields. + conf.RPCUser = supplyEnvValue(conf.RPCUser) + conf.RPCPass = supplyEnvValue(conf.RPCPass) + // Check that cookie and credentials don't contradict each // other. if (conf.RPCUser != "" || conf.RPCPass != "") && @@ -1919,6 +1929,70 @@ func parseRPCParams(cConfig *lncfg.Chain, nodeConfig interface{}, return nil } +// supplyEnvValue supplies the value of an environment variable from a string. +// It supports the following formats: +// 1) $ENV_VAR +// 2) ${ENV_VAR} +// 3) ${ENV_VAR:-DEFAULT} +// +// Standard environment variable naming conventions: +// - ENV_VAR contains letters, digits, and underscores, and does +// not start with a digit. +// - DEFAULT follows the rule that it can contain any characters except +// whitespace. +// +// Parameters: +// - value: The input string containing references to environment variables +// (if any). +// +// Returns: +// - string: The value of the specified environment variable, the default +// value if provided, or the original input string if no matching variable is +// found or set. +func supplyEnvValue(value string) string { + // Regex for $ENV_VAR format. + var reEnvVar = regexp.MustCompile(`^\$([a-zA-Z_][a-zA-Z0-9_]*)$`) + + // Regex for ${ENV_VAR} format. + var reEnvVarWithBrackets = regexp.MustCompile( + `^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$`, + ) + + // Regex for ${ENV_VAR:-DEFAULT} format. + var reEnvVarWithDefault = regexp.MustCompile( + `^\$\{([a-zA-Z_][a-zA-Z0-9_]*):-([\S]+)\}$`, + ) + + // Match against supported formats. + switch { + case reEnvVarWithDefault.MatchString(value): + matches := reEnvVarWithDefault.FindStringSubmatch(value) + envVariable := matches[1] + defaultValue := matches[2] + if envValue := os.Getenv(envVariable); envValue != "" { + return envValue + } + + return defaultValue + + case reEnvVarWithBrackets.MatchString(value): + matches := reEnvVarWithBrackets.FindStringSubmatch(value) + envVariable := matches[1] + envValue := os.Getenv(envVariable) + + return envValue + + case reEnvVar.MatchString(value): + matches := reEnvVar.FindStringSubmatch(value) + envVariable := matches[1] + envValue := os.Getenv(envVariable) + + return envValue + } + + return value +} + // extractBtcdRPCParams attempts to extract the RPC credentials for an existing // btcd instance. The passed path is expected to be the location of btcd's // application data directory on the target system. @@ -1962,7 +2036,8 @@ func extractBtcdRPCParams(btcdConfigPath string) (string, string, error) { return "", "", fmt.Errorf("unable to find rpcuser in config") } - return string(userSubmatches[1]), string(passSubmatches[1]), nil + return supplyEnvValue(string(userSubmatches[1])), + supplyEnvValue(string(passSubmatches[1])), nil } // extractBitcoindRPCParams attempts to extract the RPC credentials for an @@ -2087,7 +2162,8 @@ func extractBitcoindRPCParams(networkName, bitcoindDataDir, bitcoindConfigPath, "in config") } - return string(userSubmatches[1]), string(passSubmatches[1]), + return supplyEnvValue(string(userSubmatches[1])), + supplyEnvValue(string(passSubmatches[1])), zmqBlockHost, zmqTxHost, nil } diff --git a/config_test.go b/config_test.go index 0c82db28d4..6383a99003 100644 --- a/config_test.go +++ b/config_test.go @@ -52,3 +52,66 @@ func TestConfigToFlatMap(t *testing.T) { require.Equal(t, redactedPassword, result["db.etcd.pass"]) require.Equal(t, redactedPassword, result["db.postgres.dsn"]) } + +// TestSupplyEnvValue tests that the supplyEnvValue function works as +// expected on the passed inputs. +func TestSupplyEnvValue(t *testing.T) { + // Mock environment variables for testing. + t.Setenv("EXISTING_VAR", "existing_value") + t.Setenv("EMPTY_VAR", "") + + tests := []struct { + input string + expected string + description string + }{ + { + input: "$EXISTING_VAR", + expected: "existing_value", + description: "Valid environment variable without " + + "default value", + }, + { + input: "${EXISTING_VAR:-default_value}", + expected: "existing_value", + description: "Valid environment variable with " + + "default value", + }, + { + input: "$NON_EXISTENT_VAR", + expected: "", + description: "Non-existent environment variable " + + "without default value", + }, + { + input: "${NON_EXISTENT_VAR:-default_value}", + expected: "default_value", + description: "Non-existent environment variable " + + "with default value", + }, + { + input: "$EMPTY_VAR", + expected: "", + description: "Empty environment variable without " + + "default value", + }, + { + input: "${EMPTY_VAR:-default_value}", + expected: "default_value", + description: "Empty environment variable with " + + "default value", + }, + { + input: "raw_input", + expected: "raw_input", + description: "Raw input - no matching format", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + result := supplyEnvValue(test.input) + require.Equal(t, test.expected, result) + }) + } +} diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index ed485af77c..fdc9239701 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -145,6 +145,9 @@ funding operations and the new `PsbtCoinSelect` option of the `FundPsbt` RPC](https://github.com/lightningnetwork/lnd/pull/8378). +* [Env Variables in lnd.conf](https://github.com/lightningnetwork/lnd/pull/8310) + Support utilizing the usage of environment variables in `lnd.conf` for `rpcuser` and `rpcpass` fields to better protect the secrets. + ## RPC Additions * [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)