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
This commit is contained in:
Calvin Leung Huang
2018-03-21 19:56:47 -04:00
committed by GitHub
parent a03a722fa9
commit c54c9519c8
12 changed files with 409 additions and 75 deletions

View File

@@ -98,6 +98,7 @@ type AuthConfigInput struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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 { type AuthMount struct {
@@ -117,4 +118,5 @@ type AuthConfigOutput struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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"`
} }

View File

@@ -138,6 +138,7 @@ type MountConfigInput struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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 { type MountOutput struct {
@@ -159,4 +160,5 @@ type MountConfigOutput struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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"`
} }

View File

@@ -24,6 +24,7 @@ type AuthEnableCommand struct {
flagAuditNonHMACRequestKeys []string flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string flagAuditNonHMACResponseKeys []string
flagListingVisibility string flagListingVisibility string
flagPassthroughRequestHeaders []string
flagPluginName string flagPluginName string
flagOptions map[string]string flagOptions map[string]string
flagLocal bool flagLocal bool
@@ -121,6 +122,13 @@ func (c *AuthEnableCommand) Flags() *FlagSets {
Usage: "Determines the visibility of the mount in the UI-specific listing endpoint.", 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{ f.StringVar(&StringVar{
Name: "plugin-name", Name: "plugin-name",
Target: &c.flagPluginName, Target: &c.flagPluginName,
@@ -229,6 +237,10 @@ func (c *AuthEnableCommand) Run(args []string) int {
if fl.Name == flagNameListingVisibility { if fl.Name == flagNameListingVisibility {
authOpts.Config.ListingVisibility = c.flagListingVisibility authOpts.Config.ListingVisibility = c.flagListingVisibility
} }
if fl.Name == flagNamePassthroughRequestHeaders {
authOpts.Config.PassthroughRequestHeaders = c.flagPassthroughRequestHeaders
}
}) })
if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil { if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil {

View File

@@ -79,6 +79,8 @@ const (
flagNameAuditNonHMACResponseKeys = "audit-non-hmac-response-keys" flagNameAuditNonHMACResponseKeys = "audit-non-hmac-response-keys"
// flagListingVisibility is the flag to toggle whether to show the mount in the UI-specific listing endpoint // flagListingVisibility is the flag to toggle whether to show the mount in the UI-specific listing endpoint
flagNameListingVisibility = "listing-visibility" flagNameListingVisibility = "listing-visibility"
// flagNamePassthroughRequestHeaders is the flag name used to set passthrough request headers to the backend
flagNamePassthroughRequestHeaders = "passthrough-request-headers"
) )
var ( var (

View File

@@ -24,6 +24,7 @@ type SecretsEnableCommand struct {
flagAuditNonHMACRequestKeys []string flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string flagAuditNonHMACResponseKeys []string
flagListingVisibility string flagListingVisibility string
flagPassthroughRequestHeaders []string
flagForceNoCache bool flagForceNoCache bool
flagPluginName string flagPluginName string
flagOptions map[string]string flagOptions map[string]string
@@ -129,6 +130,13 @@ func (c *SecretsEnableCommand) Flags() *FlagSets {
Usage: "Determines the visibility of the mount in the UI-specific listing endpoint.", 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{ f.BoolVar(&BoolVar{
Name: "force-no-cache", Name: "force-no-cache",
Target: &c.flagForceNoCache, Target: &c.flagForceNoCache,
@@ -249,6 +257,10 @@ func (c *SecretsEnableCommand) Run(args []string) int {
if fl.Name == flagNameListingVisibility { if fl.Name == flagNameListingVisibility {
mountInput.Config.ListingVisibility = c.flagListingVisibility mountInput.Config.ListingVisibility = c.flagListingVisibility
} }
if fl.Name == flagNamePassthroughRequestHeaders {
mountInput.Config.PassthroughRequestHeaders = c.flagPassthroughRequestHeaders
}
}) })
if err := client.Sys().Mount(mountPath, mountInput); err != nil { if err := client.Sys().Mount(mountPath, mountInput); err != nil {

View File

@@ -1287,6 +1287,7 @@ func TestSysTuneMount_nonHMACKeys(t *testing.T) {
resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{
"audit_non_hmac_response_keys": "", "audit_non_hmac_response_keys": "",
}) })
testResponseStatus(t, resp, 204)
// Check results // Check results
resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") 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) core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core) ln, addr := TestServer(t, core)
defer ln.Close() defer ln.Close()
@@ -1390,3 +1391,78 @@ func TestSysTuneMount_showUIMount(t *testing.T) {
t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) 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)
}
}

View File

@@ -2276,3 +2276,88 @@ func TestCore_Standby_Rotate(t *testing.T) {
t.Fatalf("bad: %#v", resp) 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")
}
}

View File

@@ -275,6 +275,10 @@ func NewSystemBackend(core *Core) *SystemBackend {
Type: framework.TypeString, Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["listing_visibility"][0]), 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{ Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.handleAuthTuneRead, logical.ReadOperation: b.handleAuthTuneRead,
@@ -320,6 +324,10 @@ func NewSystemBackend(core *Core) *SystemBackend {
Type: framework.TypeString, Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["listing_visibility"][0]), 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{ 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 { if len(entry.Config.ListingVisibility) > 0 {
entryConfig["listing_visibility"] = entry.Config.ListingVisibility 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 info["config"] = entryConfig
resp.Data[entry.Path] = info 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 { if len(apiConfig.AuditNonHMACResponseKeys) > 0 {
config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys
} }
if len(apiConfig.PassthroughRequestHeaders) > 0 {
config.PassthroughRequestHeaders = apiConfig.PassthroughRequestHeaders
}
// Create the mount entry // Create the mount entry
me := &MountEntry{ me := &MountEntry{
@@ -1782,6 +1797,10 @@ func (b *SystemBackend) handleTuneReadCommon(path string) (*logical.Response, er
resp.Data["listing_visibility"] = mountEntry.Config.ListingVisibility 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 { if len(mountEntry.Options) > 0 {
resp.Data["options"] = mountEntry.Options 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 var resp *logical.Response
if optionsRaw, ok := data.GetOk("options"); ok { if optionsRaw, ok := data.GetOk("options"); ok {
b.Core.logger.Info("core: mount tuning of options", "path", path) 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") delete(mountEntry.Options, "upgrade")
} }
return resp, nil return resp, nil
} }
@@ -2214,6 +2260,10 @@ func (b *SystemBackend) handleAuthTable(ctx context.Context, req *logical.Reques
if len(entry.Config.ListingVisibility) > 0 { if len(entry.Config.ListingVisibility) > 0 {
entryConfig["listing_visibility"] = entry.Config.ListingVisibility 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 info["config"] = entryConfig
resp.Data[entry.Path] = info resp.Data[entry.Path] = info
} }
@@ -2320,6 +2370,9 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque
if len(apiConfig.AuditNonHMACResponseKeys) > 0 { if len(apiConfig.AuditNonHMACResponseKeys) > 0 {
config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys config.AuditNonHMACResponseKeys = apiConfig.AuditNonHMACResponseKeys
} }
if len(apiConfig.PassthroughRequestHeaders) > 0 {
config.PassthroughRequestHeaders = apiConfig.PassthroughRequestHeaders
}
// Create the mount entry // Create the mount entry
me := &MountEntry{ me := &MountEntry{
@@ -3820,4 +3873,7 @@ This path responds to the following HTTP methods.
"listing_visibility": { "listing_visibility": {
"Determines the visibility of the mount in the UI-specific listing endpoint.", "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.",
},
} }

View File

@@ -184,7 +184,10 @@ type MountEntry struct {
SealWrap bool `json:"seal_wrap"` // Whether to wrap CSPs SealWrap bool `json:"seal_wrap"` // Whether to wrap CSPs
Tainted bool `json:"tainted,omitempty"` // Set as a Write-Ahead flag for unmount/remount 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 synthesizedConfigCache sync.Map
} }
@@ -197,6 +200,7 @@ type MountConfig struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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 // APIMountConfig is an embedded struct of api.MountConfigInput
@@ -208,6 +212,7 @@ type APIMountConfig struct {
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` 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"` 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"` 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 // Clone returns a deep copy of the mount entry
@@ -219,7 +224,9 @@ func (e *MountEntry) Clone() (*MountEntry, error) {
return cp.(*MountEntry), nil 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() { func (e *MountEntry) SyncCache() {
if len(e.Config.AuditNonHMACRequestKeys) == 0 { if len(e.Config.AuditNonHMACRequestKeys) == 0 {
e.synthesizedConfigCache.Delete("audit_non_hmac_request_keys") e.synthesizedConfigCache.Delete("audit_non_hmac_request_keys")
@@ -232,6 +239,12 @@ func (e *MountEntry) SyncCache() {
} else { } else {
e.synthesizedConfigCache.Store("audit_non_hmac_response_keys", e.Config.AuditNonHMACResponseKeys) 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. // Mount is used to mount a new backend to the mount table.

View File

@@ -15,6 +15,12 @@ import (
"github.com/hashicorp/vault/logical" "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 // Router is used to do prefix based routing of a request to a logical backend
type Router struct { type Router struct {
l sync.RWMutex l sync.RWMutex
@@ -473,16 +479,15 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc
originalClientTokenRemainingUses := req.ClientTokenRemainingUses originalClientTokenRemainingUses := req.ClientTokenRemainingUses
req.ClientTokenRemainingUses = 0 req.ClientTokenRemainingUses = 0
// Cache the headers and hide them from backends // Cache the headers
headers := req.Headers headers := req.Headers
req.Headers = nil
// Whitelist the X-Vault-Kv-Client header for use in the kv backend. // Filter and add passthrough headers to the backend
if val, ok := headers[consts.VaultKVCLIClientHeader]; ok { var passthroughRequestHeaders []string
req.Headers = map[string][]string{ if rawVal, ok := re.mountEntry.synthesizedConfigCache.Load("passthrough_request_headers"); ok {
consts.VaultKVCLIClientHeader: val, passthroughRequestHeaders = rawVal.([]string)
}
} }
req.Headers = filteredPassthroughHeaders(headers, passthroughRequestHeaders)
// Cache the wrap info of the request // Cache the wrap info of the request
var wrapInfo *logical.RequestWrapInfo var wrapInfo *logical.RequestWrapInfo
@@ -624,3 +629,48 @@ func pathsToRadix(paths []string) *radix.Tree {
return 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
}

View File

@@ -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 - `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. 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, The plugin_name can be provided in the config map or as a top-level option,
with the former taking precedence. 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 list of keys that will not be HMAC'd by audit devices in the response data
object. 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 ### Sample Payload
```json ```json

View File

@@ -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 - `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. 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 These control the default and maximum lease time-to-live, force
disabling backend caching, and option plugin name for plugin backends disabling backend caching, and option plugin name for plugin backends
respectively. The first three options override the global defaults if 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 list of keys that will not be HMAC'd by audit devices in the response data
object. 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 ### Sample Payload
```json ```json