vault/command/server/config_test.go
Ryan Cragun 012cd5a42a
VAULT-33008: ipv6: always display RFC-5952 §4 conformant addresses (#29228)
USGv6[0] requires implementing §4.1.1 of the NISTv6-r1 profile[1] for
IPv6-Only capabilities. This section requires that whenever Vault
displays IPv6 addresses (including CLI output, Web UI, logs, etc.) that
_all_ IPv6 addresses must conform to RFC-5952 §4 text representation
recommendations[2].

These recommendations do not prevent us from accepting RFC-4241[3] IPv6
addresses, however, whenever these same addresses are displayed they
must conform to the strict RFC-5952 §4 guidelines.

This PR implements handling of IPv6 address conformance in our
`vault server` routine. We handle conformance normalization for all
server, http_proxy, listener, seal, storage and telemetry
configuration where an input could contain an IPv6 address, whether
configured via an HCL file or via corresponding environment variables.

The approach I've taken is to handle conformance normalization at
parse time to ensure that all log output and subsequent usage
inside of Vaults various subsystems always reference a conformant
address, that way we don't need concern ourselves with conformance
later. This approach ought to be backwards compatible to prior loose
address configuration requirements, with the understanding that
going forward all IPv6 representation will be strict regardless of
what has been configured.

In many cases I've updated our various parser functions to call the
new `configutil.NormalizeAddr()` to apply conformance normalization.
Others required no changes because they rely on standard library URL
string output, which always displays IPv6 URLs in a conformant way.

Not included in this changes is any other vault exec mode other than
server. Client, operator commands, agent mode, proxy mode, etc. will
be included in subsequent changes if necessary.

[0]: https://www.nist.gov/publications/usgv6-profile
[1]: https://www.nist.gov/publications/nist-ipv6-profile
[2]: https://www.rfc-editor.org/rfc/rfc5952.html#section-4
[3]: https://www.rfc-editor.org/rfc/rfc4291

Signed-off-by: Ryan Cragun <me@ryan.ec>
2025-01-27 14:14:28 -07:00

305 lines
7.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package server
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/stretchr/testify/require"
)
func TestLoadConfigFile(t *testing.T) {
testLoadConfigFile(t)
}
func TestLoadConfigFile_json(t *testing.T) {
testLoadConfigFile_json(t)
}
func TestLoadConfigFileIntegerAndBooleanValues(t *testing.T) {
testLoadConfigFileIntegerAndBooleanValues(t)
}
func TestLoadConfigFileIntegerAndBooleanValuesJson(t *testing.T) {
testLoadConfigFileIntegerAndBooleanValuesJson(t)
}
func TestLoadConfigFileWithLeaseMetricTelemetry(t *testing.T) {
testLoadConfigFileLeaseMetrics(t)
}
func TestLoadConfigDir(t *testing.T) {
testLoadConfigDir(t)
}
func TestConfig_Sanitized(t *testing.T) {
testConfig_Sanitized(t)
}
func TestParseListeners(t *testing.T) {
testParseListeners(t)
}
func TestParseUserLockouts(t *testing.T) {
testParseUserLockouts(t)
}
func TestParseSockaddrTemplate(t *testing.T) {
testParseSockaddrTemplate(t)
}
func TestConfigRaftRetryJoin(t *testing.T) {
testConfigRaftRetryJoin(t)
}
func TestParseSeals(t *testing.T) {
testParseSeals(t)
}
func TestParseStorage(t *testing.T) {
testParseStorageTemplate(t)
}
// TestParseStorageURLConformance tests that all config attrs whose values can be
// URLs, IP addresses, or host:port addresses, when configured with an IPv6
// address, the normalized to be conformant with RFC-5942 §4
// See: https://rfc-editor.org/rfc/rfc5952.html
func TestParseStorageURLConformance(t *testing.T) {
testParseStorageURLConformance(t)
}
// TestConfigWithAdministrativeNamespace tests that .hcl and .json configurations are correctly parsed when the administrative_namespace_path is present.
func TestConfigWithAdministrativeNamespace(t *testing.T) {
testConfigWithAdministrativeNamespaceHcl(t)
testConfigWithAdministrativeNamespaceJson(t)
}
func TestUnknownFieldValidation(t *testing.T) {
testUnknownFieldValidation(t)
}
func TestUnknownFieldValidationJson(t *testing.T) {
testUnknownFieldValidationJson(t)
}
func TestUnknownFieldValidationHcl(t *testing.T) {
testUnknownFieldValidationHcl(t)
}
func TestUnknownFieldValidationListenerAndStorage(t *testing.T) {
testUnknownFieldValidationStorageAndListener(t)
}
func TestExperimentsConfigParsing(t *testing.T) {
const envKey = "VAULT_EXPERIMENTS"
originalValue := validExperiments
validExperiments = []string{"foo", "bar", "baz"}
t.Cleanup(func() {
validExperiments = originalValue
})
for name, tc := range map[string]struct {
fromConfig []string
fromEnv []string
fromCLI []string
expected []string
expectedError string
}{
// Multiple sources.
"duplication": {[]string{"foo"}, []string{"foo"}, []string{"foo"}, []string{"foo"}, ""},
"disjoint set": {[]string{"foo"}, []string{"bar"}, []string{"baz"}, []string{"foo", "bar", "baz"}, ""},
// Single source.
"config only": {[]string{"foo"}, nil, nil, []string{"foo"}, ""},
"env only": {nil, []string{"foo"}, nil, []string{"foo"}, ""},
"CLI only": {nil, nil, []string{"foo"}, []string{"foo"}, ""},
// Validation errors.
"config invalid": {[]string{"invalid"}, nil, nil, nil, "from config"},
"env invalid": {nil, []string{"invalid"}, nil, nil, "from environment variable"},
"CLI invalid": {nil, nil, []string{"invalid"}, nil, "from command line flag"},
} {
t.Run(name, func(t *testing.T) {
var configString string
t.Setenv(envKey, strings.Join(tc.fromEnv, ","))
if len(tc.fromConfig) != 0 {
configString = fmt.Sprintf("experiments = [\"%s\"]", strings.Join(tc.fromConfig, "\", \""))
}
config, err := ParseConfig(configString, "")
if err == nil {
err = ExperimentsFromEnvAndCLI(config, envKey, tc.fromCLI)
}
switch tc.expectedError {
case "":
if err != nil {
t.Fatal(err)
}
default:
if err == nil || !strings.Contains(err.Error(), tc.expectedError) {
t.Fatalf("Expected error to contain %q, but got: %s", tc.expectedError, err)
}
}
})
}
}
func TestValidate(t *testing.T) {
originalValue := validExperiments
for name, tc := range map[string]struct {
validSet []string
input []string
expectError bool
}{
// Valid cases
"minimal valid": {[]string{"foo"}, []string{"foo"}, false},
"valid subset": {[]string{"foo", "bar"}, []string{"bar"}, false},
"repeated": {[]string{"foo"}, []string{"foo", "foo"}, false},
// Error cases
"partially valid": {[]string{"foo", "bar"}, []string{"foo", "baz"}, true},
"empty": {[]string{"foo"}, []string{""}, true},
"no valid experiments": {[]string{}, []string{"foo"}, true},
} {
t.Run(name, func(t *testing.T) {
t.Cleanup(func() {
validExperiments = originalValue
})
validExperiments = tc.validSet
err := validateExperiments(tc.input)
if tc.expectError && err == nil {
t.Fatal("Expected error but got none")
}
if !tc.expectError && err != nil {
t.Fatal("Did not expect error but got", err)
}
})
}
}
func TestMerge(t *testing.T) {
for name, tc := range map[string]struct {
left []string
right []string
expected []string
}{
"disjoint": {[]string{"foo"}, []string{"bar"}, []string{"foo", "bar"}},
"empty left": {[]string{}, []string{"foo"}, []string{"foo"}},
"empty right": {[]string{"foo"}, []string{}, []string{"foo"}},
"overlapping": {[]string{"foo", "bar"}, []string{"foo", "baz"}, []string{"foo", "bar", "baz"}},
} {
t.Run(name, func(t *testing.T) {
result := mergeExperiments(tc.left, tc.right)
if !reflect.DeepEqual(tc.expected, result) {
t.Fatalf("Expected %v but got %v", tc.expected, result)
}
})
}
}
// Test_parseDevTLSConfig verifies that both Windows and Unix directories are correctly escaped when creating a dev TLS
// configuration in HCL
func Test_parseDevTLSConfig(t *testing.T) {
tests := []struct {
name string
certDirectory string
}{
{
name: "windows path",
certDirectory: `C:\Users\ADMINI~1\AppData\Local\Temp\2\vault-tls4169358130`,
},
{
name: "unix path",
certDirectory: "/tmp/vault-tls4169358130",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := parseDevTLSConfig("file", tt.certDirectory)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("%s/%s", tt.certDirectory, VaultDevCertFilename), cfg.Listeners[0].TLSCertFile)
require.Equal(t, fmt.Sprintf("%s/%s", tt.certDirectory, VaultDevKeyFilename), cfg.Listeners[0].TLSKeyFile)
})
}
}
func TestCheckConfig(t *testing.T) {
testCases := []struct {
name string
config *Config
expectError bool
}{
{
name: "no-seals-configured",
config: &Config{SharedConfig: &configutil.SharedConfig{Seals: []*configutil.KMS{}}},
expectError: false,
},
{
name: "seal-with-empty-name",
config: &Config{SharedConfig: &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "awskms",
Disabled: false,
},
},
}},
expectError: true,
},
{
name: "seals-with-unique-names",
config: &Config{SharedConfig: &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "awskms",
Disabled: false,
Name: "enabled-awskms",
},
{
Type: "awskms",
Disabled: true,
Name: "disabled-awskms",
},
},
}},
expectError: false,
},
{
name: "seals-with-same-names",
config: &Config{SharedConfig: &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "awskms",
Disabled: false,
Name: "awskms",
},
{
Type: "awskms",
Disabled: true,
Name: "awskms",
},
},
}},
expectError: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
_, err := CheckConfig(tt.config, nil)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}