Non-HMAC audit values (#4033)

* Add non-hmac request keys

* Update comment

* Initial audit request keys implementation

* Add audit_non_hmac_response_keys

* Move where req.NonHMACKeys gets set

* Minor refactor

* Add params to auth tune endpoints

* Sync cache on loadCredentials

* Explicitly unset req.NonHMACKeys

* Do not error if entry is nil

* Add tests

* docs: Add params to api sections

* Refactor audit.Backend and Formatter interfaces, update audit broker methods

* Add audit_broker.go

* Fix method call params in audit backends

* Remove fields from logical.Request and logical.Response, pass keys via LogInput

* Use data.GetOk to allow unsetting existing values

* Remove debug lines

* Add test for unsetting values

* Address review feedback

* Initialize values in FormatRequest and FormatResponse using input values

* Update docs

* Use strutil.StrListContains

* Use strutil.StrListContains
This commit is contained in:
Calvin Leung Huang 2018-03-02 12:18:39 -05:00 committed by GitHub
parent 49068a42be
commit e2fb199ce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 820 additions and 351 deletions

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

200
vault/audit_broker.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.",
`

View File

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

View File

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

View File

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

View File

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

View File

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