diff --git a/api/sys_mounts.go b/api/sys_mounts.go index ab3ab752ee..2f433d2e4a 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -129,10 +129,12 @@ type MountInput struct { } type MountConfigInput struct { - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` } type MountOutput struct { @@ -145,8 +147,10 @@ type MountOutput struct { } type MountConfigOutput struct { - DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` } diff --git a/audit/audit.go b/audit/audit.go index 65d4644c7c..6adf3b8bcb 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -16,13 +16,13 @@ type Backend interface { // request is authorized but before the request is executed. The arguments // MUST not be modified in anyway. They should be deep copied if this is // a possibility. - LogRequest(context.Context, *logical.Auth, *logical.Request, error) error + LogRequest(context.Context, *LogInput) error // LogResponse is used to synchronously log a response. This is done after // the request is processed but before the response is sent. The arguments // MUST not be modified in anyway. They should be deep copied if this is // a possibility. - LogResponse(context.Context, *logical.Auth, *logical.Request, *logical.Response, error) error + LogResponse(context.Context, *LogInput) error // GetHash is used to return the given data with the backend's hash, // so that a caller can determine if a value in the audit log matches @@ -36,6 +36,18 @@ type Backend interface { Invalidate(context.Context) } +// LogInput contains the input parameters passed into LogRequest and LogResponse +type LogInput struct { + Auth *logical.Auth + Request *logical.Request + Response *logical.Response + OuterErr error + NonHMACReqDataKeys []string + NonHMACRespDataKeys []string +} + +// BackendConfig contains configuration parameters used in the factory func to +// instantiate audit backends type BackendConfig struct { // The view to store the salt SaltView logical.Storage diff --git a/audit/format.go b/audit/format.go index eaf29f0ede..aaa92a5730 100644 --- a/audit/format.go +++ b/audit/format.go @@ -25,14 +25,10 @@ type AuditFormatter struct { AuditFormatWriter } -func (f *AuditFormatter) FormatRequest( - w io.Writer, - config FormatterConfig, - auth *logical.Auth, - req *logical.Request, - inErr error) error { +var _ Formatter = (*AuditFormatter)(nil) - if req == nil { +func (f *AuditFormatter) FormatRequest(w io.Writer, config FormatterConfig, in *LogInput) error { + if in == nil || in.Request == nil { return fmt.Errorf("request to request-audit a nil request") } @@ -49,28 +45,31 @@ func (f *AuditFormatter) FormatRequest( return errwrap.Wrapf("error fetching salt: {{err}}", err) } + // Set these to the input values at first + auth := in.Auth + req := in.Request + if !config.Raw { // Before we copy the structure we must nil out some data // otherwise we will cause reflection to panic and die - if req.Connection != nil && req.Connection.ConnState != nil { - origReq := req - origState := req.Connection.ConnState - req.Connection.ConnState = nil + if in.Request.Connection != nil && in.Request.Connection.ConnState != nil { + origState := in.Request.Connection.ConnState + in.Request.Connection.ConnState = nil defer func() { - origReq.Connection.ConnState = origState + in.Request.Connection.ConnState = origState }() } // Copy the auth structure - if auth != nil { - cp, err := copystructure.Copy(auth) + if in.Auth != nil { + cp, err := copystructure.Copy(in.Auth) if err != nil { return err } auth = cp.(*logical.Auth) } - cp, err := copystructure.Copy(req) + cp, err := copystructure.Copy(in.Request) if err != nil { return err } @@ -83,7 +82,7 @@ func (f *AuditFormatter) FormatRequest( if !config.HMACAccessor && auth.Accessor != "" { authAccessor = auth.Accessor } - if err := Hash(salt, auth); err != nil { + if err := Hash(salt, auth, nil); err != nil { return err } if authAccessor != "" { @@ -96,7 +95,7 @@ func (f *AuditFormatter) FormatRequest( if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" { clientTokenAccessor = req.ClientTokenAccessor } - if err := Hash(salt, req); err != nil { + if err := Hash(salt, req, in.NonHMACReqDataKeys); err != nil { return err } if clientTokenAccessor != "" { @@ -109,8 +108,8 @@ func (f *AuditFormatter) FormatRequest( auth = new(logical.Auth) } var errString string - if inErr != nil { - errString = inErr.Error() + if in.OuterErr != nil { + errString = in.OuterErr.Error() } reqEntry := &AuditRequestEntry{ @@ -152,15 +151,8 @@ func (f *AuditFormatter) FormatRequest( return f.AuditFormatWriter.WriteRequest(w, reqEntry) } -func (f *AuditFormatter) FormatResponse( - w io.Writer, - config FormatterConfig, - auth *logical.Auth, - req *logical.Request, - resp *logical.Response, - inErr error) error { - - if req == nil { +func (f *AuditFormatter) FormatResponse(w io.Writer, config FormatterConfig, in *LogInput) error { + if in == nil || in.Request == nil { return fmt.Errorf("request to response-audit a nil request") } @@ -177,35 +169,39 @@ func (f *AuditFormatter) FormatResponse( return errwrap.Wrapf("error fetching salt: {{err}}", err) } + // Set these to the input values at first + auth := in.Auth + req := in.Request + resp := in.Response + if !config.Raw { // Before we copy the structure we must nil out some data // otherwise we will cause reflection to panic and die - if req.Connection != nil && req.Connection.ConnState != nil { - origReq := req - origState := req.Connection.ConnState - req.Connection.ConnState = nil + if in.Request.Connection != nil && in.Request.Connection.ConnState != nil { + origState := in.Request.Connection.ConnState + in.Request.Connection.ConnState = nil defer func() { - origReq.Connection.ConnState = origState + in.Request.Connection.ConnState = origState }() } // Copy the auth structure - if auth != nil { - cp, err := copystructure.Copy(auth) + if in.Auth != nil { + cp, err := copystructure.Copy(in.Auth) if err != nil { return err } auth = cp.(*logical.Auth) } - cp, err := copystructure.Copy(req) + cp, err := copystructure.Copy(in.Request) if err != nil { return err } req = cp.(*logical.Request) - if resp != nil { - cp, err := copystructure.Copy(resp) + if in.Response != nil { + cp, err := copystructure.Copy(in.Response) if err != nil { return err } @@ -220,7 +216,7 @@ func (f *AuditFormatter) FormatResponse( if !config.HMACAccessor && auth.Accessor != "" { accessor = auth.Accessor } - if err := Hash(salt, auth); err != nil { + if err := Hash(salt, auth, nil); err != nil { return err } if accessor != "" { @@ -233,7 +229,7 @@ func (f *AuditFormatter) FormatResponse( if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" { clientTokenAccessor = req.ClientTokenAccessor } - if err := Hash(salt, req); err != nil { + if err := Hash(salt, req, in.NonHMACReqDataKeys); err != nil { return err } if clientTokenAccessor != "" { @@ -250,7 +246,7 @@ func (f *AuditFormatter) FormatResponse( wrappedAccessor = resp.WrapInfo.WrappedAccessor wrappingAccessor = resp.WrapInfo.Accessor } - if err := Hash(salt, resp); err != nil { + if err := Hash(salt, resp, in.NonHMACRespDataKeys); err != nil { return err } if accessor != "" { @@ -273,8 +269,8 @@ func (f *AuditFormatter) FormatResponse( resp = new(logical.Response) } var errString string - if inErr != nil { - errString = inErr.Error() + if in.OuterErr != nil { + errString = in.OuterErr.Error() } var respAuth *AuditAuth @@ -358,7 +354,7 @@ func (f *AuditFormatter) FormatResponse( return f.AuditFormatWriter.WriteResponse(w, respEntry) } -// AuditRequest is the structure of a request audit log entry in Audit. +// AuditRequestEntry is the structure of a request audit log entry in Audit. type AuditRequestEntry struct { Time string `json:"time,omitempty"` Type string `json:"type"` diff --git a/audit/format_json_test.go b/audit/format_json_test.go index 688ae3d90b..90c44a09fa 100644 --- a/audit/format_json_test.go +++ b/audit/format_json_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" + "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/logical" @@ -84,7 +85,12 @@ func TestFormatJSON_formatRequest(t *testing.T) { config := FormatterConfig{ HMACAccessor: false, } - if err := formatter.FormatRequest(&buf, config, tc.Auth, tc.Req, tc.Err); err != nil { + in := &LogInput{ + Auth: tc.Auth, + Request: tc.Req, + OuterErr: tc.Err, + } + if err := formatter.FormatRequest(&buf, config, in); err != nil { t.Fatalf("bad: %s\nerr: %s", name, err) } diff --git a/audit/format_jsonx_test.go b/audit/format_jsonx_test.go index 1ec6d83c2e..c9096430b7 100644 --- a/audit/format_jsonx_test.go +++ b/audit/format_jsonx_test.go @@ -89,7 +89,12 @@ func TestFormatJSONx_formatRequest(t *testing.T) { OmitTime: true, HMACAccessor: false, } - if err := formatter.FormatRequest(&buf, config, tc.Auth, tc.Req, tc.Err); err != nil { + in := &LogInput{ + Auth: tc.Auth, + Request: tc.Req, + OuterErr: tc.Err, + } + if err := formatter.FormatRequest(&buf, config, in); err != nil { t.Fatalf("bad: %s\nerr: %s", name, err) } diff --git a/audit/format_test.go b/audit/format_test.go index 5390229db2..54da56154f 100644 --- a/audit/format_test.go +++ b/audit/format_test.go @@ -40,10 +40,14 @@ func TestFormatRequestErrors(t *testing.T) { AuditFormatWriter: &noopFormatWriter{}, } - if err := formatter.FormatRequest(ioutil.Discard, config, nil, nil, nil); err == nil { + if err := formatter.FormatRequest(ioutil.Discard, config, &LogInput{}); err == nil { t.Fatal("expected error due to nil request") } - if err := formatter.FormatRequest(nil, config, nil, &logical.Request{}, nil); err == nil { + + in := &LogInput{ + Request: &logical.Request{}, + } + if err := formatter.FormatRequest(nil, config, in); err == nil { t.Fatal("expected error due to nil writer") } } @@ -54,10 +58,14 @@ func TestFormatResponseErrors(t *testing.T) { AuditFormatWriter: &noopFormatWriter{}, } - if err := formatter.FormatResponse(ioutil.Discard, config, nil, nil, nil, nil); err == nil { + if err := formatter.FormatResponse(ioutil.Discard, config, &LogInput{}); err == nil { t.Fatal("expected error due to nil request") } - if err := formatter.FormatResponse(nil, config, nil, &logical.Request{}, nil, nil); err == nil { + + in := &LogInput{ + Request: &logical.Request{}, + } + if err := formatter.FormatResponse(nil, config, in); err == nil { t.Fatal("expected error due to nil writer") } } diff --git a/audit/formatter.go b/audit/formatter.go index 3c1748f53a..d296e55b77 100644 --- a/audit/formatter.go +++ b/audit/formatter.go @@ -2,8 +2,6 @@ package audit import ( "io" - - "github.com/hashicorp/vault/logical" ) // Formatter is an interface that is responsible for formating a @@ -12,8 +10,8 @@ import ( // // It is recommended that you pass data through Hash prior to formatting it. type Formatter interface { - FormatRequest(io.Writer, FormatterConfig, *logical.Auth, *logical.Request, error) error - FormatResponse(io.Writer, FormatterConfig, *logical.Auth, *logical.Request, *logical.Response, error) error + FormatRequest(io.Writer, FormatterConfig, *LogInput) error + FormatResponse(io.Writer, FormatterConfig, *LogInput) error } type FormatterConfig struct { diff --git a/audit/hashstructure.go b/audit/hashstructure.go index d3925721be..be1aad97ef 100644 --- a/audit/hashstructure.go +++ b/audit/hashstructure.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hashicorp/vault/helper/salt" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/wrapping" "github.com/hashicorp/vault/logical" "github.com/mitchellh/copystructure" @@ -23,7 +24,7 @@ func HashString(salter *salt.Salt, data string) string { // it will be passed through. // // The structure is modified in-place. -func Hash(salter *salt.Salt, raw interface{}) error { +func Hash(salter *salt.Salt, raw interface{}, nonHMACDataKeys []string) error { fn := salter.GetIdentifiedHMAC switch s := raw.(type) { @@ -43,7 +44,7 @@ func Hash(salter *salt.Salt, raw interface{}) error { return nil } if s.Auth != nil { - if err := Hash(salter, s.Auth); err != nil { + if err := Hash(salter, s.Auth, nil); err != nil { return err } } @@ -56,7 +57,7 @@ func Hash(salter *salt.Salt, raw interface{}) error { s.ClientTokenAccessor = fn(s.ClientTokenAccessor) } - data, err := HashStructure(s.Data, fn) + data, err := HashStructure(s.Data, fn, nonHMACDataKeys) if err != nil { return err } @@ -69,18 +70,18 @@ func Hash(salter *salt.Salt, raw interface{}) error { } if s.Auth != nil { - if err := Hash(salter, s.Auth); err != nil { + if err := Hash(salter, s.Auth, nil); err != nil { return err } } if s.WrapInfo != nil { - if err := Hash(salter, s.WrapInfo); err != nil { + if err := Hash(salter, s.WrapInfo, nil); err != nil { return err } } - data, err := HashStructure(s.Data, fn) + data, err := HashStructure(s.Data, fn, nonHMACDataKeys) if err != nil { return err } @@ -107,13 +108,13 @@ func Hash(salter *salt.Salt, raw interface{}) error { // the structure. Only _values_ are hashed: keys of objects are not. // // For the HashCallback, see the built-in HashCallbacks below. -func HashStructure(s interface{}, cb HashCallback) (interface{}, error) { +func HashStructure(s interface{}, cb HashCallback, ignoredKeys []string) (interface{}, error) { s, err := copystructure.Copy(s) if err != nil { return nil, err } - walker := &hashWalker{Callback: cb} + walker := &hashWalker{Callback: cb, IgnoredKeys: ignoredKeys} if err := reflectwalk.Walk(s, walker); err != nil { return nil, err } @@ -134,6 +135,9 @@ type hashWalker struct { // immediately and the error returned. Callback HashCallback + // IgnoreKeys are the keys that wont have the HashCallback applied + IgnoredKeys []string + key []string lastValue reflect.Value loc reflectwalk.Location @@ -247,6 +251,12 @@ func (w *hashWalker) Primitive(v reflect.Value) error { return nil } + // See if the current key is part of the ignored keys + currentKey := w.key[len(w.key)-1] + if strutil.StrListContains(w.IgnoredKeys, currentKey) { + return nil + } + replaceVal := w.Callback(v.String()) resultVal := reflect.ValueOf(replaceVal) diff --git a/audit/hashstructure_test.go b/audit/hashstructure_test.go index 9f37986d8e..21cf05b1e9 100644 --- a/audit/hashstructure_test.go +++ b/audit/hashstructure_test.go @@ -116,32 +116,37 @@ func TestHash(t *testing.T) { now := time.Now() cases := []struct { - Input interface{} - Output interface{} + Input interface{} + Output interface{} + NonHMACDataKeys []string }{ { &logical.Auth{ClientToken: "foo"}, &logical.Auth{ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a"}, + nil, }, { &logical.Request{ Data: map[string]interface{}{ "foo": "bar", + "baz": "foobar", "private_key_type": certutil.PrivateKeyType("rsa"), }, }, &logical.Request{ Data: map[string]interface{}{ "foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", + "baz": "foobar", "private_key_type": "hmac-sha256:995230dca56fffd310ff591aa404aab52b2abb41703c787cfa829eceb4595bf1", }, }, + []string{"baz"}, }, { &logical.Response{ Data: map[string]interface{}{ "foo": "bar", - + "baz": "foobar", // Responses can contain time values, so test that with // a known fixed value. "bar": now, @@ -157,6 +162,7 @@ func TestHash(t *testing.T) { &logical.Response{ Data: map[string]interface{}{ "foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", + "baz": "foobar", "bar": now.Format(time.RFC3339Nano), }, WrapInfo: &wrapping.ResponseWrapInfo{ @@ -167,10 +173,12 @@ func TestHash(t *testing.T) { WrappedAccessor: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", }, }, + []string{"baz"}, }, { "foo", "foo", + nil, }, { &logical.Auth{ @@ -189,6 +197,7 @@ func TestHash(t *testing.T) { ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", }, + nil, }, } @@ -206,16 +215,16 @@ func TestHash(t *testing.T) { } for _, tc := range cases { input := fmt.Sprintf("%#v", tc.Input) - if err := Hash(localSalt, tc.Input); err != nil { + if err := Hash(localSalt, tc.Input, tc.NonHMACDataKeys); err != nil { t.Fatalf("err: %s\n\n%s", err, input) } if _, ok := tc.Input.(*logical.Response); ok { if !reflect.DeepEqual(tc.Input.(*logical.Response).WrapInfo, tc.Output.(*logical.Response).WrapInfo) { - t.Fatalf("bad:\nInput:\n%s\nTest case input:\n%#v\nTest case output\n%#v", input, tc.Input.(*logical.Response).WrapInfo, tc.Output.(*logical.Response).WrapInfo) + t.Fatalf("bad:\nInput:\n%s\nTest case input:\n%#v\nTest case output:\n%#v", input, tc.Input.(*logical.Response).WrapInfo, tc.Output.(*logical.Response).WrapInfo) } } if !reflect.DeepEqual(tc.Input, tc.Output) { - t.Fatalf("bad:\nInput:\n%s\nTest case input:\n%#v\nTest case output\n%#v", input, tc.Input, tc.Output) + t.Fatalf("bad:\nInput:\n%s\nTest case input:\n%#v\nTest case output:\n%#v", input, tc.Input, tc.Output) } } } @@ -249,7 +258,7 @@ func TestHashWalker(t *testing.T) { for _, tc := range cases { output, err := HashStructure(tc.Input, func(string) string { return replaceText - }) + }, []string{}) if err != nil { t.Fatalf("err: %s\n\n%#v", err, tc.Input) } diff --git a/builtin/audit/file/backend.go b/builtin/audit/file/backend.go index 37fda61738..7bf066d1c0 100644 --- a/builtin/audit/file/backend.go +++ b/builtin/audit/file/backend.go @@ -141,6 +141,8 @@ type Backend struct { saltView logical.Storage } +var _ audit.Backend = (*Backend)(nil) + func (b *Backend) Salt() (*salt.Salt, error) { b.saltMutex.RLock() if b.salt != nil { @@ -169,27 +171,22 @@ func (b *Backend) GetHash(data string) (string, error) { return audit.HashString(salt, data), nil } -func (b *Backend) LogRequest( - _ context.Context, - auth *logical.Auth, - req *logical.Request, - outerErr error) error { - +func (b *Backend) LogRequest(_ context.Context, in *audit.LogInput) error { b.fileLock.Lock() defer b.fileLock.Unlock() switch b.path { case "stdout": - return b.formatter.FormatRequest(os.Stdout, b.formatConfig, auth, req, outerErr) + return b.formatter.FormatRequest(os.Stdout, b.formatConfig, in) case "discard": - return b.formatter.FormatRequest(ioutil.Discard, b.formatConfig, auth, req, outerErr) + return b.formatter.FormatRequest(ioutil.Discard, b.formatConfig, in) } if err := b.open(); err != nil { return err } - if err := b.formatter.FormatRequest(b.f, b.formatConfig, auth, req, outerErr); err == nil { + if err := b.formatter.FormatRequest(b.f, b.formatConfig, in); err == nil { return nil } @@ -201,31 +198,26 @@ func (b *Backend) LogRequest( return err } - return b.formatter.FormatRequest(b.f, b.formatConfig, auth, req, outerErr) + return b.formatter.FormatRequest(b.f, b.formatConfig, in) } -func (b *Backend) LogResponse( - _ context.Context, - auth *logical.Auth, - req *logical.Request, - resp *logical.Response, - err error) error { +func (b *Backend) LogResponse(_ context.Context, in *audit.LogInput) error { b.fileLock.Lock() defer b.fileLock.Unlock() switch b.path { case "stdout": - return b.formatter.FormatResponse(os.Stdout, b.formatConfig, auth, req, resp, err) + return b.formatter.FormatResponse(os.Stdout, b.formatConfig, in) case "discard": - return b.formatter.FormatResponse(ioutil.Discard, b.formatConfig, auth, req, resp, err) + return b.formatter.FormatResponse(ioutil.Discard, b.formatConfig, in) } if err := b.open(); err != nil { return err } - if err := b.formatter.FormatResponse(b.f, b.formatConfig, auth, req, resp, err); err == nil { + if err := b.formatter.FormatResponse(b.f, b.formatConfig, in); err == nil { return nil } @@ -237,7 +229,7 @@ func (b *Backend) LogResponse( return err } - return b.formatter.FormatResponse(b.f, b.formatConfig, auth, req, resp, err) + return b.formatter.FormatResponse(b.f, b.formatConfig, in) } // The file lock must be held before calling this diff --git a/builtin/audit/socket/backend.go b/builtin/audit/socket/backend.go index bfe7fbe57e..d99d28f574 100644 --- a/builtin/audit/socket/backend.go +++ b/builtin/audit/socket/backend.go @@ -121,6 +121,8 @@ type Backend struct { saltView logical.Storage } +var _ audit.Backend = (*Backend)(nil) + func (b *Backend) GetHash(data string) (string, error) { salt, err := b.Salt() if err != nil { @@ -129,9 +131,9 @@ func (b *Backend) GetHash(data string) (string, error) { return audit.HashString(salt, data), nil } -func (b *Backend) LogRequest(ctx context.Context, auth *logical.Auth, req *logical.Request, outerErr error) error { +func (b *Backend) LogRequest(ctx context.Context, in *audit.LogInput) error { var buf bytes.Buffer - if err := b.formatter.FormatRequest(&buf, b.formatConfig, auth, req, outerErr); err != nil { + if err := b.formatter.FormatRequest(&buf, b.formatConfig, in); err != nil { return err } @@ -152,10 +154,9 @@ func (b *Backend) LogRequest(ctx context.Context, auth *logical.Auth, req *logic return err } -func (b *Backend) LogResponse(ctx context.Context, auth *logical.Auth, req *logical.Request, - resp *logical.Response, outerErr error) error { +func (b *Backend) LogResponse(ctx context.Context, in *audit.LogInput) error { var buf bytes.Buffer - if err := b.formatter.FormatResponse(&buf, b.formatConfig, auth, req, resp, outerErr); err != nil { + if err := b.formatter.FormatResponse(&buf, b.formatConfig, in); err != nil { return err } diff --git a/builtin/audit/syslog/backend.go b/builtin/audit/syslog/backend.go index 6e45a06984..2df25f2298 100644 --- a/builtin/audit/syslog/backend.go +++ b/builtin/audit/syslog/backend.go @@ -108,6 +108,8 @@ type Backend struct { saltView logical.Storage } +var _ audit.Backend = (*Backend)(nil) + func (b *Backend) GetHash(data string) (string, error) { salt, err := b.Salt() if err != nil { @@ -116,9 +118,9 @@ func (b *Backend) GetHash(data string) (string, error) { return audit.HashString(salt, data), nil } -func (b *Backend) LogRequest(_ context.Context, auth *logical.Auth, req *logical.Request, outerErr error) error { +func (b *Backend) LogRequest(_ context.Context, in *audit.LogInput) error { var buf bytes.Buffer - if err := b.formatter.FormatRequest(&buf, b.formatConfig, auth, req, outerErr); err != nil { + if err := b.formatter.FormatRequest(&buf, b.formatConfig, in); err != nil { return err } @@ -127,14 +129,14 @@ func (b *Backend) LogRequest(_ context.Context, auth *logical.Auth, req *logical return err } -func (b *Backend) LogResponse(_ context.Context, auth *logical.Auth, req *logical.Request, resp *logical.Response, err error) error { +func (b *Backend) LogResponse(_ context.Context, in *audit.LogInput) error { var buf bytes.Buffer - if err := b.formatter.FormatResponse(&buf, b.formatConfig, auth, req, resp, err); err != nil { + if err := b.formatter.FormatResponse(&buf, b.formatConfig, in); err != nil { return err } // Write out to syslog - _, err = b.logger.Write(buf.Bytes()) + _, err := b.logger.Write(buf.Bytes()) return err } diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 75f2981b0a..e075c7e9a9 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -1072,3 +1072,90 @@ func TestSysTuneMount(t *testing.T) { t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, structs.Map(result)) } } + +func TestSysTuneMount_nonHMACKeys(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + TestServerAuth(t, addr, token) + + // Mount-tune the audit_non_hmac_request_keys + resp := testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "audit_non_hmac_request_keys": "foo", + }) + testResponseStatus(t, resp, 204) + + // Mount-tune the audit_non_hmac_response_keys + resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "audit_non_hmac_response_keys": "bar", + }) + testResponseStatus(t, resp, 204) + + // Check results + resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") + testResponseStatus(t, resp, 200) + + actual := map[string]interface{}{} + expected := map[string]interface{}{ + "lease_id": "", + "renewable": false, + "lease_duration": json.Number("0"), + "wrap_info": nil, + "warnings": nil, + "auth": nil, + "data": map[string]interface{}{ + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + "audit_non_hmac_request_keys": []interface{}{"foo"}, + "audit_non_hmac_response_keys": []interface{}{"bar"}, + }, + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + "audit_non_hmac_request_keys": []interface{}{"foo"}, + "audit_non_hmac_response_keys": []interface{}{"bar"}, + } + testResponseBody(t, resp, &actual) + expected["request_id"] = actual["request_id"] + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) + } + + // Unset those mount tune values + resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "audit_non_hmac_request_keys": "", + }) + testResponseStatus(t, resp, 204) + + resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ + "audit_non_hmac_response_keys": "", + }) + + // Check results + resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") + testResponseStatus(t, resp, 200) + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "lease_id": "", + "renewable": false, + "lease_duration": json.Number("0"), + "wrap_info": nil, + "warnings": nil, + "auth": nil, + "data": map[string]interface{}{ + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + }, + "default_lease_ttl": json.Number("2764800"), + "max_lease_ttl": json.Number("2764800"), + "force_no_cache": false, + } + testResponseBody(t, resp, &actual) + expected["request_id"] = actual["request_id"] + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) + } +} diff --git a/vault/audit.go b/vault/audit.go index 1f1bdd2839..3bd9141e05 100644 --- a/vault/audit.go +++ b/vault/audit.go @@ -6,13 +6,7 @@ import ( "errors" "fmt" "strings" - "sync" - "time" - log "github.com/mgutz/logxi/v1" - - "github.com/armon/go-metrics" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/jsonutil" @@ -461,191 +455,3 @@ func defaultAuditTable() *MountTable { } return table } - -type backendEntry struct { - backend audit.Backend - view *BarrierView -} - -// AuditBroker is used to provide a single ingest interface to auditable -// events given that multiple backends may be configured. -type AuditBroker struct { - sync.RWMutex - backends map[string]backendEntry - logger log.Logger -} - -// NewAuditBroker creates a new audit broker -func NewAuditBroker(log log.Logger) *AuditBroker { - b := &AuditBroker{ - backends: make(map[string]backendEntry), - logger: log, - } - return b -} - -// Register is used to add new audit backend to the broker -func (a *AuditBroker) Register(name string, b audit.Backend, v *BarrierView) { - a.Lock() - defer a.Unlock() - a.backends[name] = backendEntry{ - backend: b, - view: v, - } -} - -// Deregister is used to remove an audit backend from the broker -func (a *AuditBroker) Deregister(name string) { - a.Lock() - defer a.Unlock() - delete(a.backends, name) -} - -// IsRegistered is used to check if a given audit backend is registered -func (a *AuditBroker) IsRegistered(name string) bool { - a.RLock() - defer a.RUnlock() - _, ok := a.backends[name] - return ok -} - -// GetHash returns a hash using the salt of the given backend -func (a *AuditBroker) GetHash(name string, input string) (string, error) { - a.RLock() - defer a.RUnlock() - be, ok := a.backends[name] - if !ok { - return "", fmt.Errorf("unknown audit backend %s", name) - } - - return be.backend.GetHash(input) -} - -// LogRequest is used to ensure all the audit backends have an opportunity to -// log the given request and that *at least one* succeeds. -func (a *AuditBroker) LogRequest(ctx context.Context, auth *logical.Auth, req *logical.Request, headersConfig *AuditedHeadersConfig, outerErr error) (ret error) { - defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now()) - a.RLock() - defer a.RUnlock() - - var retErr *multierror.Error - - defer func() { - if r := recover(); r != nil { - a.logger.Error("audit: panic during logging", "request_path", req.Path, "error", r) - retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log")) - } - - ret = retErr.ErrorOrNil() - failure := float32(0.0) - if ret != nil { - failure = 1.0 - } - metrics.IncrCounter([]string{"audit", "log_request_failure"}, failure) - }() - - // All logged requests must have an identifier - //if req.ID == "" { - // a.logger.Error("audit: missing identifier in request object", "request_path", req.Path) - // retErr = multierror.Append(retErr, fmt.Errorf("missing identifier in request object: %s", req.Path)) - // return - //} - - headers := req.Headers - defer func() { - req.Headers = headers - }() - - // Ensure at least one backend logs - anyLogged := false - for name, be := range a.backends { - req.Headers = nil - transHeaders, thErr := headersConfig.ApplyConfig(headers, be.backend.GetHash) - if thErr != nil { - a.logger.Error("audit: backend failed to include headers", "backend", name, "error", thErr) - continue - } - req.Headers = transHeaders - - start := time.Now() - lrErr := be.backend.LogRequest(ctx, auth, req, outerErr) - metrics.MeasureSince([]string{"audit", name, "log_request"}, start) - if lrErr != nil { - a.logger.Error("audit: backend failed to log request", "backend", name, "error", lrErr) - } else { - anyLogged = true - } - } - if !anyLogged && len(a.backends) > 0 { - retErr = multierror.Append(retErr, fmt.Errorf("no audit backend succeeded in logging the request")) - } - - return retErr.ErrorOrNil() -} - -// LogResponse is used to ensure all the audit backends have an opportunity to -// log the given response and that *at least one* succeeds. -func (a *AuditBroker) LogResponse(ctx context.Context, auth *logical.Auth, req *logical.Request, - resp *logical.Response, headersConfig *AuditedHeadersConfig, err error) (ret error) { - defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now()) - a.RLock() - defer a.RUnlock() - - var retErr *multierror.Error - - defer func() { - if r := recover(); r != nil { - a.logger.Error("audit: panic during logging", "request_path", req.Path, "error", r) - retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log")) - } - - ret = retErr.ErrorOrNil() - - failure := float32(0.0) - if ret != nil { - failure = 1.0 - } - metrics.IncrCounter([]string{"audit", "log_response_failure"}, failure) - }() - - headers := req.Headers - defer func() { - req.Headers = headers - }() - - // Ensure at least one backend logs - anyLogged := false - for name, be := range a.backends { - req.Headers = nil - transHeaders, thErr := headersConfig.ApplyConfig(headers, be.backend.GetHash) - if thErr != nil { - a.logger.Error("audit: backend failed to include headers", "backend", name, "error", thErr) - continue - } - req.Headers = transHeaders - - start := time.Now() - lrErr := be.backend.LogResponse(ctx, auth, req, resp, err) - metrics.MeasureSince([]string{"audit", name, "log_response"}, start) - if lrErr != nil { - a.logger.Error("audit: backend failed to log response", "backend", name, "error", lrErr) - } else { - anyLogged = true - } - } - if !anyLogged && len(a.backends) > 0 { - retErr = multierror.Append(retErr, fmt.Errorf("no audit backend succeeded in logging the response")) - } - - return retErr.ErrorOrNil() -} - -func (a *AuditBroker) Invalidate(ctx context.Context, key string) { - // For now we ignore the key as this would only apply to salts. We just - // sort of brute force it on each one. - a.Lock() - defer a.Unlock() - for _, be := range a.backends { - be.backend.Invalidate(ctx) - } -} diff --git a/vault/audit_broker.go b/vault/audit_broker.go new file mode 100644 index 0000000000..3584f8a6f6 --- /dev/null +++ b/vault/audit_broker.go @@ -0,0 +1,200 @@ +package vault + +import ( + "context" + "fmt" + "sync" + "time" + + metrics "github.com/armon/go-metrics" + multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/audit" + log "github.com/mgutz/logxi/v1" +) + +type backendEntry struct { + backend audit.Backend + view *BarrierView +} + +// AuditBroker is used to provide a single ingest interface to auditable +// events given that multiple backends may be configured. +type AuditBroker struct { + sync.RWMutex + backends map[string]backendEntry + logger log.Logger +} + +// NewAuditBroker creates a new audit broker +func NewAuditBroker(log log.Logger) *AuditBroker { + b := &AuditBroker{ + backends: make(map[string]backendEntry), + logger: log, + } + return b +} + +// Register is used to add new audit backend to the broker +func (a *AuditBroker) Register(name string, b audit.Backend, v *BarrierView) { + a.Lock() + defer a.Unlock() + a.backends[name] = backendEntry{ + backend: b, + view: v, + } +} + +// Deregister is used to remove an audit backend from the broker +func (a *AuditBroker) Deregister(name string) { + a.Lock() + defer a.Unlock() + delete(a.backends, name) +} + +// IsRegistered is used to check if a given audit backend is registered +func (a *AuditBroker) IsRegistered(name string) bool { + a.RLock() + defer a.RUnlock() + _, ok := a.backends[name] + return ok +} + +// GetHash returns a hash using the salt of the given backend +func (a *AuditBroker) GetHash(name string, input string) (string, error) { + a.RLock() + defer a.RUnlock() + be, ok := a.backends[name] + if !ok { + return "", fmt.Errorf("unknown audit backend %s", name) + } + + return be.backend.GetHash(input) +} + +// LogRequest is used to ensure all the audit backends have an opportunity to +// log the given request and that *at least one* succeeds. +func (a *AuditBroker) LogRequest(ctx context.Context, in *audit.LogInput, headersConfig *AuditedHeadersConfig) (ret error) { + defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now()) + a.RLock() + defer a.RUnlock() + + var retErr *multierror.Error + + defer func() { + if r := recover(); r != nil { + a.logger.Error("audit: panic during logging", "request_path", in.Request.Path, "error", r) + retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log")) + } + + ret = retErr.ErrorOrNil() + failure := float32(0.0) + if ret != nil { + failure = 1.0 + } + metrics.IncrCounter([]string{"audit", "log_request_failure"}, failure) + }() + + // All logged requests must have an identifier + //if req.ID == "" { + // a.logger.Error("audit: missing identifier in request object", "request_path", req.Path) + // retErr = multierror.Append(retErr, fmt.Errorf("missing identifier in request object: %s", req.Path)) + // return + //} + + headers := in.Request.Headers + defer func() { + in.Request.Headers = headers + }() + + // Ensure at least one backend logs + anyLogged := false + for name, be := range a.backends { + in.Request.Headers = nil + transHeaders, thErr := headersConfig.ApplyConfig(headers, be.backend.GetHash) + if thErr != nil { + a.logger.Error("audit: backend failed to include headers", "backend", name, "error", thErr) + continue + } + in.Request.Headers = transHeaders + + start := time.Now() + lrErr := be.backend.LogRequest(ctx, in) + metrics.MeasureSince([]string{"audit", name, "log_request"}, start) + if lrErr != nil { + a.logger.Error("audit: backend failed to log request", "backend", name, "error", lrErr) + } else { + anyLogged = true + } + } + if !anyLogged && len(a.backends) > 0 { + retErr = multierror.Append(retErr, fmt.Errorf("no audit backend succeeded in logging the request")) + } + + return retErr.ErrorOrNil() +} + +// LogResponse is used to ensure all the audit backends have an opportunity to +// log the given response and that *at least one* succeeds. +func (a *AuditBroker) LogResponse(ctx context.Context, in *audit.LogInput, headersConfig *AuditedHeadersConfig) (ret error) { + defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now()) + a.RLock() + defer a.RUnlock() + + var retErr *multierror.Error + + defer func() { + if r := recover(); r != nil { + a.logger.Error("audit: panic during logging", "request_path", in.Request.Path, "error", r) + retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log")) + } + + ret = retErr.ErrorOrNil() + + failure := float32(0.0) + if ret != nil { + failure = 1.0 + } + metrics.IncrCounter([]string{"audit", "log_response_failure"}, failure) + }() + + headers := in.Request.Headers + defer func() { + in.Request.Headers = headers + }() + + // Ensure at least one backend logs + anyLogged := false + for name, be := range a.backends { + in.Request.Headers = nil + transHeaders, thErr := headersConfig.ApplyConfig(headers, be.backend.GetHash) + if thErr != nil { + a.logger.Error("audit: backend failed to include headers", "backend", name, "error", thErr) + continue + } + in.Request.Headers = transHeaders + + start := time.Now() + lrErr := be.backend.LogResponse(ctx, in) + metrics.MeasureSince([]string{"audit", name, "log_response"}, start) + if lrErr != nil { + a.logger.Error("audit: backend failed to log response", "backend", name, "error", lrErr) + } else { + anyLogged = true + } + } + if !anyLogged && len(a.backends) > 0 { + retErr = multierror.Append(retErr, fmt.Errorf("no audit backend succeeded in logging the response")) + } + + return retErr.ErrorOrNil() +} + +func (a *AuditBroker) Invalidate(ctx context.Context, key string) { + // For now we ignore the key as this would only apply to salts. We just + // sort of brute force it on each one. + a.Lock() + defer a.Unlock() + for _, be := range a.backends { + be.backend.Invalidate(ctx) + } +} diff --git a/vault/audit_test.go b/vault/audit_test.go index b2f048fd0f..8ea249ce8b 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -23,36 +23,46 @@ import ( ) type NoopAudit struct { - Config *audit.BackendConfig - ReqErr error - ReqAuth []*logical.Auth - Req []*logical.Request - ReqHeaders []map[string][]string - ReqErrs []error + Config *audit.BackendConfig + ReqErr error + ReqAuth []*logical.Auth + Req []*logical.Request + ReqHeaders []map[string][]string + ReqNonHMACKeys []string + ReqErrs []error - RespErr error - RespAuth []*logical.Auth - RespReq []*logical.Request - Resp []*logical.Response - RespErrs []error + RespErr error + RespAuth []*logical.Auth + RespReq []*logical.Request + Resp []*logical.Response + RespNonHMACKeys []string + RespReqNonHMACKeys []string + RespErrs []error salt *salt.Salt saltMutex sync.RWMutex } -func (n *NoopAudit) LogRequest(ctx context.Context, a *logical.Auth, r *logical.Request, err error) error { - n.ReqAuth = append(n.ReqAuth, a) - n.Req = append(n.Req, r) - n.ReqHeaders = append(n.ReqHeaders, r.Headers) - n.ReqErrs = append(n.ReqErrs, err) +func (n *NoopAudit) LogRequest(ctx context.Context, in *audit.LogInput) error { + n.ReqAuth = append(n.ReqAuth, in.Auth) + n.Req = append(n.Req, in.Request) + n.ReqHeaders = append(n.ReqHeaders, in.Request.Headers) + n.ReqNonHMACKeys = in.NonHMACReqDataKeys + n.ReqErrs = append(n.ReqErrs, in.OuterErr) return n.ReqErr } -func (n *NoopAudit) LogResponse(ctx context.Context, a *logical.Auth, r *logical.Request, re *logical.Response, err error) error { - n.RespAuth = append(n.RespAuth, a) - n.RespReq = append(n.RespReq, r) - n.Resp = append(n.Resp, re) - n.RespErrs = append(n.RespErrs, err) +func (n *NoopAudit) LogResponse(ctx context.Context, in *audit.LogInput) error { + n.RespAuth = append(n.RespAuth, in.Auth) + n.RespReq = append(n.RespReq, in.Request) + n.Resp = append(n.Resp, in.Response) + n.RespErrs = append(n.RespErrs, in.OuterErr) + + if in.Response != nil { + n.RespNonHMACKeys = in.NonHMACRespDataKeys + n.RespReqNonHMACKeys = in.NonHMACReqDataKeys + } + return n.RespErr } @@ -442,7 +452,7 @@ func TestAuditBroker_LogRequest(t *testing.T) { Path: "sys/mounts", } - // Copy so we can verify nothing canged + // Copy so we can verify nothing changed authCopyRaw, err := copystructure.Copy(auth) if err != nil { t.Fatal(err) @@ -468,7 +478,12 @@ func TestAuditBroker_LogRequest(t *testing.T) { Headers: make(map[string]*auditedHeaderSettings), } - err = b.LogRequest(context.Background(), authCopy, reqCopy, headersConf, reqErrs) + logInput := &audit.LogInput{ + Auth: authCopy, + Request: reqCopy, + OuterErr: reqErrs, + } + err = b.LogRequest(context.Background(), logInput, headersConf) if err != nil { t.Fatalf("err: %v", err) } @@ -487,13 +502,17 @@ func TestAuditBroker_LogRequest(t *testing.T) { // Should still work with one failing backend a1.ReqErr = fmt.Errorf("failed") - if err := b.LogRequest(context.Background(), auth, req, headersConf, nil); err != nil { + logInput = &audit.LogInput{ + Auth: auth, + Request: req, + } + if err := b.LogRequest(context.Background(), logInput, headersConf); err != nil { t.Fatalf("err: %v", err) } // Should FAIL work with both failing backends a2.ReqErr = fmt.Errorf("failed") - if err := b.LogRequest(context.Background(), auth, req, headersConf, nil); !errwrap.Contains(err, "no audit backend succeeded in logging the request") { + if err := b.LogRequest(context.Background(), logInput, headersConf); !errwrap.Contains(err, "no audit backend succeeded in logging the request") { t.Fatalf("err: %v", err) } } @@ -555,7 +574,13 @@ func TestAuditBroker_LogResponse(t *testing.T) { Headers: make(map[string]*auditedHeaderSettings), } - err = b.LogResponse(context.Background(), authCopy, reqCopy, respCopy, headersConf, respErr) + logInput := &audit.LogInput{ + Auth: authCopy, + Request: reqCopy, + Response: respCopy, + OuterErr: respErr, + } + err = b.LogResponse(context.Background(), logInput, headersConf) if err != nil { t.Fatalf("err: %v", err) } @@ -577,14 +602,20 @@ func TestAuditBroker_LogResponse(t *testing.T) { // Should still work with one failing backend a1.RespErr = fmt.Errorf("failed") - err = b.LogResponse(context.Background(), auth, req, resp, headersConf, respErr) + logInput = &audit.LogInput{ + Auth: auth, + Request: req, + Response: resp, + OuterErr: respErr, + } + err = b.LogResponse(context.Background(), logInput, headersConf) if err != nil { t.Fatalf("err: %v", err) } // Should FAIL work with both failing backends a2.RespErr = fmt.Errorf("failed") - err = b.LogResponse(context.Background(), auth, req, resp, headersConf, respErr) + err = b.LogResponse(context.Background(), logInput, headersConf) if !strings.Contains(err.Error(), "no audit backend succeeded in logging the response") { t.Fatalf("err: %v", err) } @@ -632,7 +663,12 @@ func TestAuditBroker_AuditHeaders(t *testing.T) { headersConf.add(context.Background(), "X-Test-Header", false) headersConf.add(context.Background(), "X-Vault-Header", false) - err = b.LogRequest(context.Background(), auth, reqCopy, headersConf, respErr) + logInput := &audit.LogInput{ + Auth: auth, + Request: reqCopy, + OuterErr: respErr, + } + err = b.LogRequest(context.Background(), logInput, headersConf) if err != nil { t.Fatalf("err: %v", err) } @@ -650,14 +686,19 @@ func TestAuditBroker_AuditHeaders(t *testing.T) { // Should still work with one failing backend a1.ReqErr = fmt.Errorf("failed") - err = b.LogRequest(context.Background(), auth, req, headersConf, respErr) + logInput = &audit.LogInput{ + Auth: auth, + Request: req, + OuterErr: respErr, + } + err = b.LogRequest(context.Background(), logInput, headersConf) if err != nil { t.Fatalf("err: %v", err) } // Should FAIL work with both failing backends a2.ReqErr = fmt.Errorf("failed") - err = b.LogRequest(context.Background(), auth, req, headersConf, respErr) + err = b.LogRequest(context.Background(), logInput, headersConf) if !errwrap.Contains(err, "no audit backend succeeded in logging the request") { t.Fatalf("err: %v", err) } diff --git a/vault/auth.go b/vault/auth.go index a29edc4a46..487b94230a 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -94,9 +94,11 @@ func (c *Core) enableCredential(ctx context.Context, entry *MountEntry) error { } entry.Accessor = accessor } + // Sync values to the cache + entry.SyncCache() + viewPath := credentialBarrierPrefix + entry.UUID + "/" view := NewBarrierView(c.barrier, viewPath) - // Mark the view as read-only until the mounting is complete and // ensure that it is reset after. This ensures that there will be no // writes during the construction of the backend. @@ -347,6 +349,9 @@ func (c *Core) loadCredentials(ctx context.Context) error { entry.Accessor = accessor needPersist = true } + + // Sync values to the cache + entry.SyncCache() } if !needPersist { diff --git a/vault/core.go b/vault/core.go index 7dd7c83766..ef43d4dc65 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1343,7 +1343,11 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr EntityID: te.EntityID, } - if err := c.auditBroker.LogRequest(ctx, auth, req, c.auditedHeaders, nil); err != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + } + if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("core: failed to audit request", "request_path", req.Path, "error", err) retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue")) c.stateLock.RUnlock() @@ -1452,7 +1456,11 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) { EntityID: te.EntityID, } - if err := c.auditBroker.LogRequest(ctx, auth, req, c.auditedHeaders, nil); err != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + } + if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("core: failed to audit request", "request_path", req.Path, "error", err) retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue")) return retErr diff --git a/vault/core_test.go b/vault/core_test.go index c34596dda1..e59ff3e8d4 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -807,6 +807,106 @@ func TestCore_HandleRequest_AuditTrail(t *testing.T) { } } +func TestCore_HandleRequest_AuditTrail_noHMACKeys(t *testing.T) { + // Create a noop audit backend + var noop *NoopAudit + c, _, root := TestCoreUnsealed(t) + c.auditBackends["noop"] = func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) { + noop = &NoopAudit{ + Config: config, + } + return noop, nil + } + + // Specify some keys to not HMAC + req := logical.TestRequest(t, logical.UpdateOperation, "sys/mounts/secret/tune") + req.Data["audit_non_hmac_request_keys"] = "foo" + req.ClientToken = root + resp, err := c.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.UpdateOperation, "sys/mounts/secret/tune") + req.Data["audit_non_hmac_response_keys"] = "baz" + req.ClientToken = root + resp, err = c.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Enable the audit backend + req = logical.TestRequest(t, logical.UpdateOperation, "sys/audit/noop") + req.Data["type"] = "noop" + req.ClientToken = root + resp, err = c.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Make a request + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "secret/test", + Data: map[string]interface{}{ + "foo": "bar", + }, + ClientToken: root, + } + req.ClientToken = root + if _, err := c.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + // Check the audit trail on request and response + if len(noop.ReqAuth) != 1 { + t.Fatalf("bad: %#v", noop) + } + auth := noop.ReqAuth[0] + if auth.ClientToken != root { + t.Fatalf("bad client token: %#v", auth) + } + if len(auth.Policies) != 1 || auth.Policies[0] != "root" { + t.Fatalf("bad: %#v", auth) + } + if len(noop.Req) != 1 || !reflect.DeepEqual(noop.Req[0], req) { + t.Fatalf("Bad: %#v", noop.Req[0]) + } + if len(noop.ReqNonHMACKeys) != 1 || noop.ReqNonHMACKeys[0] != "foo" { + t.Fatalf("Bad: %#v", noop.ReqNonHMACKeys) + } + if len(noop.RespAuth) != 2 { + t.Fatalf("bad: %#v", noop) + } + if !reflect.DeepEqual(noop.RespAuth[1], auth) { + t.Fatalf("bad: %#v", auth) + } + if len(noop.RespReq) != 2 || !reflect.DeepEqual(noop.RespReq[1], req) { + t.Fatalf("Bad: %#v", noop.RespReq[1]) + } + if len(noop.Resp) != 2 || !reflect.DeepEqual(noop.Resp[1], resp) { + t.Fatalf("Bad: %#v", noop.Resp[1]) + } + + // Test for response keys + // Make a request + req = &logical.Request{ + Operation: logical.ReadOperation, + Path: "secret/test", + ClientToken: root, + } + req.ClientToken = root + if _, err := c.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + if len(noop.RespNonHMACKeys) != 1 || noop.RespNonHMACKeys[0] != "baz" { + t.Fatalf("Bad: %#v", noop.RespNonHMACKeys) + } + if len(noop.RespReqNonHMACKeys) != 1 || noop.RespReqNonHMACKeys[0] != "foo" { + t.Fatalf("Bad: %#v", noop.RespReqNonHMACKeys) + } +} + // Ensure we get a client token func TestCore_HandleLogin_AuditTrail(t *testing.T) { // Create a badass credential backend that always logs in as armon diff --git a/vault/logical_system.go b/vault/logical_system.go index 6d3d8d71fd..5087474156 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -257,6 +257,14 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["auth_desc"][0]), }, + "audit_non_hmac_request_keys": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["tune_audit_non_hmac_request_keys"][0]), + }, + "audit_non_hmac_response_keys": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["tune_audit_non_hmac_response_keys"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.handleAuthTuneRead, @@ -286,6 +294,14 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["auth_desc"][0]), }, + "audit_non_hmac_request_keys": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["tune_audit_non_hmac_request_keys"][0]), + }, + "audit_non_hmac_response_keys": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: strings.TrimSpace(sysHelp["tune_audit_non_hmac_response_keys"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -1675,6 +1691,14 @@ func (b *SystemBackend) handleTuneReadCommon(path string) (*logical.Response, er }, } + if rawVal, ok := mountEntry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { + resp.Data["audit_non_hmac_request_keys"] = rawVal.([]string) + } + + if rawVal, ok := mountEntry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { + resp.Data["audit_non_hmac_response_keys"] = rawVal.([]string) + } + return resp, nil } @@ -1808,6 +1832,58 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, } } + if rawVal, ok := data.GetOk("audit_non_hmac_request_keys"); ok { + auditNonHMACRequestKeys := rawVal.([]string) + + oldVal := mountEntry.Config.AuditNonHMACRequestKeys + mountEntry.Config.AuditNonHMACRequestKeys = auditNonHMACRequestKeys + + // Update the mount table + var err error + switch { + case strings.HasPrefix(path, "auth/"): + err = b.Core.persistAuth(ctx, b.Core.auth, mountEntry.Local) + default: + err = b.Core.persistMounts(ctx, b.Core.mounts, mountEntry.Local) + } + if err != nil { + mountEntry.Config.AuditNonHMACRequestKeys = oldVal + return handleError(err) + } + + mountEntry.SyncCache() + + if b.Core.logger.IsInfo() { + b.Core.logger.Info("core: mount tuning of audit_non_hmac_request_keys successful", "path", path) + } + } + + if rawVal, ok := data.GetOk("audit_non_hmac_response_keys"); ok { + auditNonHMACResponseKeys := rawVal.([]string) + + oldVal := mountEntry.Config.AuditNonHMACResponseKeys + mountEntry.Config.AuditNonHMACResponseKeys = auditNonHMACResponseKeys + + // Update the mount table + var err error + switch { + case strings.HasPrefix(path, "auth/"): + err = b.Core.persistAuth(ctx, b.Core.auth, mountEntry.Local) + default: + err = b.Core.persistMounts(ctx, b.Core.mounts, mountEntry.Local) + } + if err != nil { + mountEntry.Config.AuditNonHMACResponseKeys = oldVal + return handleError(err) + } + + mountEntry.SyncCache() + + if b.Core.logger.IsInfo() { + b.Core.logger.Info("core: mount tuning of audit_non_hmac_response_keys successful", "path", path) + } + } + return nil, nil } @@ -3099,6 +3175,14 @@ in the plugin catalog.`, `The max lease TTL for this mount.`, }, + "tune_audit_non_hmac_request_keys": { + `The list of keys in the request data object that will not be HMAC'ed by audit devices.`, + }, + + "tune_audit_non_hmac_response_keys": { + `The list of keys in the response data object that will not be HMAC'ed by audit devices.`, + }, + "remount": { "Move the mount point of an already-mounted backend.", ` diff --git a/vault/mount.go b/vault/mount.go index 1ffbdfc599..baf9a90c1f 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" "strings" + "sync" "time" "github.com/hashicorp/go-uuid" @@ -171,22 +172,29 @@ type MountEntry struct { Local bool `json:"local"` // Local mounts are not replicated or affected by replication SealWrap bool `json:"seal_wrap"` // Whether to wrap CSPs Tainted bool `json:"tainted,omitempty"` // Set as a Write-Ahead flag for unmount/remount + + // synthesizedConfigCache is used to cache configuration values + synthesizedConfigCache sync.Map } // MountConfig is used to hold settable options type MountConfig struct { - DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default - MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default + MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` } // APIMountConfig is an embedded struct of api.MountConfigInput type APIMountConfig struct { - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` } // Clone returns a deep copy of the mount entry @@ -198,6 +206,21 @@ func (e *MountEntry) Clone() (*MountEntry, error) { return cp.(*MountEntry), nil } +// SyncCache syncs tunable configuration values to the cache +func (e *MountEntry) SyncCache() { + if len(e.Config.AuditNonHMACRequestKeys) == 0 { + e.synthesizedConfigCache.Delete("audit_non_hmac_request_keys") + } else { + e.synthesizedConfigCache.Store("audit_non_hmac_request_keys", e.Config.AuditNonHMACRequestKeys) + } + + if len(e.Config.AuditNonHMACResponseKeys) == 0 { + e.synthesizedConfigCache.Delete("audit_non_hmac_response_keys") + } else { + e.synthesizedConfigCache.Store("audit_non_hmac_response_keys", e.Config.AuditNonHMACResponseKeys) + } +} + // Mount is used to mount a new backend to the mount table. func (c *Core) mount(ctx context.Context, entry *MountEntry) error { // Ensure we end the path in a slash @@ -245,6 +268,9 @@ func (c *Core) mountInternal(ctx context.Context, entry *MountEntry) error { } entry.Accessor = accessor } + // Sync values to the cache + entry.SyncCache() + viewPath := backendBarrierPrefix + entry.UUID + "/" view := NewBarrierView(c.barrier, viewPath) @@ -636,6 +662,9 @@ func (c *Core) loadMounts(ctx context.Context) error { entry.Accessor = accessor needPersist = true } + + // Sync values to the cache + entry.SyncCache() } // Done if we have restored the mount table and we don't need diff --git a/vault/request_handling.go b/vault/request_handling.go index 79477db225..98a4759bdc 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -8,6 +8,7 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/jsonutil" @@ -113,8 +114,33 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err auditResp = logical.HTTPResponseToLogicalResponse(httpResp) } + var nonHMACReqDataKeys []string + var nonHMACRespDataKeys []string + entry := c.router.MatchingMountEntry(req.Path) + if entry != nil { + // Get and set ignored HMAC'd value. Reset those back to empty afterwards. + if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { + nonHMACReqDataKeys = rawVals.([]string) + } + + // Get and set ignored HMAC'd value. Reset those back to empty afterwards. + if auditResp != nil { + if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { + nonHMACRespDataKeys = rawVals.([]string) + } + } + } + // Create an audit trail of the response - if auditErr := c.auditBroker.LogResponse(ctx, auth, req, auditResp, c.auditedHeaders, err); auditErr != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + Response: auditResp, + OuterErr: err, + NonHMACReqDataKeys: nonHMACReqDataKeys, + NonHMACRespDataKeys: nonHMACRespDataKeys, + } + if auditErr := c.auditBroker.LogResponse(ctx, logInput, c.auditedHeaders); auditErr != nil { c.logger.Error("core: failed to audit response", "request_path", req.Path, "error", auditErr) return nil, ErrInternalError } @@ -125,6 +151,15 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp *logical.Response, retAuth *logical.Auth, retErr error) { defer metrics.MeasureSince([]string{"core", "handle_request"}, time.Now()) + var nonHMACReqDataKeys []string + entry := c.router.MatchingMountEntry(req.Path) + if entry != nil { + // Get and set ignored HMAC'd value. + if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { + nonHMACReqDataKeys = rawVals.([]string) + } + } + // Validate the token auth, te, ctErr := c.checkToken(ctx, req, false) // We run this logic first because we want to decrement the use count even in the case of an error @@ -172,7 +207,13 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp errType = ctErr } - if err := c.auditBroker.LogRequest(ctx, auth, req, c.auditedHeaders, ctErr); err != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + OuterErr: ctErr, + NonHMACReqDataKeys: nonHMACReqDataKeys, + } + if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) } @@ -189,7 +230,12 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp req.DisplayName = auth.DisplayName // Create an audit trail of the request - if err := c.auditBroker.LogRequest(ctx, auth, req, c.auditedHeaders, nil); err != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + NonHMACReqDataKeys: nonHMACReqDataKeys, + } + if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) retErr = multierror.Append(retErr, ErrInternalError) return nil, auth, retErr @@ -358,7 +404,11 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re // Create an audit trail of the request, auth is not available on login requests // Create an audit trail of the request. Attach auth if it was returned, // e.g. if a token was provided. - if err := c.auditBroker.LogRequest(ctx, auth, req, c.auditedHeaders, nil); err != nil { + logInput := &audit.LogInput{ + Auth: auth, + Request: req, + } + if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) return nil, nil, ErrInternalError } diff --git a/vault/testing.go b/vault/testing.go index 7d1c19722f..b91b581f4a 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -611,11 +611,11 @@ func (n *noopAudit) GetHash(data string) (string, error) { return salt.GetIdentifiedHMAC(data), nil } -func (n *noopAudit) LogRequest(_ context.Context, _ *logical.Auth, _ *logical.Request, _ error) error { +func (n *noopAudit) LogRequest(_ context.Context, _ *audit.LogInput) error { return nil } -func (n *noopAudit) LogResponse(_ context.Context, _ *logical.Auth, _ *logical.Request, _ *logical.Response, _ error) error { +func (n *noopAudit) LogResponse(_ context.Context, _ *audit.LogInput) error { return nil } diff --git a/website/source/api/system/auth.html.md b/website/source/api/system/auth.html.md index 062584cf20..4b72e9aa4c 100644 --- a/website/source/api/system/auth.html.md +++ b/website/source/api/system/auth.html.md @@ -194,6 +194,14 @@ can be achieved without `sudo` via `sys/mounts/auth/[auth-path]/tune`._ - `description` `(string: "")` – Specifies the description of the mount. This overrides the current stored value, if any. +- `audit_non_hmac_request_keys` `(array: [])` - Specifies the comma-separated + list of keys that will not be HMAC'd by audit devices in the request data + object. + +- `audit_non_hmac_response_keys` `(array: [])` - Specifies the comma-separated + list of keys that will not be HMAC'd by audit devices in the response data + object. + ### Sample Payload ```json diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index b71cbfbada..bd52bdea9b 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -202,6 +202,14 @@ This endpoint tunes configuration parameters for a given mount point. - `description` `(string: "")` – Specifies the description of the mount. This overrides the current stored value, if any. +- `audit_non_hmac_request_keys` `(array: [])` - Specifies the comma-separated + list of keys that will not be HMAC'd by audit devices in the request data + object. + +- `audit_non_hmac_response_keys` `(array: [])` - Specifies the comma-separated + list of keys that will not be HMAC'd by audit devices in the response data + object. + ### Sample Payload ```json