Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP response headers for hostname and raft node ID (if applicable) #11289

Merged
merged 13 commits into from
Apr 20, 2021
3 changes: 3 additions & 0 deletions changelog/11289.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
http: Add optional HTTP response headers for hostname and raft node ID
```
64 changes: 33 additions & 31 deletions command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1284,37 +1284,39 @@ func (c *ServerCommand) Run(args []string) int {
}

coreConfig := &vault.CoreConfig{
RawConfig: config,
Physical: backend,
RedirectAddr: config.Storage.RedirectAddr,
StorageType: config.Storage.Type,
HAPhysical: nil,
ServiceRegistration: configSR,
Seal: barrierSeal,
UnwrapSeal: unwrapSeal,
AuditBackends: c.AuditBackends,
CredentialBackends: c.CredentialBackends,
LogicalBackends: c.LogicalBackends,
Logger: c.logger,
DisableSentinelTrace: config.DisableSentinelTrace,
DisableCache: config.DisableCache,
DisableMlock: config.DisableMlock,
MaxLeaseTTL: config.MaxLeaseTTL,
DefaultLeaseTTL: config.DefaultLeaseTTL,
ClusterName: config.ClusterName,
CacheSize: config.CacheSize,
PluginDirectory: config.PluginDirectory,
EnableUI: config.EnableUI,
EnableRaw: config.EnableRawEndpoint,
DisableSealWrap: config.DisableSealWrap,
DisablePerformanceStandby: config.DisablePerformanceStandby,
DisableIndexing: config.DisableIndexing,
AllLoggers: c.allLoggers,
BuiltinRegistry: builtinplugins.Registry,
DisableKeyEncodingChecks: config.DisablePrintableCheck,
MetricsHelper: metricsHelper,
MetricSink: metricSink,
SecureRandomReader: secureRandomReader,
RawConfig: config,
Physical: backend,
RedirectAddr: config.Storage.RedirectAddr,
StorageType: config.Storage.Type,
HAPhysical: nil,
ServiceRegistration: configSR,
Seal: barrierSeal,
UnwrapSeal: unwrapSeal,
AuditBackends: c.AuditBackends,
CredentialBackends: c.CredentialBackends,
LogicalBackends: c.LogicalBackends,
Logger: c.logger,
DisableSentinelTrace: config.DisableSentinelTrace,
DisableCache: config.DisableCache,
DisableMlock: config.DisableMlock,
MaxLeaseTTL: config.MaxLeaseTTL,
DefaultLeaseTTL: config.DefaultLeaseTTL,
ClusterName: config.ClusterName,
CacheSize: config.CacheSize,
PluginDirectory: config.PluginDirectory,
EnableUI: config.EnableUI,
EnableRaw: config.EnableRawEndpoint,
DisableSealWrap: config.DisableSealWrap,
DisablePerformanceStandby: config.DisablePerformanceStandby,
DisableIndexing: config.DisableIndexing,
AllLoggers: c.allLoggers,
BuiltinRegistry: builtinplugins.Registry,
DisableKeyEncodingChecks: config.DisablePrintableCheck,
MetricsHelper: metricsHelper,
MetricSink: metricSink,
SecureRandomReader: secureRandomReader,
EnableResponseHeaderHostname: config.EnableResponseHeaderHostname,
EnableResponseHeaderRaftNodeID: config.EnableResponseHeaderRaftNodeID,
}
if c.flagDev {
coreConfig.EnableRaw = true
Expand Down
32 changes: 32 additions & 0 deletions command/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ type Config struct {

DisableSentinelTrace bool `hcl:"-"`
DisableSentinelTraceRaw interface{} `hcl:"disable_sentinel_trace"`

EnableResponseHeaderHostname bool `hcl:"-"`
EnableResponseHeaderHostnameRaw interface{} `hcl:"enable_response_header_hostname"`

EnableResponseHeaderRaftNodeID bool `hcl:"-"`
EnableResponseHeaderRaftNodeIDRaw interface{} `hcl:"enable_response_header_raft_node_id"`
}

// DevConfig is a Config that is used for dev mode of Vault.
Expand Down Expand Up @@ -245,6 +251,16 @@ func (c *Config) Merge(c2 *Config) *Config {
result.DisableIndexing = c2.DisableIndexing
}

result.EnableResponseHeaderHostname = c.EnableResponseHeaderHostname
if c2.EnableResponseHeaderHostname {
result.EnableResponseHeaderHostname = c2.EnableResponseHeaderHostname
}

result.EnableResponseHeaderRaftNodeID = c.EnableResponseHeaderRaftNodeID
if c2.EnableResponseHeaderRaftNodeID {
result.EnableResponseHeaderRaftNodeID = c2.EnableResponseHeaderRaftNodeID
}

// Use values from top-level configuration for storage if set
if storage := result.Storage; storage != nil {
if result.APIAddr != "" {
Expand Down Expand Up @@ -406,6 +422,18 @@ func ParseConfig(d string) (*Config, error) {
}
}

if result.EnableResponseHeaderHostnameRaw != nil {
if result.EnableResponseHeaderHostname, err = parseutil.ParseBool(result.EnableResponseHeaderHostnameRaw); err != nil {
return nil, err
}
}

if result.EnableResponseHeaderRaftNodeIDRaw != nil {
if result.EnableResponseHeaderRaftNodeID, err = parseutil.ParseBool(result.EnableResponseHeaderRaftNodeIDRaw); err != nil {
return nil, err
}
}

list, ok := obj.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
Expand Down Expand Up @@ -742,6 +770,10 @@ func (c *Config) Sanitized() map[string]interface{} {
"disable_sealwrap": c.DisableSealWrap,

"disable_indexing": c.DisableIndexing,

"enable_response_header_hostname": c.EnableResponseHeaderHostname,

"enable_response_header_raft_node_id": c.EnableResponseHeaderRaftNodeID,
}
for k, v := range sharedResult {
result[k] = v
Expand Down
41 changes: 24 additions & 17 deletions command/server/config_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ func testLoadConfigFile(t *testing.T) {
MaxLeaseTTLRaw: "10h",
DefaultLeaseTTL: 10 * time.Hour,
DefaultLeaseTTLRaw: "10h",

EnableResponseHeaderHostname: true,
EnableResponseHeaderHostnameRaw: true,
EnableResponseHeaderRaftNodeID: true,
EnableResponseHeaderRaftNodeIDRaw: true,
}

addExpectedEntConfig(expected, []string{})
Expand Down Expand Up @@ -606,23 +611,25 @@ func testConfig_Sanitized(t *testing.T) {
sanitizedConfig := config.Sanitized()

expected := map[string]interface{}{
"api_addr": "top_level_api_addr",
"cache_size": 0,
"cluster_addr": "top_level_cluster_addr",
"cluster_cipher_suites": "",
"cluster_name": "testcluster",
"default_lease_ttl": 10 * time.Hour,
"default_max_request_duration": 0 * time.Second,
"disable_cache": true,
"disable_clustering": false,
"disable_indexing": false,
"disable_mlock": true,
"disable_performance_standby": false,
"disable_printable_check": false,
"disable_sealwrap": true,
"raw_storage_endpoint": true,
"disable_sentinel_trace": true,
"enable_ui": true,
"api_addr": "top_level_api_addr",
"cache_size": 0,
"cluster_addr": "top_level_cluster_addr",
"cluster_cipher_suites": "",
"cluster_name": "testcluster",
"default_lease_ttl": 10 * time.Hour,
"default_max_request_duration": 0 * time.Second,
"disable_cache": true,
"disable_clustering": false,
"disable_indexing": false,
"disable_mlock": true,
"disable_performance_standby": false,
"disable_printable_check": false,
"disable_sealwrap": true,
"raw_storage_endpoint": true,
"disable_sentinel_trace": true,
"enable_ui": true,
"enable_response_header_hostname": false,
"enable_response_header_raft_node_id": false,
"ha_storage": map[string]interface{}{
"cluster_addr": "top_level_cluster_addr",
"disable_clustering": true,
Expand Down
2 changes: 2 additions & 0 deletions command/server/test-fixtures/config.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ pid_file = "./pidfile"
raw_storage_endpoint = true
disable_sealwrap = true
disable_printable_check = true
enable_response_header_hostname = true
enable_response_header_raft_node_id = true
21 changes: 19 additions & 2 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"github.com/NYTimes/gziphandler"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/hashicorp/errwrap"
cleanhttp "github.com/hashicorp/go-cleanhttp"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/helper/consts"
Expand Down Expand Up @@ -293,6 +293,11 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr
if maxRequestSize == 0 {
maxRequestSize = DefaultMaxRequestSize
}

// Swallow this error since we don't want to pollute the logs and we also don't want to
// return an HTTP error here. This information is best effort.
hostname, _ := os.Hostname()
raskchanky marked this conversation as resolved.
Show resolved Hide resolved

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set the Cache-Control header for all the responses returned
// by Vault
Expand All @@ -316,6 +321,18 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr
r = r.WithContext(ctx)
r = r.WithContext(namespace.ContextWithNamespace(r.Context(), namespace.RootNamespace))

// Set some response headers with raft node id (if applicable) and hostname, if available
if core.RaftNodeIDHeaderEnabled() {
nodeID := core.GetRaftNodeID()
if nodeID != "" {
w.Header().Set("X-Vault-Raft-Node-ID", nodeID)
}
}

if core.HostnameHeaderEnabled() && hostname != "" {
w.Header().Set("X-Vault-Hostname", hostname)
}

switch {
case strings.HasPrefix(r.URL.Path, "/v1/"):
newR, status := adjustRequest(core, r)
Expand Down
69 changes: 67 additions & 2 deletions http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import (
"testing"

"github.com/go-test/deep"

cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical"
Expand Down Expand Up @@ -191,6 +190,72 @@ func TestHandler_cors(t *testing.T) {
}
}

func TestHandler_HostnameHeader(t *testing.T) {
t.Parallel()
testCases := []struct {
description string
config *vault.CoreConfig
headerPresent bool
}{
{
description: "with no header configured",
config: nil,
headerPresent: false,
},
{
description: "with header configured",
config: &vault.CoreConfig{
EnableResponseHeaderHostname: true,
},
headerPresent: true,
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
var core *vault.Core

if tc.config == nil {
core, _, _ = vault.TestCoreUnsealed(t)
} else {
core, _, _ = vault.TestCoreUnsealedWithConfig(t, tc.config)
}

ln, addr := TestServer(t, core)
defer ln.Close()

req, err := http.NewRequest("GET", addr+"/v1/sys/seal-status", nil)
if err != nil {
t.Fatalf("err: %s", err)
}

client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

if resp == nil {
t.Fatal("nil response")
}

hnHeader := resp.Header.Get("X-Vault-Hostname")
if tc.headerPresent && hnHeader == "" {
t.Logf("header configured = %t", core.HostnameHeaderEnabled())
t.Fatal("missing 'X-Vault-Hostname' header entry in response")
}
if !tc.headerPresent && hnHeader != "" {
t.Fatal("didn't expect 'X-Vault-Hostname' header but it was present anyway")
}

rniHeader := resp.Header.Get("X-Vault-Raft-Node-ID")
if rniHeader != "" {
t.Fatalf("no raft node ID header was expected, since we're not running a raft cluster. instead, got %s", rniHeader)
}
})
}
}

func TestHandler_CacheControlNoStore(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
Expand Down
46 changes: 24 additions & 22 deletions http/sys_config_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,30 @@ func TestSysConfigState_Sanitized(t *testing.T) {
var expected map[string]interface{}

configResp := map[string]interface{}{
"api_addr": "",
"cache_size": json.Number("0"),
"cluster_addr": "",
"cluster_cipher_suites": "",
"cluster_name": "",
"default_lease_ttl": json.Number("0"),
"default_max_request_duration": json.Number("0"),
"disable_cache": false,
"disable_clustering": false,
"disable_indexing": false,
"disable_mlock": false,
"disable_performance_standby": false,
"disable_printable_check": false,
"disable_sealwrap": false,
"raw_storage_endpoint": false,
"disable_sentinel_trace": false,
"enable_ui": false,
"log_format": "",
"log_level": "",
"max_lease_ttl": json.Number("0"),
"pid_file": "",
"plugin_directory": "",
"api_addr": "",
"cache_size": json.Number("0"),
"cluster_addr": "",
"cluster_cipher_suites": "",
"cluster_name": "",
"default_lease_ttl": json.Number("0"),
"default_max_request_duration": json.Number("0"),
"disable_cache": false,
"disable_clustering": false,
"disable_indexing": false,
"disable_mlock": false,
"disable_performance_standby": false,
"disable_printable_check": false,
"disable_sealwrap": false,
"raw_storage_endpoint": false,
"disable_sentinel_trace": false,
"enable_ui": false,
"log_format": "",
"log_level": "",
"max_lease_ttl": json.Number("0"),
"pid_file": "",
"plugin_directory": "",
"enable_response_header_hostname": false,
"enable_response_header_raft_node_id": false,
}

expected = map[string]interface{}{
Expand Down
Loading