From 25792df5a9685bace9813fca730df6adc291dbc7 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 21 Mar 2018 19:56:47 -0400 Subject: [PATCH] Passthrough request headers (#4172) * Add passthrough request headers for secret/auth mounts * Update comments * Fix SyncCache deletion of passthrough_request_headers * Remove debug line * Case-insensitive header comparison * Remove unnecessary allocation * Short-circuit filteredPassthroughHeaders if there's nothing to filter * Add whitelistedHeaders list * Update router logic after merge * Add whitelist test * Add lowercase x-vault-kv-client to whitelist * Add back const * Refactor whitelist logic --- api/sys_auth.go | 26 ++++---- api/sys_mounts.go | 34 +++++----- command/auth_enable.go | 34 +++++++--- command/commands.go | 2 + command/secrets_enable.go | 36 ++++++---- http/sys_mount_test.go | 78 +++++++++++++++++++++- vault/core_test.go | 85 ++++++++++++++++++++++++ vault/logical_system.go | 56 ++++++++++++++++ vault/mount.go | 45 ++++++++----- vault/router.go | 64 ++++++++++++++++-- website/source/api/system/auth.html.md | 12 ++++ website/source/api/system/mounts.html.md | 12 ++++ 12 files changed, 409 insertions(+), 75 deletions(-) diff --git a/api/sys_auth.go b/api/sys_auth.go index 6521b9ee0..6be90989d 100644 --- a/api/sys_auth.go +++ b/api/sys_auth.go @@ -92,12 +92,13 @@ type EnableAuthOptions struct { } type AuthConfigInput struct { - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } type AuthMount struct { @@ -111,10 +112,11 @@ type AuthMount struct { } type AuthConfigOutput struct { - DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } diff --git a/api/sys_mounts.go b/api/sys_mounts.go index bcb9637a6..485372ca6 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -130,14 +130,15 @@ type MountInput struct { } type MountConfigInput struct { - Options map[string]string `json:"options" structs:"options" mapstructure:"options"` - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + Options map[string]string `json:"options" structs:"options" mapstructure:"options"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } type MountOutput struct { @@ -151,12 +152,13 @@ type MountOutput struct { } type MountConfigOutput struct { - Options map[string]string `json:"options" structs:"options" mapstructure:"options"` - DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + Options map[string]string `json:"options" structs:"options" mapstructure:"options"` + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } diff --git a/command/auth_enable.go b/command/auth_enable.go index 48235933b..07e8465d3 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -17,17 +17,18 @@ var _ cli.CommandAutocomplete = (*AuthEnableCommand)(nil) type AuthEnableCommand struct { *BaseCommand - flagDescription string - flagPath string - flagDefaultLeaseTTL time.Duration - flagMaxLeaseTTL time.Duration - flagAuditNonHMACRequestKeys []string - flagAuditNonHMACResponseKeys []string - flagListingVisibility string - flagPluginName string - flagOptions map[string]string - flagLocal bool - flagSealWrap bool + flagDescription string + flagPath string + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration + flagAuditNonHMACRequestKeys []string + flagAuditNonHMACResponseKeys []string + flagListingVisibility string + flagPassthroughRequestHeaders []string + flagPluginName string + flagOptions map[string]string + flagLocal bool + flagSealWrap bool } func (c *AuthEnableCommand) Synopsis() string { @@ -121,6 +122,13 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Usage: "Determines the visibility of the mount in the UI-specific listing endpoint.", }) + f.StringSliceVar(&StringSliceVar{ + Name: flagNamePassthroughRequestHeaders, + Target: &c.flagPassthroughRequestHeaders, + Usage: "Comma-separated string or list of request header values that " + + "will be sent to the backend", + }) + f.StringVar(&StringVar{ Name: "plugin-name", Target: &c.flagPluginName, @@ -229,6 +237,10 @@ func (c *AuthEnableCommand) Run(args []string) int { if fl.Name == flagNameListingVisibility { authOpts.Config.ListingVisibility = c.flagListingVisibility } + + if fl.Name == flagNamePassthroughRequestHeaders { + authOpts.Config.PassthroughRequestHeaders = c.flagPassthroughRequestHeaders + } }) if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil { diff --git a/command/commands.go b/command/commands.go index 3e3daabfe..381b7c1bb 100644 --- a/command/commands.go +++ b/command/commands.go @@ -79,6 +79,8 @@ const ( flagNameAuditNonHMACResponseKeys = "audit-non-hmac-response-keys" // flagListingVisibility is the flag to toggle whether to show the mount in the UI-specific listing endpoint flagNameListingVisibility = "listing-visibility" + // flagNamePassthroughRequestHeaders is the flag name used to set passthrough request headers to the backend + flagNamePassthroughRequestHeaders = "passthrough-request-headers" ) var ( diff --git a/command/secrets_enable.go b/command/secrets_enable.go index 95478380e..2eca1d761 100644 --- a/command/secrets_enable.go +++ b/command/secrets_enable.go @@ -17,18 +17,19 @@ var _ cli.CommandAutocomplete = (*SecretsEnableCommand)(nil) type SecretsEnableCommand struct { *BaseCommand - flagDescription string - flagPath string - flagDefaultLeaseTTL time.Duration - flagMaxLeaseTTL time.Duration - flagAuditNonHMACRequestKeys []string - flagAuditNonHMACResponseKeys []string - flagListingVisibility string - flagForceNoCache bool - flagPluginName string - flagOptions map[string]string - flagLocal bool - flagSealWrap bool + flagDescription string + flagPath string + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration + flagAuditNonHMACRequestKeys []string + flagAuditNonHMACResponseKeys []string + flagListingVisibility string + flagPassthroughRequestHeaders []string + flagForceNoCache bool + flagPluginName string + flagOptions map[string]string + flagLocal bool + flagSealWrap bool } func (c *SecretsEnableCommand) Synopsis() string { @@ -129,6 +130,13 @@ func (c *SecretsEnableCommand) Flags() *FlagSets { Usage: "Determines the visibility of the mount in the UI-specific listing endpoint.", }) + f.StringSliceVar(&StringSliceVar{ + Name: flagNamePassthroughRequestHeaders, + Target: &c.flagPassthroughRequestHeaders, + Usage: "Comma-separated string or list of request header values that " + + "will be sent to the backend", + }) + f.BoolVar(&BoolVar{ Name: "force-no-cache", Target: &c.flagForceNoCache, @@ -249,6 +257,10 @@ func (c *SecretsEnableCommand) Run(args []string) int { if fl.Name == flagNameListingVisibility { mountInput.Config.ListingVisibility = c.flagListingVisibility } + + if fl.Name == flagNamePassthroughRequestHeaders { + mountInput.Config.PassthroughRequestHeaders = c.flagPassthroughRequestHeaders + } }) if err := client.Sys().Mount(mountPath, mountInput); err != nil { diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index caa636707..ca952a7bd 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -1287,6 +1287,7 @@ func TestSysTuneMount_nonHMACKeys(t *testing.T) { resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ "audit_non_hmac_response_keys": "", }) + testResponseStatus(t, resp, 204) // Check results resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") @@ -1318,7 +1319,7 @@ func TestSysTuneMount_nonHMACKeys(t *testing.T) { } } -func TestSysTuneMount_showUIMount(t *testing.T) { +func TestSysTuneMount_listingVisibility(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) defer ln.Close() @@ -1390,3 +1391,78 @@ func TestSysTuneMount_showUIMount(t *testing.T) { t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) } } + +func TestSysTuneMount_passthroughRequestHeaders(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + TestServerAuth(t, addr, token) + + // Mount-tune the audit_non_hmac_request_keys + resp := testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "passthrough_request_headers": "X-Vault-Foo", + }) + testResponseStatus(t, resp, 204) + + // Check results + resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") + testResponseStatus(t, resp, 200) + + actual := map[string]interface{}{} + expected := map[string]interface{}{ + "lease_id": "", + "renewable": false, + "lease_duration": json.Number("0"), + "wrap_info": nil, + "warnings": nil, + "auth": nil, + "data": map[string]interface{}{ + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + "passthrough_request_headers": []interface{}{"X-Vault-Foo"}, + }, + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + "passthrough_request_headers": []interface{}{"X-Vault-Foo"}, + } + testResponseBody(t, resp, &actual) + expected["request_id"] = actual["request_id"] + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) + } + + // Unset the mount tune value + resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "passthrough_request_headers": "", + }) + testResponseStatus(t, resp, 204) + + // Check results + resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") + testResponseStatus(t, resp, 200) + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "lease_id": "", + "renewable": false, + "lease_duration": json.Number("0"), + "wrap_info": nil, + "warnings": nil, + "auth": nil, + "data": map[string]interface{}{ + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + }, + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + } + testResponseBody(t, resp, &actual) + expected["request_id"] = actual["request_id"] + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) + } +} diff --git a/vault/core_test.go b/vault/core_test.go index b46ccb1af..f890df313 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -2276,3 +2276,88 @@ func TestCore_Standby_Rotate(t *testing.T) { t.Fatalf("bad: %#v", resp) } } + +// Ensure that InternalData is never returned +func TestCore_HandleRequest_Headers(t *testing.T) { + noop := &NoopBackend{ + Response: &logical.Response{ + Data: map[string]interface{}{}, + }, + } + + c, _, root := TestCoreUnsealed(t) + c.logicalBackends["noop"] = func(context.Context, *logical.BackendConfig) (logical.Backend, error) { + return noop, nil + } + + // Enable the backend + req := logical.TestRequest(t, logical.UpdateOperation, "sys/mounts/foo") + req.Data["type"] = "noop" + req.ClientToken = root + _, err := c.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Mount tune + req = logical.TestRequest(t, logical.UpdateOperation, "sys/mounts/foo/tune") + req.Data["passthrough_request_headers"] = []string{"Should-Passthrough", "should-passthrough-case-insensitive"} + req.ClientToken = root + _, err = c.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Attempt to read + lreq := &logical.Request{ + Operation: logical.ReadOperation, + Path: "foo/test", + ClientToken: root, + Headers: map[string][]string{ + "X-Vault-Kv-Client": []string{"foo"}, + "Should-Passthrough": []string{"foo"}, + "Should-Passthrough-Case-Insensitive": []string{"baz"}, + "Should-Not-Passthrough": []string{"bar"}, + }, + } + _, err = c.HandleRequest(lreq) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check the headers + headers := noop.Requests[0].Headers + + // Test whitelisted values + if val, ok := headers["X-Vault-Kv-Client"]; ok { + expected := []string{"foo"} + if !reflect.DeepEqual(val, expected) { + t.Fatalf("expected: %v, got: %v", expected, val) + } + } else { + t.Fatalf("expected 'X-Vault-Kv-Client' to be present in the headers map") + } + + // Test passthrough values + if val, ok := headers["Should-Passthrough"]; ok { + expected := []string{"foo"} + if !reflect.DeepEqual(val, expected) { + t.Fatalf("expected: %v, got: %v", expected, val) + } + } else { + t.Fatalf("expected 'Should-Passthrough' to be present in the headers map") + } + + if val, ok := headers["Should-Passthrough-Case-Insensitive"]; ok { + expected := []string{"baz"} + if !reflect.DeepEqual(val, expected) { + t.Fatalf("expected: %v, got: %v", expected, val) + } + } else { + t.Fatalf("expected 'Should-Passthrough-Case-Insensitive' to be present in the headers map") + } + + if _, ok := headers["Should-Not-Passthrough"]; ok { + t.Fatalf("did not expect 'Should-Not-Passthrough' to be in the headers map") + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index ba6560273..1d04ebca7 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -275,6 +275,10 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["listing_visibility"][0]), }, + "passthrough_request_headers": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["passthrough_request_headers"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.handleAuthTuneRead, @@ -320,6 +324,10 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["listing_visibility"][0]), }, + "passthrough_request_headers": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["passthrough_request_headers"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -1498,6 +1506,10 @@ func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Reque if len(entry.Config.ListingVisibility) > 0 { entryConfig["listing_visibility"] = entry.Config.ListingVisibility } + if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + entryConfig["passthrough_request_headers"] = rawVal.([]string) + } + info["config"] = entryConfig resp.Data[entry.Path] = info } @@ -1612,6 +1624,9 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d if len(apiConfig.AuditNonHMACResponseKeys) > 0 { config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys } + if len(apiConfig.PassthroughRequestHeaders) > 0 { + config.PassthroughRequestHeaders = apiConfig.PassthroughRequestHeaders + } // Create the mount entry me := &MountEntry{ @@ -1782,6 +1797,10 @@ func (b *SystemBackend) handleTuneReadCommon(path string) (*logical.Response, er resp.Data["listing_visibility"] = mountEntry.Config.ListingVisibility } + if rawVal, ok := mountEntry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + resp.Data["passthrough_request_headers"] = rawVal.([]string) + } + if len(mountEntry.Options) > 0 { resp.Data["options"] = mountEntry.Options } @@ -2000,6 +2019,32 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, } } + if rawVal, ok := data.GetOk("passthrough_request_headers"); ok { + headers := rawVal.([]string) + + oldVal := mountEntry.Config.PassthroughRequestHeaders + mountEntry.Config.PassthroughRequestHeaders = headers + + // Update the mount table + var err error + switch { + case strings.HasPrefix(path, "auth/"): + err = b.Core.persistAuth(ctx, b.Core.auth, mountEntry.Local) + default: + err = b.Core.persistMounts(ctx, b.Core.mounts, mountEntry.Local) + } + if err != nil { + mountEntry.Config.PassthroughRequestHeaders = oldVal + return handleError(err) + } + + mountEntry.SyncCache() + + if b.Core.logger.IsInfo() { + b.Core.logger.Info("core: mount tuning of passthrough_request_headers successful", "path", path) + } + } + var resp *logical.Response if optionsRaw, ok := data.GetOk("options"); ok { b.Core.logger.Info("core: mount tuning of options", "path", path) @@ -2050,6 +2095,7 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, delete(mountEntry.Options, "upgrade") } + return resp, nil } @@ -2214,6 +2260,10 @@ func (b *SystemBackend) handleAuthTable(ctx context.Context, req *logical.Reques if len(entry.Config.ListingVisibility) > 0 { entryConfig["listing_visibility"] = entry.Config.ListingVisibility } + if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + entryConfig["passthrough_request_headers"] = rawVal.([]string) + } + info["config"] = entryConfig resp.Data[entry.Path] = info } @@ -2320,6 +2370,9 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque if len(apiConfig.AuditNonHMACResponseKeys) > 0 { config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys } + if len(apiConfig.PassthroughRequestHeaders) > 0 { + config.PassthroughRequestHeaders = apiConfig.PassthroughRequestHeaders + } // Create the mount entry me := &MountEntry{ @@ -3820,4 +3873,7 @@ This path responds to the following HTTP methods. "listing_visibility": { "Determines the visibility of the mount in the UI-specific listing endpoint.", }, + "passthrough_request_headers": { + "A list of headers to whitelist and pass from the request to the backend.", + }, } diff --git a/vault/mount.go b/vault/mount.go index aa2c739e9..21c0b353a 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -184,30 +184,35 @@ type MountEntry struct { SealWrap bool `json:"seal_wrap"` // Whether to wrap CSPs Tainted bool `json:"tainted,omitempty"` // Set as a Write-Ahead flag for unmount/remount - // synthesizedConfigCache is used to cache configuration values + // synthesizedConfigCache is used to cache configuration values. These + // particular values are cached since we want to get them at a point-in-time + // without separately managing their locks individually. See SyncCache() for + // the specific values that are being cached. synthesizedConfigCache sync.Map } // MountConfig is used to hold settable options type MountConfig struct { - DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default - MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility ListingVisiblityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default + MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility ListingVisiblityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } // APIMountConfig is an embedded struct of api.MountConfigInput type APIMountConfig struct { - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility ListingVisiblityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility ListingVisiblityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` } // Clone returns a deep copy of the mount entry @@ -219,7 +224,9 @@ func (e *MountEntry) Clone() (*MountEntry, error) { return cp.(*MountEntry), nil } -// SyncCache syncs tunable configuration values to the cache +// SyncCache syncs tunable configuration values to the cache. In the case of +// cached values, they should be retrieved via synthesizedConfigCache.Load() +// instead of accessing them directly through MountConfig. func (e *MountEntry) SyncCache() { if len(e.Config.AuditNonHMACRequestKeys) == 0 { e.synthesizedConfigCache.Delete("audit_non_hmac_request_keys") @@ -232,6 +239,12 @@ func (e *MountEntry) SyncCache() { } else { e.synthesizedConfigCache.Store("audit_non_hmac_response_keys", e.Config.AuditNonHMACResponseKeys) } + + if len(e.Config.PassthroughRequestHeaders) == 0 { + e.synthesizedConfigCache.Delete("passthrough_request_headers") + } else { + e.synthesizedConfigCache.Store("passthrough_request_headers", e.Config.PassthroughRequestHeaders) + } } // Mount is used to mount a new backend to the mount table. diff --git a/vault/router.go b/vault/router.go index 209c29856..9b58282f3 100644 --- a/vault/router.go +++ b/vault/router.go @@ -15,6 +15,12 @@ import ( "github.com/hashicorp/vault/logical" ) +var ( + whitelistedHeaders = []string{ + consts.VaultKVCLIClientHeader, + } +) + // Router is used to do prefix based routing of a request to a logical backend type Router struct { l sync.RWMutex @@ -473,16 +479,15 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc originalClientTokenRemainingUses := req.ClientTokenRemainingUses req.ClientTokenRemainingUses = 0 - // Cache the headers and hide them from backends + // Cache the headers headers := req.Headers - req.Headers = nil - // Whitelist the X-Vault-Kv-Client header for use in the kv backend. - if val, ok := headers[consts.VaultKVCLIClientHeader]; ok { - req.Headers = map[string][]string{ - consts.VaultKVCLIClientHeader: val, - } + // Filter and add passthrough headers to the backend + var passthroughRequestHeaders []string + if rawVal, ok := re.mountEntry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + passthroughRequestHeaders = rawVal.([]string) } + req.Headers = filteredPassthroughHeaders(headers, passthroughRequestHeaders) // Cache the wrap info of the request var wrapInfo *logical.RequestWrapInfo @@ -624,3 +629,48 @@ func pathsToRadix(paths []string) *radix.Tree { return tree } + +// filteredPassthroughHeaders returns a headers map[string][]string that +// contains the filtered values contained in passthroughHeaders, as well as the +// values in whitelistedHeaders. Filtering of passthroughHeaders from the +// origHeaders is done is a case-insensitive manner. +func filteredPassthroughHeaders(origHeaders map[string][]string, passthroughHeaders []string) map[string][]string { + retHeaders := make(map[string][]string) + + // Handle whitelisted values + for _, header := range whitelistedHeaders { + if val, ok := origHeaders[header]; ok { + retHeaders[header] = val + } else { + // Try to check if a lowercased version of the header exists in the + // originating request. The header key that gets used is the one from the + // whitelist. + if val, ok := origHeaders[strings.ToLower(header)]; ok { + retHeaders[header] = val + } + } + } + + // Short-circuit if there's nothing to filter + if len(passthroughHeaders) == 0 { + return retHeaders + } + + // Create a map that uses lowercased header values as the key and the original + // header naming as the value for comparison down below. + lowerHeadersRef := make(map[string]string, len(origHeaders)) + for key := range origHeaders { + lowerHeadersRef[strings.ToLower(key)] = key + } + + // Case-insensitive compare of passthrough headers against originating + // headers. The returned headers will be the same casing as the originating + // header name. + for _, ph := range passthroughHeaders { + if header, ok := lowerHeadersRef[strings.ToLower(ph)]; ok { + retHeaders[header] = origHeaders[header] + } + } + + return retHeaders +} diff --git a/website/source/api/system/auth.html.md b/website/source/api/system/auth.html.md index 2f85c191a..bfd3898dd 100644 --- a/website/source/api/system/auth.html.md +++ b/website/source/api/system/auth.html.md @@ -92,6 +92,12 @@ For example, enable the "foo" auth method will make it accessible at - `audit_non_hmac_response_keys` `(array: [])` - Comma-separated list of keys that will not be HMAC'd by audit devices in the response data object. + - `listing_visibility` `(string: "")` - Speficies whether to show this mount + in the UI-specific listing endpoint. + + - `passthrough_request_headers` `(array: [])` - Comma-separated list of headers + to whitelist and pass from the request to the backend. + The plugin_name can be provided in the config map or as a top-level option, with the former taking precedence. @@ -215,6 +221,12 @@ can be achieved without `sudo` via `sys/mounts/auth/[auth-path]/tune`._ list of keys that will not be HMAC'd by audit devices in the response data object. +- `listing_visibility` `(string: "")` - Speficies whether to show this mount + in the UI-specific listing endpoint. + +- `passthrough_request_headers` `(array: [])` - Comma-separated list of headers + to whitelist and pass from the request to the backend. + ### Sample Payload ```json diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index d2a69900c..3291eb86b 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -97,6 +97,12 @@ This endpoint enables a new secrets engine at the given path. - `audit_non_hmac_response_keys` `(array: [])` - Comma-separated list of keys that will not be HMAC'd by audit devices in the response data object. + - `listing_visibility` `(string: "")` - Speficies whether to show this mount + in the UI-specific listing endpoint. + + - `passthrough_request_headers` `(array: [])` - Comma-separated list of headers + to whitelist and pass from the request to the backend. + These control the default and maximum lease time-to-live, force disabling backend caching, and option plugin name for plugin backends respectively. The first three options override the global defaults if @@ -216,6 +222,12 @@ This endpoint tunes configuration parameters for a given mount point. list of keys that will not be HMAC'd by audit devices in the response data object. +- `listing_visibility` `(string: "")` - Speficies whether to show this mount + in the UI-specific listing endpoint. + +- `passthrough_request_headers` `(array: [])` - Comma-separated list of headers + to whitelist and pass from the request to the backend. + ### Sample Payload ```json