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 9 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:improvement
peteski22 marked this conversation as resolved.
Show resolved Hide resolved
config/listener: allow per-listener configuration settings to redact parts of response to unauthenticated endpoints.
peteski22 marked this conversation as resolved.
Show resolved Hide resolved
```
3 changes: 2 additions & 1 deletion command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1529,7 +1529,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
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: "", // Redact using empty string.
peteski22 marked this conversation as resolved.
Show resolved Hide resolved
}
}

// 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
}
}
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)
}
21 changes: 16 additions & 5 deletions http/sys_seal.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func handleSysUnseal(core *vault.Core) http.Handler {
return
}
core.ResetUnsealProcess()
handleSysSealStatusRaw(core, w, r)
handleSysSealStatusRaw(core, w)
return
}

Expand Down Expand Up @@ -148,18 +148,18 @@ func handleSysUnseal(core *vault.Core) http.Handler {
}

// Return the seal status
handleSysSealStatusRaw(core, w, r)
handleSysSealStatusRaw(core, w)
})
}

func handleSysSealStatus(core *vault.Core) http.Handler {
func handleSysSealStatus(core *vault.Core, opt ...ListenerConfigOption) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
respondError(w, http.StatusMethodNotAllowed, nil)
return
}

handleSysSealStatusRaw(core, w, r)
handleSysSealStatusRaw(core, w, opt...)
})
}

Expand All @@ -174,14 +174,25 @@ func handleSysSealBackendStatus(core *vault.Core) http.Handler {
})
}

func handleSysSealStatusRaw(core *vault.Core, w http.ResponseWriter, r *http.Request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cleaning up this unused parameter!

func handleSysSealStatusRaw(core *vault.Core, w http.ResponseWriter, opt ...ListenerConfigOption) {
ctx := context.Background()
status, err := core.GetSealStatus(ctx)
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}

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

if opts.withRedactVersion {
status.Version = opts.withRedactionValue
status.BuildDate = opts.withRedactionValue
}

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

respondOk(w, status)
}

Expand Down
80 changes: 59 additions & 21 deletions internalshared/configutil/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ type Listener struct {
// ChrootNamespace will prepend the specified namespace to requests
ChrootNamespaceRaw interface{} `hcl:"chroot_namespace"`
ChrootNamespace string `hcl:"-"`

// Per-listener redaction configuration
VioletHynes marked this conversation as resolved.
Show resolved Hide resolved
RedactAddressesRaw any `hcl:"redact_addresses"`
RedactAddresses bool `hcl:"-"`
RedactClusterNameRaw any `hcl:"redact_cluster_name"`
RedactClusterName bool `hcl:"-"`
RedactVersionRaw any `hcl:"redact_version"`
RedactVersion bool `hcl:"-"`
}

// AgentAPI allows users to select which parts of the Agent API they want enabled.
Expand All @@ -144,6 +152,32 @@ func (l *Listener) Validate(path string) []ConfigError {
return append(results, ValidateUnusedFields(l.Profiling.UnusedKeys, path)...)
}

// ParseSingleIPTemplate is used as a helper function to parse out a single IP
// address from a config parameter.
// If the input doesn't appear to contain the 'template' format,
// it will return the specified input unchanged.
func ParseSingleIPTemplate(ipTmpl string) (string, error) {
r := regexp.MustCompile("{{.*?}}")
if !r.MatchString(ipTmpl) {
return ipTmpl, nil
}

out, err := template.Parse(ipTmpl)
if err != nil {
return "", fmt.Errorf("unable to parse address template %q: %v", ipTmpl, err)
}

ips := strings.Split(out, " ")
switch len(ips) {
case 0:
return "", errors.New("no addresses found, please configure one")
case 1:
return strings.TrimSpace(ips[0]), nil
default:
return "", fmt.Errorf("multiple addresses found (%q), please configure one", out)
}
}

// ParseListeners attempts to parse the AST list of objects into listeners.
func ParseListeners(list *ast.ObjectList) ([]*Listener, error) {
listeners := make([]*Listener, len(list.Items))
Expand Down Expand Up @@ -209,6 +243,7 @@ func parseListener(item *ast.ObjectItem) (*Listener, error) {
l.parseCORSSettings,
l.parseHTTPHeaderSettings,
l.parseChrootNamespaceSettings,
l.parseRedactionSettings,
} {
err := parser()
if err != nil {
Expand Down Expand Up @@ -565,28 +600,31 @@ func (l *Listener) parseCORSSettings() error {
return nil
}

// ParseSingleIPTemplate is used as a helper function to parse out a single IP
// address from a config parameter.
// If the input doesn't appear to contain the 'template' format,
// it will return the specified input unchanged.
func ParseSingleIPTemplate(ipTmpl string) (string, error) {
r := regexp.MustCompile("{{.*?}}")
if !r.MatchString(ipTmpl) {
return ipTmpl, nil
}
// parseRedactionSettings attempts to parse the raw listener redaction settings.
// The state of the listener will be modified, raw data will be cleared upon
// successful parsing.
func (l *Listener) parseRedactionSettings() error {
var err error

out, err := template.Parse(ipTmpl)
if err != nil {
return "", fmt.Errorf("unable to parse address template %q: %v", ipTmpl, err)
if l.RedactAddressesRaw != nil {
if l.RedactAddresses, err = parseutil.ParseBool(l.RedactAddressesRaw); err != nil {
return fmt.Errorf("invalid value for redact_addresses: %w", err)
}
}

ips := strings.Split(out, " ")
switch len(ips) {
case 0:
return "", errors.New("no addresses found, please configure one")
case 1:
return strings.TrimSpace(ips[0]), nil
default:
return "", fmt.Errorf("multiple addresses found (%q), please configure one", out)
if l.RedactClusterNameRaw != nil {
if l.RedactClusterName, err = parseutil.ParseBool(l.RedactClusterNameRaw); err != nil {
return fmt.Errorf("invalid value for redact_cluster_name: %w", err)
}
}
if l.RedactVersionRaw != nil {
if l.RedactVersion, err = parseutil.ParseBool(l.RedactVersionRaw); err != nil {
return fmt.Errorf("invalid value for redact_version: %w", err)
}
}

l.RedactAddressesRaw = nil
l.RedactClusterNameRaw = nil
l.RedactVersionRaw = nil

return nil
}
Loading
Loading