mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-30 08:19:09 +01:00
[self-hosted] Override relay auth secret and store encryption key from the environment (#8405)
* [self-hosted] Override relay auth secret and store encryption key from the environment Combined server.authSecret and server.store.encryptionKey could only be set in YAML, so operators could not keep config.yaml in git without secrets. A non-empty NB_AUTH_SECRET or NB_STORE_ENCRYPTION_KEY now overrides those fields after YAML is parsed and before defaults and validation, matching the standalone relay env name for the auth secret. * [self-hosted] Cover secret env overrides when no config path is given LoadConfig with an empty path starts from DefaultConfig and applies NB_AUTH_SECRET and NB_STORE_ENCRYPTION_KEY without reading YAML. The existing tests only used YAML paths, so that branch had no coverage.
This commit is contained in:
@@ -21,6 +21,13 @@ import (
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
)
|
||||
|
||||
const (
|
||||
// envRelayAuthSecret is the standalone relay --auth-secret env name so split
|
||||
// and combined deployments share it.
|
||||
envRelayAuthSecret = "NB_AUTH_SECRET" //nolint:gosec // env var name, not a credential
|
||||
envStoreEncryptionKey = "NB_STORE_ENCRYPTION_KEY"
|
||||
)
|
||||
|
||||
// CombinedConfig is the root configuration for the combined server.
|
||||
// The combined server is primarily a Management server with optional embedded
|
||||
// Signal, Relay, and STUN services.
|
||||
@@ -446,6 +453,7 @@ func LoadConfig(configPath string) (*CombinedConfig, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if configPath == "" {
|
||||
applyServerSecretEnv(cfg)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -458,12 +466,27 @@ func LoadConfig(configPath string) (*CombinedConfig, error) {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
applyServerSecretEnv(cfg)
|
||||
|
||||
// Populate internal configs from server settings
|
||||
cfg.ApplySimplifiedDefaults()
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// applyServerSecretEnv copies non-empty environment overrides onto the parsed
|
||||
// YAML values before defaults and validation run.
|
||||
func applyServerSecretEnv(cfg *CombinedConfig) {
|
||||
if secret := os.Getenv(envRelayAuthSecret); secret != "" {
|
||||
cfg.Server.AuthSecret = secret
|
||||
log.Infof("server.authSecret taken from %s", envRelayAuthSecret)
|
||||
}
|
||||
if key := os.Getenv(envStoreEncryptionKey); key != "" {
|
||||
cfg.Server.Store.EncryptionKey = key
|
||||
log.Infof("server.store.encryptionKey taken from %s", envStoreEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c *CombinedConfig) Validate() error {
|
||||
if c.Server.ExposedAddress == "" {
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func unsetSecretEnvs(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv(envRelayAuthSecret, "")
|
||||
t.Setenv(envStoreEncryptionKey, "")
|
||||
require.NoError(t, os.Unsetenv(envRelayAuthSecret))
|
||||
require.NoError(t, os.Unsetenv(envStoreEncryptionKey))
|
||||
}
|
||||
|
||||
func writeCombinedConfig(t *testing.T, authSecret, encryptionKey string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
content := fmt.Sprintf(`
|
||||
server:
|
||||
exposedAddress: "https://example.com:443"
|
||||
dataDir: %q
|
||||
authSecret: %q
|
||||
store:
|
||||
encryptionKey: %q
|
||||
`, dir, authSecret, encryptionKey)
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
||||
return path
|
||||
}
|
||||
|
||||
func TestLoadConfigRelayAuthSecretEnv(t *testing.T) {
|
||||
envSecret := "env-relay-auth-secret"
|
||||
yamlSecret := "yaml-relay-auth-secret"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlSecret string
|
||||
envValue string
|
||||
envSet bool
|
||||
wantSecret string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "env unset uses YAML",
|
||||
yamlSecret: yamlSecret,
|
||||
wantSecret: yamlSecret,
|
||||
},
|
||||
{
|
||||
name: "env overrides YAML",
|
||||
yamlSecret: yamlSecret,
|
||||
envValue: envSecret,
|
||||
envSet: true,
|
||||
wantSecret: envSecret,
|
||||
},
|
||||
{
|
||||
name: "empty env leaves YAML",
|
||||
yamlSecret: yamlSecret,
|
||||
envValue: "",
|
||||
envSet: true,
|
||||
wantSecret: yamlSecret,
|
||||
},
|
||||
{
|
||||
name: "env set with empty YAML passes validation",
|
||||
yamlSecret: "",
|
||||
envValue: envSecret,
|
||||
envSet: true,
|
||||
wantSecret: envSecret,
|
||||
},
|
||||
{
|
||||
name: "neither set requires authSecret",
|
||||
yamlSecret: "",
|
||||
wantErr: "server.authSecret is required when running local relay",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
unsetSecretEnvs(t)
|
||||
if tc.envSet {
|
||||
t.Setenv(envRelayAuthSecret, tc.envValue)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(writeCombinedConfig(t, tc.yamlSecret, "yaml-store-key"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.Validate()
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr, "validation error should mention authSecret")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantSecret, cfg.Server.AuthSecret, "server.authSecret")
|
||||
assert.Equal(t, tc.wantSecret, cfg.Relay.AuthSecret, "relay.authSecret")
|
||||
assert.Equal(t, tc.wantSecret, cfg.Management.Relays.Secret, "management.relays.secret")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigStoreEncryptionKeyEnv(t *testing.T) {
|
||||
envKey := "env-store-encryption-key"
|
||||
yamlKey := "yaml-store-encryption-key"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlKey string
|
||||
envValue string
|
||||
envSet bool
|
||||
wantKey string
|
||||
}{
|
||||
{
|
||||
name: "env unset uses YAML",
|
||||
yamlKey: yamlKey,
|
||||
wantKey: yamlKey,
|
||||
},
|
||||
{
|
||||
name: "env overrides YAML",
|
||||
yamlKey: yamlKey,
|
||||
envValue: envKey,
|
||||
envSet: true,
|
||||
wantKey: envKey,
|
||||
},
|
||||
{
|
||||
name: "empty env leaves YAML",
|
||||
yamlKey: yamlKey,
|
||||
envValue: "",
|
||||
envSet: true,
|
||||
wantKey: yamlKey,
|
||||
},
|
||||
{
|
||||
name: "env set with empty YAML",
|
||||
yamlKey: "",
|
||||
envValue: envKey,
|
||||
envSet: true,
|
||||
wantKey: envKey,
|
||||
},
|
||||
{
|
||||
name: "neither set leaves encryption key empty",
|
||||
yamlKey: "",
|
||||
wantKey: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
unsetSecretEnvs(t)
|
||||
if tc.envSet {
|
||||
t.Setenv(envStoreEncryptionKey, tc.envValue)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(writeCombinedConfig(t, "yaml-relay-secret", tc.yamlKey))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
assert.Equal(t, tc.wantKey, cfg.Server.Store.EncryptionKey, "server.store.encryptionKey")
|
||||
assert.Equal(t, tc.wantKey, cfg.Management.Store.EncryptionKey, "management.store.encryptionKey")
|
||||
|
||||
mgmt, err := cfg.ToManagementConfig()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantKey, mgmt.DataStoreEncryptionKey, "management DataStoreEncryptionKey")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigNoPathSecretEnv(t *testing.T) {
|
||||
unsetSecretEnvs(t)
|
||||
t.Setenv(envRelayAuthSecret, "env-relay-auth-secret")
|
||||
t.Setenv(envStoreEncryptionKey, "env-store-encryption-key")
|
||||
|
||||
cfg, err := LoadConfig("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "env-relay-auth-secret", cfg.Server.AuthSecret, "server.authSecret")
|
||||
assert.Equal(t, "env-store-encryption-key", cfg.Server.Store.EncryptionKey, "server.store.encryptionKey")
|
||||
}
|
||||
|
||||
func TestApplyServerSecretEnvLogsNameNotValue(t *testing.T) {
|
||||
unsetSecretEnvs(t)
|
||||
secret := "relay-secret-must-not-appear"
|
||||
key := "store-key-must-not-appear"
|
||||
t.Setenv(envRelayAuthSecret, secret)
|
||||
t.Setenv(envStoreEncryptionKey, key)
|
||||
|
||||
buf := captureLogOutput(t)
|
||||
cfg := DefaultConfig()
|
||||
applyServerSecretEnv(cfg)
|
||||
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, envRelayAuthSecret, "info log should name the relay auth env var")
|
||||
assert.Contains(t, out, envStoreEncryptionKey, "info log should name the store encryption env var")
|
||||
assert.NotContains(t, out, secret, "info log must not include the relay auth secret")
|
||||
assert.NotContains(t, out, key, "info log must not include the store encryption key")
|
||||
assert.Equal(t, secret, cfg.Server.AuthSecret, "auth secret should be taken from the environment")
|
||||
assert.Equal(t, key, cfg.Server.Store.EncryptionKey, "encryption key should be taken from the environment")
|
||||
}
|
||||
|
||||
func TestLogEnvVarsMasksRelayAndStoreSecrets(t *testing.T) {
|
||||
unsetSecretEnvs(t)
|
||||
secret := "relay-secret-must-not-appear"
|
||||
key := "store-key-must-not-appear"
|
||||
t.Setenv(envRelayAuthSecret, secret)
|
||||
t.Setenv(envStoreEncryptionKey, key)
|
||||
|
||||
buf := captureLogOutput(t)
|
||||
logEnvVars()
|
||||
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, envRelayAuthSecret+"=", "logEnvVars should list the relay auth env var")
|
||||
assert.Contains(t, out, envStoreEncryptionKey+"=", "logEnvVars should list the store encryption env var")
|
||||
assert.NotContains(t, out, secret, "logEnvVars must not include the relay auth secret")
|
||||
assert.NotContains(t, out, key, "logEnvVars must not include the store encryption key")
|
||||
assert.Contains(t, out, maskSecret(secret), "logEnvVars should mask the relay auth secret")
|
||||
assert.Contains(t, out, maskSecret(key), "logEnvVars should mask the store encryption key")
|
||||
}
|
||||
|
||||
func captureLogOutput(t *testing.T) *bytes.Buffer {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
logger := log.StandardLogger()
|
||||
origOut := logger.Out
|
||||
origLevel := logger.GetLevel()
|
||||
logger.SetOutput(&buf)
|
||||
logger.SetLevel(log.DebugLevel)
|
||||
t.Cleanup(func() {
|
||||
logger.SetOutput(origOut)
|
||||
logger.SetLevel(origLevel)
|
||||
})
|
||||
return &buf
|
||||
}
|
||||
Reference in New Issue
Block a user