diff --git a/command/agent.go b/command/agent.go index 2e5f550a55f9..712f194efcde 100644 --- a/command/agent.go +++ b/command/agent.go @@ -939,12 +939,13 @@ func (c *AgentCommand) applyConfigOverrides(f *FlagSets, config *agentConfig.Con }) c.setStringFlag(f, config.Vault.Address, &StringVar{ - Name: flagNameAddress, - Target: &c.flagAddress, - Default: "https://127.0.0.1:8200", - EnvVar: api.EnvVaultAddress, + Name: flagNameAddress, + Target: &c.flagAddress, + Default: "https://127.0.0.1:8200", + EnvVar: api.EnvVaultAddress, + Normalizers: []func(string) string{configutil.NormalizeAddr}, }) - config.Vault.Address = c.flagAddress + config.Vault.Address = configutil.NormalizeAddr(c.flagAddress) c.setStringFlag(f, config.Vault.CACert, &StringVar{ Name: flagNameCACert, Target: &c.flagCACert, @@ -1029,6 +1030,7 @@ func (c *AgentCommand) setStringFlag(f *FlagSets, configVal string, fVar *String switch { case isFlagSet: // Don't do anything as the flag is already set from the command line + return case flagEnvSet: // Use value from env var *fVar.Target = flagEnvValue diff --git a/command/agent/config/config.go b/command/agent/config/config.go index 7b9a942824ec..75aaebaf6377 100644 --- a/command/agent/config/config.go +++ b/command/agent/config/config.go @@ -743,6 +743,10 @@ func parseVault(result *Config, list *ast.ObjectList) error { return err } + if v.Address != "" { + v.Address = configutil.NormalizeAddr(v.Address) + } + if v.TLSSkipVerifyRaw != nil { v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw) if err != nil { @@ -1030,10 +1034,63 @@ func parseMethod(result *Config, list *ast.ObjectList) error { // Canonicalize namespace path if provided m.Namespace = namespace.Canonicalize(m.Namespace) + // Normalize any configuration addresses + if len(m.Config) > 0 { + var err error + for k, v := range m.Config { + vStr, ok := v.(string) + if !ok { + continue + } + m.Config[k], err = normalizeAutoAuthMethod(m.Type, k, vStr) + if err != nil { + return err + } + } + } + result.AutoAuth.Method = &m return nil } +// autoAuthMethodKeys maps an auto-auth method type to its associated +// configuration whose values are URLs, IP addresses, or host:port style +// addresses. All auto-auth types must have an entry in this map, otherwise our +// normalization check will fail when parsing the storage entry config. +// Auto-auth method types which don't contain such keys should include an empty +// array. +var autoAuthMethodKeys = map[string][]string{ + "alicloud": {""}, + "approle": {""}, + "aws": {""}, + "azure": {"resource"}, + "cert": {""}, + "cf": {""}, + "gcp": {"service_account"}, + "jwt": {""}, + "ldap": {""}, + "kerberos": {""}, + "kubernetes": {""}, + "oci": {""}, + "token_file": {""}, +} + +// normalizeAutoAuthMethod takes a storage name, a configuration key +// and it's associated value and will normalize any URLs, IP addresses, or +// host:port style addresses. +func normalizeAutoAuthMethod(method string, key string, value string) (string, error) { + keys, ok := autoAuthMethodKeys[method] + if !ok { + return "", fmt.Errorf("unknown auto-auth method type %s", method) + } + + if slices.Contains(keys, key) { + return configutil.NormalizeAddr(value), nil + } + + return value, nil +} + func parseSinks(result *Config, list *ast.ObjectList) error { name := "sink" diff --git a/command/agent/config/config_test.go b/command/agent/config/config_test.go index 6c12ebe5def3..41a37c0c37cf 100644 --- a/command/agent/config/config_test.go +++ b/command/agent/config/config_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/vault/command/agentproxyshared" "github.com/hashicorp/vault/internalshared/configutil" "github.com/hashicorp/vault/sdk/helper/pointerutil" + "github.com/stretchr/testify/require" "golang.org/x/exp/slices" ) @@ -230,6 +231,9 @@ func TestLoadConfigDir_AgentCache(t *testing.T) { t.Fatal(err) } config2, err := LoadConfigFile("./test-fixtures/config-dir-cache/config-cache2.hcl") + if err != nil { + t.Fatal(err) + } mergedConfig := config.Merge(config2) @@ -441,77 +445,117 @@ func TestLoadConfigFile_AgentCache_NoListeners(t *testing.T) { } } -func TestLoadConfigFile(t *testing.T) { - if err := os.Setenv("TEST_AAD_ENV", "aad"); err != nil { - t.Fatal(err) - } - defer func() { - if err := os.Unsetenv("TEST_AAD_ENV"); err != nil { - t.Fatal(err) - } - }() - - config, err := LoadConfigFile("./test-fixtures/config.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } +// Test_LoadConfigFile_AutoAuth_AddrConformance verifies basic config file +// loading in addition to RFC-5942 §4 normalization of auto-auth methods. +// See: https://rfc-editor.org/rfc/rfc5952.html +func Test_LoadConfigFile_AutoAuth_AddrConformance(t *testing.T) { + t.Setenv("TEST_AAD_ENV", "aad") + + for name, method := range map[string]*Method{ + "aws": { + Type: "aws", + MountPath: "auth/aws", + Namespace: "aws-namespace/", + Config: map[string]any{ + "role": "foobar", + }, + }, + "azure": { + Type: "azure", + MountPath: "auth/azure", + Namespace: "azure-namespace/", + Config: map[string]any{ + "authenticate_from_environment": true, + "role": "dev-role", + "resource": "https://[2001:0:0:1::1]", + }, + }, + "gcp": { + Type: "gcp", + MountPath: "auth/gcp", + Namespace: "gcp-namespace/", + Config: map[string]any{ + "role": "dev-role", + "service_account": "https://[2001:db8:ac3:fe4::1]", + }, + }, + } { + t.Run(name, func(t *testing.T) { + config, err := LoadConfigFile("./test-fixtures/config-auto-auth-" + name + ".hcl") + require.NoError(t, err) - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - LogFile: "/var/log/vault/vault-agent.log", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", + expected := &Config{ + SharedConfig: &configutil.SharedConfig{ + PidFile: "./pidfile", + Listeners: []*configutil.Listener{ + { + Type: "unix", + Address: "/path/to/socket", + TLSDisable: true, + AgentAPI: &configutil.AgentAPI{ + EnableQuit: true, + }, + }, + { + Type: "tcp", + Address: "2001:db8::1:8200", // Normalized + TLSDisable: true, + }, + { + Type: "tcp", + Address: "[2001:0:0:1::1]:3000", // Normalized + Role: "metrics_only", + TLSDisable: true, + }, + { + Type: "tcp", + Role: "default", + Address: "2001:db8:0:1:1:1:1:1:8400", // Normalized + TLSKeyFile: "/path/to/cakey.pem", + TLSCertFile: "/path/to/cacert.pem", + }, + }, + LogFile: "/var/log/vault/vault-agent.log", }, - MaxBackoff: 0, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", + Vault: &Vault{ + Address: "https://[2001:db8::1]:8200", // Address is normalized + Retry: &Retry{ + NumRetries: 12, // Default number of retries when a vault stanza is set }, }, - { - Type: "file", - WrapTTL: 5 * time.Minute, - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath2", - AAD: "aad", - DeriveKey: true, - Config: map[string]interface{}{ - "path": "/tmp/file-bar", + AutoAuth: &AutoAuth{ + Method: method, // Method properties are normalized correctly + Sinks: []*Sink{ + { + Type: "file", + DHType: "curve25519", + DHPath: "/tmp/file-foo-dhpath", + AAD: "foobar", + Config: map[string]interface{}{ + "path": "/tmp/file-foo", + }, + }, + { + Type: "file", + WrapTTL: 5 * time.Minute, + DHType: "curve25519", + DHPath: "/tmp/file-foo-dhpath2", + AAD: "aad", + DeriveKey: true, + Config: map[string]interface{}{ + "path": "/tmp/file-bar", + }, + }, }, }, - }, - }, - TemplateConfig: &TemplateConfig{ - MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-embedded-type.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } + TemplateConfig: &TemplateConfig{ + MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost, + }, + } - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) + config.Prune() + require.EqualValues(t, expected, config) + }) } } diff --git a/command/agent/config/test-fixtures/config-auto-auth-aws.hcl b/command/agent/config/test-fixtures/config-auto-auth-aws.hcl new file mode 100644 index 000000000000..3da04a6409e2 --- /dev/null +++ b/command/agent/config/test-fixtures/config-auto-auth-aws.hcl @@ -0,0 +1,69 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" +log_file = "/var/log/vault/vault-agent.log" + +vault { + address = "https://[2001:0db8::0001]:8200" +} + +auto_auth { + method { + type = "aws" + namespace = "/aws-namespace" + config = { + role = "foobar" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } + + sink { + type = "file" + wrap_ttl = "5m" + aad_env_var = "TEST_AAD_ENV" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath2" + derive_key = true + config = { + path = "/tmp/file-bar" + } + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + + agent_api { + enable_quit = true + } +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} diff --git a/command/agent/config/test-fixtures/config-auto-auth-azure.hcl b/command/agent/config/test-fixtures/config-auto-auth-azure.hcl new file mode 100644 index 000000000000..1174f709d386 --- /dev/null +++ b/command/agent/config/test-fixtures/config-auto-auth-azure.hcl @@ -0,0 +1,71 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" +log_file = "/var/log/vault/vault-agent.log" + +vault { + address = "https://[2001:0db8::0001]:8200" +} + +auto_auth { + method { + type = "azure" + namespace = "/azure-namespace" + config = { + authenticate_from_environment = true + role = "dev-role" + resource = "https://[2001:0:0:1:0:0:0:1]", + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } + + sink { + type = "file" + wrap_ttl = "5m" + aad_env_var = "TEST_AAD_ENV" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath2" + derive_key = true + config = { + path = "/tmp/file-bar" + } + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + + agent_api { + enable_quit = true + } +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} diff --git a/command/agent/config/test-fixtures/config-auto-auth-gcp.hcl b/command/agent/config/test-fixtures/config-auto-auth-gcp.hcl new file mode 100644 index 000000000000..3b27f7372adc --- /dev/null +++ b/command/agent/config/test-fixtures/config-auto-auth-gcp.hcl @@ -0,0 +1,70 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" +log_file = "/var/log/vault/vault-agent.log" + +vault { + address = "https://[2001:0db8::0001]:8200" +} + +auto_auth { + method { + type = "gcp" + namespace = "/gcp-namespace" + config = { + role = "dev-role" + service_account = "https://[2001:DB8:AC3:FE4::1]" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } + + sink { + type = "file" + wrap_ttl = "5m" + aad_env_var = "TEST_AAD_ENV" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath2" + derive_key = true + config = { + path = "/tmp/file-bar" + } + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + + agent_api { + enable_quit = true + } +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} diff --git a/command/agent/config/test-fixtures/config-embedded-type.hcl b/command/agent/config/test-fixtures/config-embedded-type.hcl deleted file mode 100644 index cf3c182a85f3..000000000000 --- a/command/agent/config/test-fixtures/config-embedded-type.hcl +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -log_file = "/var/log/vault/vault-agent.log" - -auto_auth { - method "aws" { - mount_path = "auth/aws" - namespace = "my-namespace" - config = { - role = "foobar" - } - } - - sink "file" { - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } - - sink "file" { - wrap_ttl = "5m" - aad_env_var = "TEST_AAD_ENV" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath2" - derive_key = true - config = { - path = "/tmp/file-bar" - } - } -} diff --git a/command/agent/config/test-fixtures/config.hcl b/command/agent/config/test-fixtures/config.hcl deleted file mode 100644 index f6ca0e684e03..000000000000 --- a/command/agent/config/test-fixtures/config.hcl +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -log_file = "/var/log/vault/vault-agent.log" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } - - sink { - type = "file" - wrap_ttl = "5m" - aad_env_var = "TEST_AAD_ENV" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath2" - derive_key = true - config = { - path = "/tmp/file-bar" - } - } -} diff --git a/command/agent_test.go b/command/agent_test.go index 17c74fc316cc..2a8a1606d28d 100644 --- a/command/agent_test.go +++ b/command/agent_test.go @@ -3569,6 +3569,108 @@ template { } } +// TestAgent_Config_Normalization verifies that the vault address is correctly +// normalized to conform to RFC-5942 §4 when configured by a config file, +// environment variables, or CLI flags. +// See: https://rfc-editor.org/rfc/rfc5952.html +func TestAgent_Config_Normalization(t *testing.T) { + for name, test := range map[string]struct { + args []string + envVars map[string]string + cfg string + expected *agentConfig.Config + }{ + "ipv4": { + cfg: ` +vault { + address = "https://127.0.0.1:8200" +} +`, + expected: &agentConfig.Config{ + Vault: &agentConfig.Vault{ + Address: "https://127.0.0.1:8200", + }, + }, + }, + "ipv6 config": { + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &agentConfig.Config{ + Vault: &agentConfig.Vault{ + // Use the normalized version in the config + Address: "https://[2001:db8::1]:8200", + }, + }, + }, + "ipv6 cli arg overrides": { + args: []string{"-address=https://[2001:0:0:1:0:0:0:1]:8200"}, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &agentConfig.Config{ + Vault: &agentConfig.Vault{ + // Use a normalized version of the args address + Address: "https://[2001:0:0:1::1]:8200", + }, + }, + }, + "ipv6 env var overrides": { + envVars: map[string]string{ + "VAULT_ADDR": "https://[2001:DB8:AC3:FE4::1]:8200", + }, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &agentConfig.Config{ + Vault: &agentConfig.Vault{ + // Use a normalized version of the env var address + Address: "https://[2001:db8:ac3:fe4::1]:8200", + }, + }, + }, + "ipv6 all uses cli overrides": { + args: []string{"-address=https://[2001:0:0:1:0:0:0:1]:8200"}, + envVars: map[string]string{ + "VAULT_ADDR": "https://[2001:DB8:AC3:FE4::1]:8200", + }, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &agentConfig.Config{ + Vault: &agentConfig.Vault{ + // Use a normalized version of the args address + Address: "https://[2001:0:0:1::1]:8200", + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + for k, v := range test.envVars { + t.Setenv(k, v) + } + configFile := populateTempFile(t, "agent-config.hcl", test.cfg) + cfg, err := agentConfig.LoadConfigFile(configFile.Name()) + require.NoError(t, err) + + cmd := &AgentCommand{BaseCommand: &BaseCommand{}} + f := cmd.Flags() + require.NoError(t, f.Parse(test.args)) + + cmd.applyConfigOverrides(f, cfg) + require.Equal(t, test.expected.Vault.Address, cfg.Vault.Address) + }) + } +} + // Get a randomly assigned port and then free it again before returning it. // There is still a race when trying to use it, but should work better // than a static port. diff --git a/command/base.go b/command/base.go index ceba362e34ba..f8c2a8a92068 100644 --- a/command/base.go +++ b/command/base.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/vault/api/tokenhelper" "github.com/hashicorp/vault/command/config" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/internalshared/configutil" "github.com/mattn/go-isatty" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" @@ -387,11 +388,12 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { f := set.NewFlagSet("HTTP Options") addrStringVar := &StringVar{ - Name: flagNameAddress, - Target: &c.flagAddress, - EnvVar: api.EnvVaultAddress, - Completion: complete.PredictAnything, - Usage: "Address of the Vault server.", + Name: flagNameAddress, + Target: &c.flagAddress, + EnvVar: api.EnvVaultAddress, + Completion: complete.PredictAnything, + Normalizers: []func(string) string{configutil.NormalizeAddr}, + Usage: "Address of the Vault server.", } if c.flagAddress != "" { @@ -403,11 +405,12 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { f.StringVar(addrStringVar) agentAddrStringVar := &StringVar{ - Name: "agent-address", - Target: &c.flagAgentProxyAddress, - EnvVar: api.EnvVaultAgentAddr, - Completion: complete.PredictAnything, - Usage: "Address of the Agent.", + Name: "agent-address", + Target: &c.flagAgentProxyAddress, + EnvVar: api.EnvVaultAgentAddr, + Completion: complete.PredictAnything, + Normalizers: []func(string) string{configutil.NormalizeAddr}, + Usage: "Address of the Agent.", } f.StringVar(agentAddrStringVar) diff --git a/command/base_flags.go b/command/base_flags.go index 3f3fc1abdad1..04b7112442b1 100644 --- a/command/base_flags.go +++ b/command/base_flags.go @@ -460,34 +460,38 @@ func (i *uint64Value) Hidden() bool { return i.hidden } // -- StringVar and stringValue type StringVar struct { - Name string - Aliases []string - Usage string - Default string - Hidden bool - EnvVar string - Target *string - Completion complete.Predictor + Name string + Aliases []string + Usage string + Default string + Hidden bool + EnvVar string + Target *string + Normalizers []func(string) string + Completion complete.Predictor } func (f *FlagSet) StringVar(i *StringVar) { - initial := i.Default + if i == nil { + return + } + + value := i.Default if v, exist := os.LookupEnv(i.EnvVar); exist { - initial = v + value = v } - def := "" - if i.Default != "" { - def = i.Default + for _, f := range i.Normalizers { + value = f(value) } f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: def, + Default: i.Default, EnvVar: i.EnvVar, - Value: newStringValue(initial, i.Target, i.Hidden), + Value: newStringValue(value, i.Target, i.Hidden), Completion: i.Completion, }) } @@ -658,39 +662,44 @@ func appendDurationSuffix(s string) string { // -- StringSliceVar and stringSliceValue type StringSliceVar struct { - Name string - Aliases []string - Usage string - Default []string - Hidden bool - EnvVar string - Target *[]string - Completion complete.Predictor -} - -func (f *FlagSet) StringSliceVar(i *StringSliceVar) { - initial := i.Default - if v, exist := os.LookupEnv(i.EnvVar); exist { + Name string + Aliases []string + Usage string + Default []string + Hidden bool + EnvVar string + Target *[]string + Completion complete.Predictor + Normalizers []func(string) string +} + +func (f *FlagSet) StringSliceVar(in *StringSliceVar) { + initial := in.Default + if v, exist := os.LookupEnv(in.EnvVar); exist { parts := strings.Split(v, ",") for i := range parts { - parts[i] = strings.TrimSpace(parts[i]) + part := strings.TrimSpace(parts[i]) + for _, f := range in.Normalizers { + part = f(part) + } + parts[i] = part } initial = parts } def := "" - if i.Default != nil { - def = strings.Join(i.Default, ",") + if in.Default != nil { + def = strings.Join(in.Default, ",") } f.VarFlag(&VarFlag{ - Name: i.Name, - Aliases: i.Aliases, - Usage: i.Usage, + Name: in.Name, + Aliases: in.Aliases, + Usage: in.Usage, Default: def, - EnvVar: i.EnvVar, - Value: newStringSliceValue(initial, i.Target, i.Hidden), - Completion: i.Completion, + EnvVar: in.EnvVar, + Value: newStringSliceValue(initial, in.Target, in.Hidden), + Completion: in.Completion, }) } diff --git a/command/base_test.go b/command/base_test.go index 2be878a09b20..8778dfa68aa7 100644 --- a/command/base_test.go +++ b/command/base_test.go @@ -24,60 +24,64 @@ func getDefaultCliHeaders(t *testing.T) http.Header { } func TestClient_FlagHeader(t *testing.T) { - defaultHeaders := getDefaultCliHeaders(t) + t.Parallel() - cases := []struct { + cases := map[string]struct { Input map[string]string Valid bool }{ - { + "empty": { map[string]string{}, true, }, - { + "valid": { map[string]string{"foo": "bar", "header2": "value2"}, true, }, - { + "invalid": { map[string]string{"X-Vault-foo": "bar", "header2": "value2"}, false, }, } - for _, tc := range cases { - expectedHeaders := defaultHeaders.Clone() - for key, val := range tc.Input { - expectedHeaders.Add(key, val) - } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + expectedHeaders := getDefaultCliHeaders(t) + for key, val := range tc.Input { + expectedHeaders.Add(key, val) + } - bc := &BaseCommand{flagHeader: tc.Input} - cli, err := bc.Client() + bc := &BaseCommand{flagHeader: tc.Input} + cli, err := bc.Client() - if err == nil && !tc.Valid { - t.Errorf("No error for input[%#v], but not valid", tc.Input) - continue - } + if err == nil && !tc.Valid { + t.Errorf("No error for input[%#v], but not valid", tc.Input) + } - if err != nil { - if tc.Valid { - t.Errorf("Error[%v] with input[%#v], but valid", err, tc.Input) + if err != nil { + if tc.Valid { + t.Errorf("Error[%v] with input[%#v], but valid", err, tc.Input) + } + return } - continue - } - if cli == nil { - t.Error("client should not be nil") - } + if cli == nil { + t.Error("client should not be nil") + } - actualHeaders := cli.Headers() - if !reflect.DeepEqual(expectedHeaders, actualHeaders) { - t.Errorf("expected [%#v] but got [%#v]", expectedHeaders, actualHeaders) - } + actualHeaders := cli.Headers() + if !reflect.DeepEqual(expectedHeaders, actualHeaders) { + t.Errorf("expected [%#v] but got [%#v]", expectedHeaders, actualHeaders) + } + }) } } // TestClient_HCPConfiguration tests that the HCP configuration is applied correctly when it exists in cache. func TestClient_HCPConfiguration(t *testing.T) { + t.Parallel() + cases := map[string]struct { Valid bool ExpectedAddr string @@ -94,7 +98,8 @@ func TestClient_HCPConfiguration(t *testing.T) { for n, tst := range cases { t.Run(n, func(t *testing.T) { - bc := &BaseCommand{hcpTokenHelper: &hcpvlib.TestingHCPTokenHelper{tst.Valid}} + t.Parallel() + bc := &BaseCommand{hcpTokenHelper: &hcpvlib.TestingHCPTokenHelper{ValidCache: tst.Valid}} cli, err := bc.Client() assert.NoError(t, err) @@ -109,3 +114,78 @@ func TestClient_HCPConfiguration(t *testing.T) { }) } } + +func Test_FlagSet_StringVar_Normalizers(t *testing.T) { + appendA := func(in string) string { return in + "a" } + prependB := func(in string) string { return "b" + in } + + for name, test := range map[string]struct { + in func() *StringVar + envVars map[string]string + expectedValue string + }{ + "no normalizers no env vars uses default value": { + in: func() *StringVar { + resT := "" + return &StringVar{ + Name: "test", + Target: &resT, + EnvVar: "VAULT_TEST", + Default: "default", + } + }, + expectedValue: "default", + }, + "one normalizer no env vars normalizes default value": { + in: func() *StringVar { + resT := "" + return &StringVar{ + Name: "test", + Target: &resT, + EnvVar: "VAULT_TEST", + Default: "default", + Normalizers: []func(string) string{appendA}, + } + }, + expectedValue: "defaulta", + }, + "two normalizers no env vars normalizes default value with both": { + in: func() *StringVar { + resT := "" + return &StringVar{ + Name: "test", + Target: &resT, + EnvVar: "VAULT_TEST", + Default: "default", + Normalizers: []func(string) string{appendA, prependB}, + } + }, + expectedValue: "bdefaulta", + }, + "two normalizers with env vars normalizes env var value with both": { + in: func() *StringVar { + resT := "" + return &StringVar{ + Name: "test", + Target: &resT, + EnvVar: "VAULT_TEST", + Default: "default", + Normalizers: []func(string) string{appendA, prependB}, + } + }, + envVars: map[string]string{"VAULT_TEST": "env_override"}, + expectedValue: "benv_overridea", + }, + } { + t.Run(name, func(t *testing.T) { + for k, v := range test.envVars { + t.Setenv(k, v) + } + fsets := NewFlagSets(nil) + fs := fsets.NewFlagSet("test") + sv := test.in() + fs.StringVar(sv) + require.Equal(t, test.expectedValue, *sv.Target) + }) + } +} diff --git a/command/proxy.go b/command/proxy.go index 05a52398224f..979ae43b2d43 100644 --- a/command/proxy.go +++ b/command/proxy.go @@ -865,10 +865,11 @@ func (c *ProxyCommand) applyConfigOverrides(f *FlagSets, config *proxyConfig.Con }) c.setStringFlag(f, config.Vault.Address, &StringVar{ - Name: flagNameAddress, - Target: &c.flagAddress, - Default: "https://127.0.0.1:8200", - EnvVar: api.EnvVaultAddress, + Name: flagNameAddress, + Target: &c.flagAddress, + Default: "https://127.0.0.1:8200", + EnvVar: api.EnvVaultAddress, + Normalizers: []func(string) string{configutil.NormalizeAddr}, }) config.Vault.Address = c.flagAddress c.setStringFlag(f, config.Vault.CACert, &StringVar{ diff --git a/command/proxy/config/config.go b/command/proxy/config/config.go index e0a9080cc38e..a8e7a56941c6 100644 --- a/command/proxy/config/config.go +++ b/command/proxy/config/config.go @@ -11,6 +11,7 @@ import ( "net" "os" "path/filepath" + "slices" "strings" "time" @@ -517,6 +518,10 @@ func parseVault(result *Config, list *ast.ObjectList) error { return err } + if v.Address != "" { + v.Address = configutil.NormalizeAddr(v.Address) + } + if v.TLSSkipVerifyRaw != nil { v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw) if err != nil { @@ -793,10 +798,63 @@ func parseMethod(result *Config, list *ast.ObjectList) error { // Canonicalize namespace path if provided m.Namespace = namespace.Canonicalize(m.Namespace) + // Normalize any configuration addresses + if len(m.Config) > 0 { + var err error + for k, v := range m.Config { + vStr, ok := v.(string) + if !ok { + continue + } + m.Config[k], err = normalizeAutoAuthMethod(m.Type, k, vStr) + if err != nil { + return err + } + } + } + result.AutoAuth.Method = &m return nil } +// autoAuthMethodKeys maps an auto-auth method type to its associated +// configuration whose values are URLs, IP addresses, or host:port style +// addresses. All auto-auth types must have an entry in this map, otherwise our +// normalization check will fail when parsing the storage entry config. +// Auto-auth method types which don't contain such keys should include an empty +// array. +var autoAuthMethodKeys = map[string][]string{ + "alicloud": {""}, + "approle": {""}, + "aws": {""}, + "azure": {"resource"}, + "cert": {""}, + "cf": {""}, + "gcp": {"service_account"}, + "jwt": {""}, + "ldap": {""}, + "kerberos": {""}, + "kubernetes": {""}, + "oci": {""}, + "token_file": {""}, +} + +// normalizeAutoAuthMethod takes a storage name, a configuration key +// and it's associated value and will normalize any URLs, IP addresses, or +// host:port style addresses. +func normalizeAutoAuthMethod(method string, key string, value string) (string, error) { + keys, ok := autoAuthMethodKeys[method] + if !ok { + return "", fmt.Errorf("unknown auto-auth method type %s", method) + } + + if slices.Contains(keys, key) { + return configutil.NormalizeAddr(value), nil + } + + return value, nil +} + func parseSinks(result *Config, list *ast.ObjectList) error { name := "sink" diff --git a/command/proxy/config/config_test.go b/command/proxy/config/config_test.go index e0afc50de54a..84b4c136e940 100644 --- a/command/proxy/config/config_test.go +++ b/command/proxy/config/config_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/vault/command/agentproxyshared" "github.com/hashicorp/vault/internalshared/configutil" + "github.com/stretchr/testify/require" ) // TestLoadConfigFile_ProxyCache tests loading a config file containing a cache @@ -203,3 +204,126 @@ func TestLoadConfigFile_ProxyCacheStaticSecrets(t *testing.T) { t.Fatal(diff) } } + +// Test_LoadConfigFile_AutoAuth_AddrConformance verifies basic config file +// loading in addition to RFC-5942 §4 normalization of auto-auth methods. +// See: https://rfc-editor.org/rfc/rfc5952.html +func Test_LoadConfigFile_AutoAuth_AddrConformance(t *testing.T) { + t.Parallel() + + for name, method := range map[string]*Method{ + "aws": { + Type: "aws", + MountPath: "auth/aws", + Namespace: "aws-namespace/", + Config: map[string]any{ + "role": "foobar", + }, + }, + "azure": { + Type: "azure", + MountPath: "auth/azure", + Namespace: "azure-namespace/", + Config: map[string]any{ + "authenticate_from_environment": true, + "role": "dev-role", + "resource": "https://[2001:0:0:1::1]", + }, + }, + "gcp": { + Type: "gcp", + MountPath: "auth/gcp", + Namespace: "gcp-namespace/", + Config: map[string]any{ + "role": "dev-role", + "service_account": "https://[2001:db8:ac3:fe4::1]", + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + config, err := LoadConfigFile("./test-fixtures/config-auto-auth-" + name + ".hcl") + require.NoError(t, err) + + expected := &Config{ + SharedConfig: &configutil.SharedConfig{ + PidFile: "./pidfile", + Listeners: []*configutil.Listener{ + { + Type: "unix", + Address: "/path/to/socket", + TLSDisable: true, + SocketMode: "configmode", + SocketUser: "configuser", + SocketGroup: "configgroup", + }, + { + Type: "tcp", + Address: "2001:db8::1:8200", // Normalized + TLSDisable: true, + }, + { + Type: "tcp", + Address: "[2001:0:0:1::1]:3000", // Normalized + Role: "metrics_only", + TLSDisable: true, + }, + { + Type: "tcp", + Role: "default", + Address: "2001:db8:0:1:1:1:1:1:8400", // Normalized + TLSKeyFile: "/path/to/cakey.pem", + TLSCertFile: "/path/to/cacert.pem", + }, + }, + }, + Vault: &Vault{ + Address: "https://[2001:db8::1]:8200", // Normalized + CACert: "config_ca_cert", + CAPath: "config_ca_path", + TLSSkipVerifyRaw: interface{}("true"), + TLSSkipVerify: true, + ClientCert: "config_client_cert", + ClientKey: "config_client_key", + Retry: &Retry{ + NumRetries: 12, + }, + }, + AutoAuth: &AutoAuth{ + Method: method, // Method properties are normalized correctly + Sinks: []*Sink{ + { + Type: "file", + DHType: "curve25519", + DHPath: "/tmp/file-foo-dhpath", + AAD: "foobar", + Config: map[string]interface{}{ + "path": "/tmp/file-foo", + }, + }, + }, + }, + APIProxy: &APIProxy{ + EnforceConsistency: "always", + WhenInconsistent: "retry", + UseAutoAuthTokenRaw: true, + UseAutoAuthToken: true, + ForceAutoAuthToken: false, + }, + Cache: &Cache{ + Persist: &agentproxyshared.PersistConfig{ + Type: "kubernetes", + Path: "/vault/agent-cache/", + KeepAfterImport: true, + ExitOnErr: true, + ServiceAccountTokenFile: "/tmp/serviceaccount/token", + }, + }, + } + + config.Prune() + require.EqualValues(t, expected, config) + }) + } +} diff --git a/command/proxy/config/test-fixtures/config-auto-auth-aws.hcl b/command/proxy/config/test-fixtures/config-auto-auth-aws.hcl new file mode 100644 index 000000000000..773f709f12aa --- /dev/null +++ b/command/proxy/config/test-fixtures/config-auto-auth-aws.hcl @@ -0,0 +1,76 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + namespace = "/aws-namespace" + config = { + role = "foobar" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "retry" +} + +cache { + persist = { + type = "kubernetes" + path = "/vault/agent-cache/" + keep_after_import = true + exit_on_err = true + service_account_token_file = "/tmp/serviceaccount/token" + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + socket_mode = "configmode" + socket_user = "configuser" + socket_group = "configgroup" +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} + +vault { + address = "https://[2001:0db8::0001]:8200" + ca_cert = "config_ca_cert" + ca_path = "config_ca_path" + tls_skip_verify = "true" + client_cert = "config_client_cert" + client_key = "config_client_key" +} diff --git a/command/proxy/config/test-fixtures/config-auto-auth-azure.hcl b/command/proxy/config/test-fixtures/config-auto-auth-azure.hcl new file mode 100644 index 000000000000..3f942a37f41a --- /dev/null +++ b/command/proxy/config/test-fixtures/config-auto-auth-azure.hcl @@ -0,0 +1,78 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" + +auto_auth { + method { + type = "azure" + namespace = "/azure-namespace" + config = { + authenticate_from_environment = true + role = "dev-role" + resource = "https://[2001:0:0:1:0:0:0:1]", + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "retry" +} + +cache { + persist = { + type = "kubernetes" + path = "/vault/agent-cache/" + keep_after_import = true + exit_on_err = true + service_account_token_file = "/tmp/serviceaccount/token" + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + socket_mode = "configmode" + socket_user = "configuser" + socket_group = "configgroup" +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} + +vault { + address = "https://[2001:0db8::0001]:8200" + ca_cert = "config_ca_cert" + ca_path = "config_ca_path" + tls_skip_verify = "true" + client_cert = "config_client_cert" + client_key = "config_client_key" +} diff --git a/command/proxy/config/test-fixtures/config-auto-auth-gcp.hcl b/command/proxy/config/test-fixtures/config-auto-auth-gcp.hcl new file mode 100644 index 000000000000..a3fc6b701e41 --- /dev/null +++ b/command/proxy/config/test-fixtures/config-auto-auth-gcp.hcl @@ -0,0 +1,77 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +pid_file = "./pidfile" + +auto_auth { + method { + type = "gcp" + namespace = "/gcp-namespace" + config = { + role = "dev-role" + service_account = "https://[2001:DB8:AC3:FE4::1]" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "retry" +} + +cache { + persist = { + type = "kubernetes" + path = "/vault/agent-cache/" + keep_after_import = true + exit_on_err = true + service_account_token_file = "/tmp/serviceaccount/token" + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + socket_mode = "configmode" + socket_user = "configuser" + socket_group = "configgroup" +} + +listener "tcp" { + address = "2001:0db8::0001:8200" + tls_disable = true +} + +listener { + type = "tcp" + address = "[2001:0:0:1:0:0:0:1]:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "2001:db8:0:1:1:1:1:1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} + +vault { + address = "https://[2001:0db8::0001]:8200" + ca_cert = "config_ca_cert" + ca_path = "config_ca_path" + tls_skip_verify = "true" + client_cert = "config_client_cert" + client_key = "config_client_key" +} diff --git a/command/proxy_test.go b/command/proxy_test.go index 3c2fdd001618..3d1585638866 100644 --- a/command/proxy_test.go +++ b/command/proxy_test.go @@ -2196,3 +2196,105 @@ func TestProxy_Config_ReloadTls(t *testing.T) { t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) } } + +// TestProxy_Config_Normalization verifies that the vault address is correctly +// normalized to conform to RFC-5942 §4 when configured by a config file, +// environment variables, or CLI flags. +// See: https://rfc-editor.org/rfc/rfc5952.html +func TestProxy_Config_Normalization(t *testing.T) { + for name, test := range map[string]struct { + args []string + envVars map[string]string + cfg string + expected *proxyConfig.Config + }{ + "ipv4": { + cfg: ` +vault { + address = "https://127.0.0.1:8200" +} +`, + expected: &proxyConfig.Config{ + Vault: &proxyConfig.Vault{ + Address: "https://127.0.0.1:8200", + }, + }, + }, + "ipv6 config": { + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &proxyConfig.Config{ + Vault: &proxyConfig.Vault{ + // Use the normalized version in the config + Address: "https://[2001:db8::1]:8200", + }, + }, + }, + "ipv6 cli arg overrides": { + args: []string{"-address=https://[2001:0:0:1:0:0:0:1]:8200"}, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &proxyConfig.Config{ + Vault: &proxyConfig.Vault{ + // Use a normalized version of the args address + Address: "https://[2001:0:0:1::1]:8200", + }, + }, + }, + "ipv6 env var overrides": { + envVars: map[string]string{ + "VAULT_ADDR": "https://[2001:DB8:AC3:FE4::1]:8200", + }, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &proxyConfig.Config{ + Vault: &proxyConfig.Vault{ + // Use a normalized version of the env var address + Address: "https://[2001:db8:ac3:fe4::1]:8200", + }, + }, + }, + "ipv6 all uses cli overrides": { + args: []string{"-address=https://[2001:0:0:1:0:0:0:1]:8200"}, + envVars: map[string]string{ + "VAULT_ADDR": "https://[2001:DB8:AC3:FE4::1]:8200", + }, + cfg: ` +vault { + address = "https://[2001:0db8::0001]:8200" +} +`, + expected: &proxyConfig.Config{ + Vault: &proxyConfig.Vault{ + // Use a normalized version of the args address + Address: "https://[2001:0:0:1::1]:8200", + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + for k, v := range test.envVars { + t.Setenv(k, v) + } + configFile := populateTempFile(t, "proxy-config.hcl", test.cfg) + cfg, err := proxyConfig.LoadConfigFile(configFile.Name()) + require.NoError(t, err) + + cmd := &ProxyCommand{BaseCommand: &BaseCommand{}} + f := cmd.Flags() + require.NoError(t, f.Parse(test.args)) + + cmd.applyConfigOverrides(f, cfg) + require.Equal(t, test.expected.Vault.Address, cfg.Vault.Address) + }) + } +} diff --git a/command/server.go b/command/server.go index bff21f98fb78..1d23499e96ad 100644 --- a/command/server.go +++ b/command/server.go @@ -266,6 +266,7 @@ func (c *ServerCommand) Flags() *FlagSets { "localhost.localdomain, and the host name as alternate DNS names, " + "and 127.0.0.1 as an alternate IP address. This flag can be specified " + "multiple times to specify multiple SANs.", + Normalizers: []func(string) string{configutil.NormalizeAddr}, }) f.StringVar(&StringVar{ @@ -278,11 +279,12 @@ func (c *ServerCommand) Flags() *FlagSets { }) f.StringVar(&StringVar{ - Name: "dev-listen-address", - Target: &c.flagDevListenAddr, - Default: "127.0.0.1:8200", - EnvVar: "VAULT_DEV_LISTEN_ADDRESS", - Usage: "Address to bind to in \"dev\" mode.", + Name: "dev-listen-address", + Target: &c.flagDevListenAddr, + Default: "127.0.0.1:8200", + EnvVar: "VAULT_DEV_LISTEN_ADDRESS", + Usage: "Address to bind to in \"dev\" mode.", + Normalizers: []func(string) string{configutil.NormalizeAddr}, }) f.BoolVar(&BoolVar{ Name: "dev-no-store-token", @@ -798,7 +800,7 @@ func (c *ServerCommand) setupStorage(config *server.Config) (physical.Backend, e } case storageTypeRaft: if envCA := os.Getenv("VAULT_CLUSTER_ADDR"); envCA != "" { - config.ClusterAddr = envCA + config.ClusterAddr = configutil.NormalizeAddr(envCA) } if len(config.ClusterAddr) == 0 { return nil, errors.New("Cluster address must be set when using raft storage") diff --git a/internalshared/configutil/normalize.go b/internalshared/configutil/normalize.go index 902699e57bfd..205551dd6afc 100644 --- a/internalshared/configutil/normalize.go +++ b/internalshared/configutil/normalize.go @@ -33,19 +33,16 @@ func NormalizeAddr(address string) string { // We haven't been given a URL. Try and parse it as an IP address ip = net.ParseIP(address) if ip == nil { - // We haven't been given a URL or IP address, try parsing an IP:Port - // combination. - idx := strings.LastIndex(address, ":") - if idx > 0 { - // We've perhaps received an IP:Port address - addr := address[:idx] - port = address[idx+1:] - if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") { - addr = strings.TrimPrefix(strings.TrimSuffix(addr, "]"), "[") - bracketedIPv6 = true - } - ip = net.ParseIP(addr) + // We can't parse it as a strict IP address. It could be some form of + // destination address (user@addr) and/or an IP:Port combo. + + // Try destination address. + if idx := strings.Index(address, "@"); idx > 0 { + return address[:idx+1] + NormalizeAddr(address[idx+1:]) } + + // Try parsing an IP:Port combination. + ip, port, bracketedIPv6 = ipPort(address) } } @@ -82,6 +79,9 @@ func NormalizeAddr(address string) string { } // Handle just an IP address + if bracketedIPv6 { + return fmt.Sprintf("[%s]", v6.String()) + } return v6.String() } @@ -89,3 +89,30 @@ func NormalizeAddr(address string) string { // to, return the string unchanged. return address } + +func ipPort(address string) (net.IP, string, bool) { + // Check for a bracked IPv6 string with no port + if isBracketedString(address) { + return net.ParseIP(address[1 : len(address)-1]), "", true + } + + // Check for a IP:Port that is not bracketed + idx := strings.LastIndex(address, ":") + if idx < 0 { + return net.ParseIP(address), "", false + } + + // Extract the address and port + addr := address[:idx] + port := address[idx+1:] + if isBracketedString(addr) { + ip, _, _ := ipPort(addr) + return ip, port, true + } + + return net.ParseIP(addr), port, false +} + +func isBracketedString(in string) bool { + return strings.HasPrefix(in, "[") && strings.HasSuffix(in, "]") +} diff --git a/internalshared/configutil/normalize_test.go b/internalshared/configutil/normalize_test.go index d1aec31e621b..caa5ff75160c 100644 --- a/internalshared/configutil/normalize_test.go +++ b/internalshared/configutil/normalize_test.go @@ -21,9 +21,33 @@ func TestNormalizeAddr(t *testing.T) { isErrorExpected bool }{ "hostname": { + addr: "vaultproject.io", + expected: "vaultproject.io", + }, + "hostname port": { + addr: "vaultproject.io:8200", + expected: "vaultproject.io:8200", + }, + "hostname URL": { + addr: "https://vaultproject.io", + expected: "https://vaultproject.io", + }, + "hostname port URL": { addr: "https://vaultproject.io:8200", expected: "https://vaultproject.io:8200", }, + "hostname destination address": { + addr: "user@vaultproject.io", + expected: "user@vaultproject.io", + }, + "hostname destination address URL": { + addr: "http://user@vaultproject.io", + expected: "http://user@vaultproject.io", + }, + "hostname destination address URL port": { + addr: "http://user@vaultproject.io:8200", + expected: "http://user@vaultproject.io:8200", + }, "ipv4": { addr: "10.10.1.10", expected: "10.10.1.10", @@ -36,6 +60,30 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://10.10.1.10:8200", expected: "https://10.10.1.10:8200", }, + "ipv4 destination address": { + addr: "username@10.10.1.10", + expected: "username@10.10.1.10", + }, + "ipv4 destination address port": { + addr: "username@10.10.1.10:8200", + expected: "username@10.10.1.10:8200", + }, + "ipv4 destination address URL": { + addr: "https://username@10.10.1.10", + expected: "https://username@10.10.1.10", + }, + "ipv4 destination address URL port": { + addr: "https://username@10.10.1.10:8200", + expected: "https://username@10.10.1.10:8200", + }, + "ipv6 addr no brackets": { + addr: "2001:0db8::0001", + expected: "2001:db8::1", + }, + "ipv6 addr with brackets": { + addr: "[2001:0db8::0001]", + expected: "[2001:db8::1]", + }, "ipv6 IP:Port addr no brackets": { addr: "2001:0db8::0001:8500", expected: "2001:db8::1:8500", @@ -44,6 +92,14 @@ func TestNormalizeAddr(t *testing.T) { addr: "[2001:0db8::0001]:8500", expected: "[2001:db8::1]:8500", }, + "ipv6 destination address no brackets": { + addr: "username@2001:0db8::0001:8500", + expected: "username@2001:db8::1:8500", + }, + "ipv6 destination address with brackets": { + addr: "username@[2001:0db8::0001]:8500", + expected: "username@[2001:db8::1]:8500", + }, "ipv6 RFC-5952 4.1 conformance leading zeroes": { addr: "2001:0db8::0001", expected: "2001:db8::1", @@ -52,6 +108,10 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://[2001:0db8::0001]:8200", expected: "https://[2001:db8::1]:8200", }, + "ipv6 destination address RFC-5952 4.1 conformance leading zeroes": { + addr: "username@[2001:0db8::0001]:8200", + expected: "username@[2001:db8::1]:8200", + }, "ipv6 RFC-5952 4.2.2 conformance one 16-bit 0 field": { addr: "2001:db8:0:1:1:1:1:1", expected: "2001:db8:0:1:1:1:1:1", @@ -60,6 +120,10 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://[2001:db8:0:1:1:1:1:1]:8200", expected: "https://[2001:db8:0:1:1:1:1:1]:8200", }, + "ipv6 destination address RFC-5952 4.2.2 conformance one 16-bit 0 field": { + addr: "username@2001:db8:0:1:1:1:1:1:8200", + expected: "username@2001:db8:0:1:1:1:1:1:8200", + }, "ipv6 RFC-5952 4.2.3 conformance longest run of 0 bits shortened": { addr: "2001:0:0:1:0:0:0:1", expected: "2001:0:0:1::1", @@ -68,6 +132,10 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://[2001:0:0:1:0:0:0:1]:8200", expected: "https://[2001:0:0:1::1]:8200", }, + "ipv6 destination address RFC-5952 4.2.3 conformance longest run of 0 bits shortened": { + addr: "username@[2001:0:0:1:0:0:0:1]", + expected: "username@[2001:0:0:1::1]", + }, "ipv6 RFC-5952 4.2.3 conformance equal runs of 0 bits shortened": { addr: "2001:db8:0:0:1:0:0:1", expected: "2001:db8::1:0:0:1", @@ -76,6 +144,10 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://[2001:db8:0:0:1:0:0:1]:8200", expected: "https://[2001:db8::1:0:0:1]:8200", }, + "ipv6 destination address RFC-5952 4.2.3 conformance equal runs of 0 bits shortened": { + addr: "username@2001:db8:0:0:1:0:0:1", + expected: "username@2001:db8::1:0:0:1", + }, "ipv6 RFC-5952 4.3 conformance downcase hex letters": { addr: "2001:DB8:AC3:FE4::1", expected: "2001:db8:ac3:fe4::1", @@ -84,6 +156,10 @@ func TestNormalizeAddr(t *testing.T) { addr: "https://[2001:DB8:AC3:FE4::1]:8200", expected: "https://[2001:db8:ac3:fe4::1]:8200", }, + "ipv6 destination address RFC-5952 4.3 conformance downcase hex letters": { + addr: "username@[2001:DB8:AC3:FE4::1]:8200", + expected: "username@[2001:db8:ac3:fe4::1]:8200", + }, } for name, tc := range tests { name := name