Skip to content

Commit

Permalink
VAULT-19863: Per-listener redaction settings (#23534)
Browse files Browse the repository at this point in the history
* add redaction config settings to listener

* sys seal redaction + test modification for default handler properties

* build date should be redacted by 'redact_version' too

* sys-health redaction + test fiddling

* sys-leader redaction

* added changelog

* Lots of places need ListenerConfig

* Renamed options to something more specific for now

* tests for listener config options

* changelog updated

* updates based on PR comments

* updates based on PR comments - removed unrequired test case field

* fixes for docker tests and potentially server dev mode related flags
  • Loading branch information
Peter Wilson authored Oct 6, 2023
1 parent ebef296 commit e5432b0
Show file tree
Hide file tree
Showing 13 changed files with 448 additions and 39 deletions.
3 changes: 3 additions & 0 deletions changelog/23534.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
config/listener: allow per-listener configuration settings to redact sensitive parts of response to unauthenticated endpoints.
```
6 changes: 4 additions & 2 deletions command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1524,7 +1524,8 @@ func (c *ServerCommand) Run(args []string) int {
// mode if it's set
core.SetClusterListenerAddrs(clusterAddrs)
core.SetClusterHandler(vaulthttp.Handler.Handler(&vault.HandlerProperties{
Core: core,
Core: core,
ListenerConfig: &configutil.Listener{},
}))

// Attempt unsealing in a background goroutine. This is needed for when a
Expand Down Expand Up @@ -2155,7 +2156,8 @@ func (c *ServerCommand) enableThreeNodeDevCluster(base *vault.CoreConfig, info m

for _, core := range testCluster.Cores {
core.Server.Handler = vaulthttp.Handler.Handler(&vault.HandlerProperties{
Core: core.Core,
Core: core.Core,
ListenerConfig: &configutil.Listener{},
})
core.SetClusterHandler(core.Server.Handler)
}
Expand Down
6 changes: 6 additions & 0 deletions command/server/config_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,9 @@ listener "tcp" {
enable_quit = true
}
chroot_namespace = "admin"
redact_addresses = true
redact_cluster_name = true
redact_version = true
}`))

config := Config{
Expand Down Expand Up @@ -938,6 +941,9 @@ listener "tcp" {
},
CustomResponseHeaders: DefaultCustomHeaders,
ChrootNamespace: "admin/",
RedactAddresses: true,
RedactClusterName: true,
RedactVersion: true,
},
},
},
Expand Down
11 changes: 8 additions & 3 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,18 @@ func handler(props *vault.HandlerProperties) http.Handler {
mux.Handle("/v1/sys/host-info", handleLogicalNoForward(core))

mux.Handle("/v1/sys/init", handleSysInit(core))
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core))
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core,
WithRedactClusterName(props.ListenerConfig.RedactClusterName),
WithRedactVersion(props.ListenerConfig.RedactVersion)))
mux.Handle("/v1/sys/seal-backend-status", handleSysSealBackendStatus(core))
mux.Handle("/v1/sys/seal", handleSysSeal(core))
mux.Handle("/v1/sys/step-down", handleRequestForwarding(core, handleSysStepDown(core)))
mux.Handle("/v1/sys/unseal", handleSysUnseal(core))
mux.Handle("/v1/sys/leader", handleSysLeader(core))
mux.Handle("/v1/sys/health", handleSysHealth(core))
mux.Handle("/v1/sys/leader", handleSysLeader(core,
WithRedactAddresses(props.ListenerConfig.RedactAddresses)))
mux.Handle("/v1/sys/health", handleSysHealth(core,
WithRedactClusterName(props.ListenerConfig.RedactClusterName),
WithRedactVersion(props.ListenerConfig.RedactVersion)))
mux.Handle("/v1/sys/monitor", handleLogicalNoForward(core))
mux.Handle("/v1/sys/generate-root/attempt", handleRequestForwarding(core,
handleAuditNonLogical(core, handleSysGenerateRootAttempt(core, vault.GenerateStandardRootTokenStrategy))))
Expand Down
3 changes: 3 additions & 0 deletions http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"strings"
"testing"

