Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lnconfig: Support utilizing Environment Variables in lnd.conf for rpcuser and rpcpass fields. #8310

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 78 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 != "") &&
Expand Down Expand Up @@ -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):
guggero marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
63 changes: 63 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
guggero marked this conversation as resolved.
Show resolved Hide resolved
// 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)
})
}
}
3 changes: 3 additions & 0 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading