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

VAULT-19863: Per-listener redaction settings #23534

Merged
merged 16 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1530,7 +1530,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 @@ -2161,7 +2162,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
Loading