"github.com/hashicorp/vault/internalshared/configutil"

"github.com/go-test/deep"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/helper/namespace"
Expand Down Expand Up @@ -806,6 +808,7 @@ func testNonPrintable(t *testing.T, disable bool) {
props := &vault.HandlerProperties{
Core: core,
DisablePrintableCheck: disable,
ListenerConfig: &configutil.Listener{},
}
TestServerWithListenerAndProperties(t, ln, addr, core, props)
defer ln.Close()
Expand Down
71 changes: 71 additions & 0 deletions http/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package http

// ListenerConfigOption is how listenerConfigOptions are passed as arguments.
type ListenerConfigOption func(*listenerConfigOptions) error

// listenerConfigOptions are used to represent configuration of listeners for http handlers.
type listenerConfigOptions struct {
withRedactionValue string
withRedactAddresses bool
withRedactClusterName bool
withRedactVersion bool
}

// getDefaultOptions returns listenerConfigOptions with their default values.
func getDefaultOptions() listenerConfigOptions {
return listenerConfigOptions{
withRedactionValue: "", // Redacted values will be set to an empty string by default.
}
}

// getOpts applies each supplied ListenerConfigOption and returns the fully configured listenerConfigOptions.
// Each ListenerConfigOption is applied in the order it appears in the argument list, so it is
// possible to supply the same ListenerConfigOption numerous times and the 'last write wins'.
func getOpts(opt ...ListenerConfigOption) (listenerConfigOptions, error) {
opts := getDefaultOptions()
for _, o := range opt {
if o == nil {
continue
}
if err := o(&opts); err != nil {
return listenerConfigOptions{}, err
}
}
return opts, nil
}

// WithRedactionValue provides an ListenerConfigOption to represent the value used to redact
// values which require redaction.
func WithRedactionValue(r string) ListenerConfigOption {
return func(o *listenerConfigOptions) error {
o.withRedactionValue = r
return nil
}
}

// WithRedactAddresses provides an ListenerConfigOption to represent whether redaction of addresses is required.
func WithRedactAddresses(r bool) ListenerConfigOption {
return func(o *listenerConfigOptions) error {
o.withRedactAddresses = r
return nil
}
}

// WithRedactClusterName provides an ListenerConfigOption to represent whether redaction of cluster names is required.
func WithRedactClusterName(r bool) ListenerConfigOption {
return func(o *listenerConfigOptions) error {
o.withRedactClusterName = r
return nil
}
}

// WithRedactVersion provides an ListenerConfigOption to represent whether redaction of version is required.
func WithRedactVersion(r bool) ListenerConfigOption {
return func(o *listenerConfigOptions) error {
o.withRedactVersion = r
return nil
}
}
159 changes: 159 additions & 0 deletions http/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package http

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestOptions_Default ensures that the default values are as expected.
func TestOptions_Default(t *testing.T) {
opts := getDefaultOptions()
require.NotNil(t, opts)
require.Equal(t, "", opts.withRedactionValue)
}

// TestOptions_WithRedactionValue ensures that we set the correct value to use for
// redaction when required.
func TestOptions_WithRedactionValue(t *testing.T) {
t.Parallel()

tests := map[string]struct {
Value string
ExpectedValue string
IsErrorExpected bool
}{
"empty": {
Value: "",
ExpectedValue: "",
IsErrorExpected: false,
},
"whitespace": {
Value: " ",
ExpectedValue: " ",
IsErrorExpected: false,
},
"value": {
Value: "*****",
ExpectedValue: "*****",
IsErrorExpected: false,
},
}

for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
opts := &listenerConfigOptions{}
applyOption := WithRedactionValue(tc.Value)
err := applyOption(opts)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
default:
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, opts.withRedactionValue)
}
})
}
}

