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

This commit is contained in:
Josh Black 2021-04-20 15:25:04 -07:00 committed by GitHub
parent 45e2bfcad7
commit 06809930a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 336 additions and 97 deletions

3
changelog/11289.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
http: Add optional HTTP response headers for hostname and raft node ID
```

View File

@ -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

View File

@ -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.
@ -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 != "" {
@ -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")
@ -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

View File

@ -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{})
@ -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,

View File

@ -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

View File

@ -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"
@ -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()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set the Cache-Control header for all the responses returned
// by Vault
@ -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)

View File

@ -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"
@ -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)

View File

@ -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{}{

View File

@ -26,8 +26,6 @@ import (
"sync/atomic"
"time"
"github.com/hashicorp/vault/physical/raft"
"github.com/armon/go-metrics"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
@ -41,6 +39,7 @@ import (
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internalshared/reloadutil"
"github.com/hashicorp/vault/physical/raft"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
@ -567,6 +566,10 @@ type Core struct {
// disableAutopilot is used to disable the autopilot subsystem in raft storage
disableAutopilot bool
// enable/disable identifying response headers
enableResponseHeaderHostname bool
enableResponseHeaderRaftNodeID bool
}
// CoreConfig is used to parameterize a core
@ -675,6 +678,10 @@ type CoreConfig struct {
// DisableAutopilot is used to disable autopilot subsystem in raft storage
DisableAutopilot bool
// Whether to send headers in the HTTP response showing hostname or raft node ID
EnableResponseHeaderHostname bool
EnableResponseHeaderRaftNodeID bool
}
// GetServiceRegistration returns the config's ServiceRegistration, or nil if it does
@ -813,15 +820,17 @@ func NewCore(conf *CoreConfig) (*Core, error) {
requests: new(uint64),
syncInterval: syncInterval,
},
recoveryMode: conf.RecoveryMode,
postUnsealStarted: new(uint32),
raftJoinDoneCh: make(chan struct{}),
clusterHeartbeatInterval: clusterHeartbeatInterval,
activityLogConfig: conf.ActivityLogConfig,
keyRotateGracePeriod: new(int64),
numExpirationWorkers: conf.NumExpirationWorkers,
raftFollowerStates: raft.NewFollowerStates(),
disableAutopilot: conf.DisableAutopilot,
recoveryMode: conf.RecoveryMode,
postUnsealStarted: new(uint32),
raftJoinDoneCh: make(chan struct{}),
clusterHeartbeatInterval: clusterHeartbeatInterval,
activityLogConfig: conf.ActivityLogConfig,
keyRotateGracePeriod: new(int64),
numExpirationWorkers: conf.NumExpirationWorkers,
raftFollowerStates: raft.NewFollowerStates(),
disableAutopilot: conf.DisableAutopilot,
enableResponseHeaderHostname: conf.EnableResponseHeaderHostname,
enableResponseHeaderRaftNodeID: conf.EnableResponseHeaderRaftNodeID,
}
c.standbyStopCh.Store(make(chan struct{}))
atomic.StoreUint32(c.sealed, 1)
@ -1004,6 +1013,18 @@ func NewCore(conf *CoreConfig) (*Core, error) {
return c, nil
}
// HostnameHeaderEnabled determines whether to add the X-Vault-Hostname header
// to HTTP responses.
func (c *Core) HostnameHeaderEnabled() bool {
return c.enableResponseHeaderHostname
}
// RaftNodeIDHeaderEnabled determines whether to add the X-Vault-Raft-Node-ID header
// to HTTP responses.
func (c *Core) RaftNodeIDHeaderEnabled() bool {
return c.enableResponseHeaderRaftNodeID
}
// Shutdown is invoked when the Vault instance is about to be terminated. It
// should not be accessible as part of an API call as it will cause an availability
// problem. It is only used to gracefully quit in the case of HA so that failover

View File

@ -12,28 +12,28 @@ import (
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/go-cleanhttp"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/helper/testhelpers/teststorage"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/physical/raft"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
)
type RaftClusterOpts struct {
DisableFollowerJoins bool
InmemCluster bool
EnableAutopilot bool
PhysicalFactoryConfig map[string]interface{}
DisablePerfStandby bool
DisableFollowerJoins bool
InmemCluster bool
EnableAutopilot bool
PhysicalFactoryConfig map[string]interface{}
DisablePerfStandby bool
EnableResponseHeaderRaftNodeID bool
}
func raftCluster(t testing.TB, ropts *RaftClusterOpts) *vault.TestCluster {
@ -45,7 +45,8 @@ func raftCluster(t testing.TB, ropts *RaftClusterOpts) *vault.TestCluster {
CredentialBackends: map[string]logical.Factory{
"userpass": credUserpass.Factory,
},
DisableAutopilot: !ropts.EnableAutopilot,
DisableAutopilot: !ropts.EnableAutopilot,
EnableResponseHeaderRaftNodeID: ropts.EnableResponseHeaderRaftNodeID,
}
opts := vault.TestClusterOptions{
@ -287,6 +288,64 @@ func TestRaft_RemovePeer(t *testing.T) {
})
}
func TestRaft_NodeIDHeader(t *testing.T) {
t.Parallel()
testCases := []struct {
description string
ropts *RaftClusterOpts
headerPresent bool
}{
{
description: "with no header configured",
ropts: nil,
headerPresent: false,
},
{
description: "with header configured",
ropts: &RaftClusterOpts{
EnableResponseHeaderRaftNodeID: true,
},
headerPresent: true,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
cluster := raftCluster(t, tc.ropts)
defer cluster.Cleanup()
for i, c := range cluster.Cores {
if c.Core.Sealed() {
t.Fatalf("failed to unseal core %d", i)
}
client := c.Client
req := client.NewRequest("GET", "/v1/sys/seal-status")
resp, err := client.RawRequest(req)
if err != nil {
t.Fatalf("err: %s", err)
}
if resp == nil {
t.Fatalf("nil response")
}
rniHeader := resp.Header.Get("X-Vault-Raft-Node-ID")
nodeID := c.Core.GetRaftNodeID()
if tc.headerPresent && rniHeader == "" {
t.Fatal("missing 'X-Vault-Raft-Node-ID' header entry in response")
}
if tc.headerPresent && rniHeader != nodeID {
t.Fatalf("got the wrong raft node id. expected %s to equal %s", rniHeader, nodeID)
}
if !tc.headerPresent && rniHeader != "" {
t.Fatal("didn't expect 'X-Vault-Raft-Node-ID' header but it was present anyway")
}
}
})
}
}
func TestRaft_Configuration(t *testing.T) {
t.Parallel()
cluster := raftCluster(t, nil)

View File

@ -40,6 +40,17 @@ var (
TestingUpdateClusterAddr uint32
)
// GetRaftNodeID returns the raft node ID if there is one, or an empty string if there's not
func (c *Core) GetRaftNodeID() string {
rb := c.getRaftBackend()
if rb == nil {
return ""
} else {
return rb.NodeID()
}
}
func (c *Core) GetRaftIndexes() (committed uint64, applied uint64) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()

View File

@ -27,7 +27,7 @@ import (
"time"
"github.com/armon/go-metrics"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-cleanhttp"
log "github.com/hashicorp/go-hclog"
raftlib "github.com/hashicorp/raft"
"github.com/hashicorp/vault/api"
@ -49,7 +49,7 @@ import (
"github.com/hashicorp/vault/vault/cluster"
"github.com/hashicorp/vault/vault/seal"
"github.com/mitchellh/copystructure"
testing "github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/go-testing-interface"
"golang.org/x/crypto/ed25519"
"golang.org/x/net/http2"
)
@ -159,6 +159,7 @@ func TestCoreWithSealAndUI(t testing.T, opts *CoreConfig) *Core {
conf.MetricSink = opts.MetricSink
conf.NumExpirationWorkers = numExpirationWorkersTest
conf.RawConfig = opts.RawConfig
conf.EnableResponseHeaderHostname = opts.EnableResponseHeaderHostname
if opts.Logger != nil {
conf.Logger = opts.Logger
@ -1518,6 +1519,8 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te
coreConfig.RecoveryMode = base.RecoveryMode
coreConfig.ActivityLogConfig = base.ActivityLogConfig
coreConfig.EnableResponseHeaderHostname = base.EnableResponseHeaderHostname
coreConfig.EnableResponseHeaderRaftNodeID = base.EnableResponseHeaderRaftNodeID
testApplyEntBaseConfig(coreConfig, base)
}

View File

@ -155,6 +155,21 @@ to specify where the configuration is.
- `pid_file` `(string: "")` - Path to the file in which the Vault server's
Process ID (PID) should be stored.
- `enable_response_header_hostname` `(bool: false)` - Enables the addition of an HTTP header
in all of Vault's HTTP responses: `X-Vault-Hostname`. This will contain the
host name of the Vault node that serviced the HTTP request. This information
is best effort and is not guaranteed to be present. If this configuration
option is enabled and the `X-Vault-Hostname` header is not present in a response,
it means there was some kind of error retrieving the host name from the
operating system.
- `enable_response_header_raft_node_id` `(bool: false)` - Enables the addition of an HTTP header
in all of Vault's HTTP responses: `X-Vault-Raft-Node-ID`. If Vault is participating
in a Raft cluster (i.e. using integrated storage), this header will contain the
Raft node ID of the Vault node that serviced the HTTP request. If Vault is not
participating in a Raft cluster, this header will be omitted, whether this configuration
option is enabled or not.
### High Availability Parameters
The following parameters are used on backends that support [high availability][high-availability].