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

@@ -92,12 +92,13 @@ type EnableAuthOptions struct {
} }
type AuthConfigInput struct { type AuthConfigInput struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` 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"` 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"` 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"` 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 {
@@ -111,10 +112,11 @@ type AuthMount struct {
} }
type AuthConfigOutput struct { type AuthConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` 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"` 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"` 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"` 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

@@ -130,14 +130,15 @@ type MountInput struct {
} }
type MountConfigInput struct { type MountConfigInput struct {
Options map[string]string `json:"options" structs:"options" mapstructure:"options"` Options map[string]string `json:"options" structs:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` 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"` 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"` 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"` 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"` 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 {
@@ -151,12 +152,13 @@ type MountOutput struct {
} }
type MountConfigOutput struct { type MountConfigOutput struct {
Options map[string]string `json:"options" structs:"options" mapstructure:"options"` Options map[string]string `json:"options" structs:"options" mapstructure:"options"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` 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"` 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"` 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"` 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"` 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

@@ -17,17 +17,18 @@ var _ cli.CommandAutocomplete = (*AuthEnableCommand)(nil)
type AuthEnableCommand struct { type AuthEnableCommand struct {
*BaseCommand *BaseCommand
flagDescription string flagDescription string
flagPath string flagPath string
flagDefaultLeaseTTL time.Duration flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string flagAuditNonHMACResponseKeys []string
flagListingVisibility string flagListingVisibility string
flagPluginName string flagPassthroughRequestHeaders []string
flagOptions map[string]string flagPluginName string
flagLocal bool flagOptions map[string]string
flagSealWrap bool flagLocal bool
flagSealWrap bool
} }
func (c *AuthEnableCommand) Synopsis() string { 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.", 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

@@ -17,18 +17,19 @@ var _ cli.CommandAutocomplete = (*SecretsEnableCommand)(nil)
type SecretsEnableCommand struct { type SecretsEnableCommand struct {
*BaseCommand *BaseCommand
flagDescription string flagDescription string
flagPath string flagPath string
flagDefaultLeaseTTL time.Duration flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string flagAuditNonHMACResponseKeys []string
flagListingVisibility string flagListingVisibility string
flagForceNoCache bool flagPassthroughRequestHeaders []string
flagPluginName string flagForceNoCache bool
flagOptions map[string]string flagPluginName string
flagLocal bool flagOptions map[string]string
flagSealWrap bool flagLocal bool
flagSealWrap bool
} }
func (c *SecretsEnableCommand) Synopsis() string { 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.", 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,30 +184,35 @@ 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
} }
// MountConfig is used to hold settable options // MountConfig is used to hold settable options
type MountConfig struct { type MountConfig struct {
DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default 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 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 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"` 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"` 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
type APIMountConfig struct { type APIMountConfig struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` 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"` 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"` 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"` 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"` 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