// TestOptions_WithRedactAddresses ensures that the option works as intended.
func TestOptions_WithRedactAddresses(t *testing.T) {
t.Parallel()

tests := map[string]struct {
Value bool
ExpectedValue bool
}{
"true": {
Value: true,
ExpectedValue: true,
},
"false": {
Value: false,
ExpectedValue: false,
},
}

for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
opts := &listenerConfigOptions{}
applyOption := WithRedactAddresses(tc.Value)
err := applyOption(opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, opts.withRedactAddresses)
})
}
}

// TestOptions_WithRedactClusterName ensures that the option works as intended.
func TestOptions_WithRedactClusterName(t *testing.T) {
t.Parallel()

tests := map[string]struct {
Value bool
ExpectedValue bool
}{
"true": {
Value: true,
ExpectedValue: true,
},
"false": {
Value: false,
ExpectedValue: false,
},
}

for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
opts := &listenerConfigOptions{}
applyOption := WithRedactClusterName(tc.Value)
err := applyOption(opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, opts.withRedactClusterName)
})
}
}

// TestOptions_WithRedactVersion ensures that the option works as intended.
func TestOptions_WithRedactVersion(t *testing.T) {
t.Parallel()

tests := map[string]struct {
Value bool
ExpectedValue bool
}{
"true": {
Value: true,
ExpectedValue: true,
},
"false": {
Value: false,
ExpectedValue: false,
},
}

for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
opts := &listenerConfigOptions{}
applyOption := WithRedactVersion(tc.Value)
err := applyOption(opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, opts.withRedactVersion)
})
}
}
16 changes: 13 additions & 3 deletions http/sys_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import (
"github.com/hashicorp/vault/version"
)

func handleSysHealth(core *vault.Core) http.Handler {
func handleSysHealth(core *vault.Core, opt ...ListenerConfigOption) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
handleSysHealthGet(core, w, r)
handleSysHealthGet(core, w, r, opt...)
case "HEAD":
handleSysHealthHead(core, w, r)
default:
Expand All @@ -43,7 +43,7 @@ func fetchStatusCode(r *http.Request, field string) (int, bool, bool) {
return statusCode, false, true
}

func handleSysHealthGet(core *vault.Core, w http.ResponseWriter, r *http.Request) {
func handleSysHealthGet(core *vault.Core, w http.ResponseWriter, r *http.Request, opt ...ListenerConfigOption) {
code, body, err := getSysHealth(core, r)
if err != nil {
core.Logger().Error("error checking health", "error", err)
Expand All @@ -56,6 +56,16 @@ func handleSysHealthGet(core *vault.Core, w http.ResponseWriter, r *http.Request
return
}

opts, err := getOpts(opt...)

if opts.withRedactVersion {
body.Version = opts.withRedactionValue
}

if opts.withRedactClusterName {
body.ClusterName = opts.withRedactionValue
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)

Expand Down
13 changes: 10 additions & 3 deletions http/sys_leader.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,29 @@ import (

// This endpoint is needed to answer queries before Vault unseals
// or becomes the leader.
func handleSysLeader(core *vault.Core) http.Handler {
func handleSysLeader(core *vault.Core, opt ...ListenerConfigOption) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
handleSysLeaderGet(core, w, r)
handleSysLeaderGet(core, w, opt...)
default:
respondError(w, http.StatusMethodNotAllowed, nil)
}
})
}

func handleSysLeaderGet(core *vault.Core, w http.ResponseWriter, r *http.Request) {
func handleSysLeaderGet(core *vault.Core, w http.ResponseWriter, opt ...ListenerConfigOption) {
resp, err := core.GetLeaderStatus()
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}

opts, err := getOpts(opt...)
if opts.withRedactAddresses {
resp.LeaderAddress = opts.withRedactionValue
resp.LeaderClusterAddress = opts.withRedactionValue
}

respondOk(w, resp)
}
Loading

0 comments on commit e5432b0

Please sign in to comment.