diff --git a/api/client.go b/api/client.go index 1735657936..0090321caa 100644 --- a/api/client.go +++ b/api/client.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "net" "net/http" @@ -41,6 +42,7 @@ const ( EnvVaultClientCert = "VAULT_CLIENT_CERT" EnvVaultClientKey = "VAULT_CLIENT_KEY" EnvVaultClientTimeout = "VAULT_CLIENT_TIMEOUT" + EnvVaultHeaders = "VAULT_HEADERS" EnvVaultSRVLookup = "VAULT_SRV_LOOKUP" EnvVaultSkipVerify = "VAULT_SKIP_VERIFY" EnvVaultNamespace = "VAULT_NAMESPACE" @@ -665,6 +667,30 @@ func NewClient(c *Config) (*Client, error) { client.setNamespace(namespace) } + if envHeaders := os.Getenv(EnvVaultHeaders); envHeaders != "" { + var result map[string]any + err := json.Unmarshal([]byte(envHeaders), &result) + if err != nil { + return nil, fmt.Errorf("could not unmarshal environment-supplied headers") + } + var forbiddenHeaders []string + for key, value := range result { + if strings.HasPrefix(key, "X-Vault-") { + forbiddenHeaders = append(forbiddenHeaders, key) + continue + } + + value, ok := value.(string) + if !ok { + return nil, fmt.Errorf("environment-supplied headers include non-string values") + } + client.AddHeader(key, value) + } + if len(forbiddenHeaders) > 0 { + return nil, fmt.Errorf("failed to setup Headers[%s]: Header starting by 'X-Vault-' are for internal usage only", strings.Join(forbiddenHeaders, ", ")) + } + } + return client, nil } diff --git a/api/client_test.go b/api/client_test.go index 3d75aabd6b..1ed4dfd3d3 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -374,6 +374,61 @@ func TestDefaulRetryPolicy(t *testing.T) { } } +func TestClientEnvHeaders(t *testing.T) { + oldHeaders := os.Getenv(EnvVaultHeaders) + + defer func() { + os.Setenv(EnvVaultHeaders, oldHeaders) + }() + + cases := []struct { + Input string + Valid bool + }{ + { + "{}", + true, + }, + { + "{\"foo\": \"bar\"}", + true, + }, + { + "{\"foo\": 1}", // Values must be strings + false, + }, + { + "{\"X-Vault-Foo\": \"bar\"}", // X-Vault-* not allowed + false, + }, + } + + for _, tc := range cases { + os.Setenv(EnvVaultHeaders, tc.Input) + config := DefaultConfig() + config.ReadEnvironment() + _, err := NewClient(config) + if err != nil { + if tc.Valid { + t.Fatalf("unexpected error reading headers from environment: %v", err) + } + } else { + if !tc.Valid { + t.Fatal("no error reading headers from environment when error was expected") + } + } + } + + os.Setenv(EnvVaultHeaders, "{\"foo\": \"bar\"}") + config := DefaultConfig() + config.ReadEnvironment() + cli, _ := NewClient(config) + + if !reflect.DeepEqual(cli.Headers().Values("foo"), []string{"bar"}) { + t.Error("Environment-supplied headers not set in CLI client") + } +} + func TestClientEnvSettings(t *testing.T) { cwd, _ := os.Getwd() diff --git a/changelog/21993.txt b/changelog/21993.txt new file mode 100644 index 0000000000..856cfc9662 --- /dev/null +++ b/changelog/21993.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Allow vault CLI HTTP headers to be specified using the JSON-encoded VAULT_HEADERS environment variable +``` \ No newline at end of file diff --git a/website/content/docs/commands/index.mdx b/website/content/docs/commands/index.mdx index 7632dd452c..15109f9c8f 100644 --- a/website/content/docs/commands/index.mdx +++ b/website/content/docs/commands/index.mdx @@ -439,6 +439,12 @@ Prevents the Vault client from following redirects. By default, the Vault client ~> **Note:** Disabling redirect following behavior could cause issues with commands such as 'vault operator raft snapshot' as this command redirects the request to the cluster's primary node. +### `VAULT_HEADERS` + +JSON-encoded headers to include in Vault HTTP requests performed by the CLI. For example: `{"FOO": "BAR"}`. + +Like the `-header` CLI parameter, headers starting with `X-Vault-` are forbidden. + ## Flags There are different CLI flags that are available depending on subcommands. Some