From d917c5c88837f6877a09f94e66f7aacaa3d05a99 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Fri, 27 Jan 2017 17:12:45 -0800 Subject: [PATCH 01/20] Add /sys/config/audited-headers endpoint for configuring the headers that will be audited --- audit/audit.go | 4 +-- audit/auditedheaders.go | 62 +++++++++++++++++++++++++++++++++ audit/format.go | 15 ++++++++ audit/formatter.go | 4 +-- audit/hashstructure.go | 5 +++ builtin/audit/file/backend.go | 7 ++-- builtin/audit/syslog/backend.go | 9 +++-- http/logical.go | 4 ++- logical/request.go | 4 +++ vault/audit.go | 8 ++--- vault/core.go | 13 +++++-- vault/logical_system.go | 60 +++++++++++++++++++++++++++++++ vault/request_handling.go | 8 ++--- vault/router.go | 5 +++ vault/testing.go | 4 +-- 15 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 audit/auditedheaders.go diff --git a/audit/audit.go b/audit/audit.go index dffa8eee54da..0d69538966d4 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -14,13 +14,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(*logical.Auth, *logical.Request, error) error + LogRequest(*logical.Auth, *logical.Request, *AuditedHeadersConfig, error) 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(*logical.Auth, *logical.Request, *logical.Response, error) error + LogResponse(*logical.Auth, *logical.Request, *logical.Response, *AuditedHeadersConfig, error) 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 diff --git a/audit/auditedheaders.go b/audit/auditedheaders.go new file mode 100644 index 000000000000..f993c03d13c8 --- /dev/null +++ b/audit/auditedheaders.go @@ -0,0 +1,62 @@ +package audit + +import ( + "fmt" + "sync" + + "github.com/hashicorp/vault/helper/salt" +) + +type auditedHeaderSettings struct { + HMAC bool +} + +type AuditedHeadersConfig struct { + Headers map[string]*auditedHeaderSettings + + sync.RWMutex +} + +func NewAuditedHeadersConfig() *AuditedHeadersConfig { + return &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + } +} + +func (a *AuditedHeadersConfig) Add(header string, hmac bool) { + a.Lock() + a.Headers[header] = &auditedHeaderSettings{hmac} + a.Unlock() +} + +func (a *AuditedHeadersConfig) Remove(header string) { + a.Lock() + delete(a.Headers, header) + a.Unlock() +} + +func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, salt *salt.Salt) (result map[string][]string, err error) { + a.RLock() + defer a.RUnlock() + + fmt.Println(a.Headers) + + result = make(map[string][]string) + for key, val := range headers { + hVals := make([]string, len(val)) + copy(hVals, val) + + if settings, ok := a.Headers[key]; ok { + if settings.HMAC { + fmt.Println("HMAC'ING") + if err := Hash(salt, hVals); err != nil { + return nil, err + } + } + + result[key] = hVals + } + } + + return +} diff --git a/audit/format.go b/audit/format.go index 60833a21507d..5f4ffa5f6395 100644 --- a/audit/format.go +++ b/audit/format.go @@ -25,6 +25,7 @@ type AuditFormatter struct { func (f *AuditFormatter) FormatRequest( w io.Writer, config FormatterConfig, + headersConfig *AuditedHeadersConfig, auth *logical.Auth, req *logical.Request, err error) error { @@ -89,6 +90,11 @@ func (f *AuditFormatter) FormatRequest( errString = err.Error() } + headers, err := headersConfig.ApplyConfig(req.Headers, config.Salt) + if err != nil { + return err + } + reqEntry := &AuditRequestEntry{ Type: "request", Error: errString, @@ -107,6 +113,7 @@ func (f *AuditFormatter) FormatRequest( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), + Headers: headers, }, } @@ -124,6 +131,7 @@ func (f *AuditFormatter) FormatRequest( func (f *AuditFormatter) FormatResponse( w io.Writer, config FormatterConfig, + headersConfig *AuditedHeadersConfig, auth *logical.Auth, req *logical.Request, resp *logical.Response, @@ -257,6 +265,11 @@ func (f *AuditFormatter) FormatResponse( } } + headers, err := headersConfig.ApplyConfig(req.Headers, config.Salt) + if err != nil { + return err + } + respEntry := &AuditResponseEntry{ Type: "response", Error: errString, @@ -275,6 +288,7 @@ func (f *AuditFormatter) FormatResponse( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), + Headers: headers, }, Response: AuditResponse{ @@ -325,6 +339,7 @@ type AuditRequest struct { Data map[string]interface{} `json:"data"` RemoteAddr string `json:"remote_address"` WrapTTL int `json:"wrap_ttl"` + Headers map[string][]string `json:"headers"` } type AuditResponse struct { diff --git a/audit/formatter.go b/audit/formatter.go index 318bd1bc5976..2e5635ce8d79 100644 --- a/audit/formatter.go +++ b/audit/formatter.go @@ -13,8 +13,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, *AuditedHeadersConfig, *logical.Auth, *logical.Request, error) error + FormatResponse(io.Writer, FormatterConfig, *AuditedHeadersConfig, *logical.Auth, *logical.Request, *logical.Response, error) error } type FormatterConfig struct { diff --git a/audit/hashstructure.go b/audit/hashstructure.go index 8d0fd7c6c7c6..b5d70129a6ec 100644 --- a/audit/hashstructure.go +++ b/audit/hashstructure.go @@ -94,6 +94,11 @@ func Hash(salter *salt.Salt, raw interface{}) error { if s.WrappedAccessor != "" { s.WrappedAccessor = fn(s.WrappedAccessor) } + + case []string: + for i, el := range s { + s[i] = fn(el) + } } return nil diff --git a/builtin/audit/file/backend.go b/builtin/audit/file/backend.go index 0318052a4be8..efba004fa15f 100644 --- a/builtin/audit/file/backend.go +++ b/builtin/audit/file/backend.go @@ -111,7 +111,7 @@ func (b *Backend) GetHash(data string) string { return audit.HashString(b.formatConfig.Salt, data) } -func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr error) error { +func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headerConfig *audit.AuditedHeadersConfig, outerErr error) error { b.fileLock.Lock() defer b.fileLock.Unlock() @@ -119,13 +119,14 @@ func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr return err } - return b.formatter.FormatRequest(b.f, b.formatConfig, auth, req, outerErr) + return b.formatter.FormatRequest(b.f, b.formatConfig, headerConfig, auth, req, outerErr) } func (b *Backend) LogResponse( auth *logical.Auth, req *logical.Request, resp *logical.Response, + headerConfig *audit.AuditedHeadersConfig, err error) error { b.fileLock.Lock() @@ -135,7 +136,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, headerConfig, auth, req, resp, err) } // The file lock must be held before calling this diff --git a/builtin/audit/syslog/backend.go b/builtin/audit/syslog/backend.go index 1bf81e8c46e1..1902706a9fcf 100644 --- a/builtin/audit/syslog/backend.go +++ b/builtin/audit/syslog/backend.go @@ -94,9 +94,9 @@ func (b *Backend) GetHash(data string) string { return audit.HashString(b.formatConfig.Salt, data) } -func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr error) error { +func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headersConfig *audit.AuditedHeadersConfig, outerErr error) 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, headersConfig, auth, req, outerErr); err != nil { return err } @@ -105,10 +105,9 @@ func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr return err } -func (b *Backend) LogResponse(auth *logical.Auth, req *logical.Request, - resp *logical.Response, err error) error { +func (b *Backend) LogResponse(auth *logical.Auth, req *logical.Request, resp *logical.Response, headersConfig *audit.AuditedHeadersConfig, err error) 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, headersConfig, auth, req, resp, err); err != nil { return err } diff --git a/http/logical.go b/http/logical.go index b3dcbc5162a0..2d1b024aff7b 100644 --- a/http/logical.go +++ b/http/logical.go @@ -1,6 +1,7 @@ package http import ( + "fmt" "io" "net" "net/http" @@ -78,6 +79,7 @@ func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Reques Path: path, Data: data, Connection: getConnection(r), + Headers: r.Header, }) req, err = requestWrapInfo(r, req) @@ -142,7 +144,7 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa return } } - + fmt.Println(r.Header) // Build the proper response respondLogical(w, r, req, dataOnly, resp) }) diff --git a/logical/request.go b/logical/request.go index 4420d73b4383..b0f93a588500 100644 --- a/logical/request.go +++ b/logical/request.go @@ -48,6 +48,10 @@ type Request struct { // to represent the auth that was returned prior. Auth *Auth `json:"auth" structs:"auth" mapstructure:"auth"` + // Auth will be non-nil only for Renew operations + // to represent the auth that was returned prior. + Headers map[string][]string `json:"headers" structs:"headers" mapstructure:"headers"` + // Connection will be non-nil only for credential providers to // inspect the connection information and potentially use it for // authentication/protection. diff --git a/vault/audit.go b/vault/audit.go index 7919b249af53..8c0daec0ef81 100644 --- a/vault/audit.go +++ b/vault/audit.go @@ -408,7 +408,7 @@ func (a *AuditBroker) GetHash(name string, input string) (string, error) { // 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(auth *logical.Auth, req *logical.Request, outerErr error) (retErr error) { +func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, headersConfig *audit.AuditedHeadersConfig, outerErr error) (retErr error) { defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now()) a.RLock() defer a.RUnlock() @@ -430,7 +430,7 @@ func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, outer anyLogged := false for name, be := range a.backends { start := time.Now() - err := be.backend.LogRequest(auth, req, outerErr) + err := be.backend.LogRequest(auth, req, headersConfig, outerErr) metrics.MeasureSince([]string{"audit", name, "log_request"}, start) if err != nil { a.logger.Error("audit: backend failed to log request", "backend", name, "error", err) @@ -448,7 +448,7 @@ func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, outer // 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(auth *logical.Auth, req *logical.Request, - resp *logical.Response, err error) (reterr error) { + resp *logical.Response, headersConfig *audit.AuditedHeadersConfig, err error) (reterr error) { defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now()) a.RLock() defer a.RUnlock() @@ -463,7 +463,7 @@ func (a *AuditBroker) LogResponse(auth *logical.Auth, req *logical.Request, anyLogged := false for name, be := range a.backends { start := time.Now() - err := be.backend.LogResponse(auth, req, resp, err) + err := be.backend.LogResponse(auth, req, resp, headersConfig, err) metrics.MeasureSince([]string{"audit", name, "log_response"}, start) if err != nil { a.logger.Error("audit: backend failed to log response", "backend", name, "error", err) diff --git a/vault/core.go b/vault/core.go index 4d4c8c69f0a5..e240c356817e 100644 --- a/vault/core.go +++ b/vault/core.go @@ -218,6 +218,10 @@ type Core struct { // out into the configured audit backends auditBroker *AuditBroker + // auditedHeaders is used to configure which http headers + // can be output in the audit logs + auditedHeaders *audit.AuditedHeadersConfig + // systemBarrierView is the barrier view for the system backend systemBarrierView *BarrierView @@ -426,6 +430,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { clusterCertPool: x509.NewCertPool(), clusterListenerShutdownCh: make(chan struct{}), clusterListenerShutdownSuccessCh: make(chan struct{}), + auditedHeaders: audit.NewAuditedHeadersConfig(), } // Wrap the backend in a cache unless disabled @@ -964,7 +969,7 @@ func (c *Core) sealInitCommon(req *logical.Request) (retErr error) { DisplayName: te.DisplayName, } - if err := c.auditBroker.LogRequest(auth, req, nil); err != nil { + if err := c.auditBroker.LogRequest(auth, req, c.auditedHeaders, nil); 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 @@ -1050,7 +1055,7 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) { DisplayName: te.DisplayName, } - if err := c.auditBroker.LogRequest(auth, req, nil); err != nil { + if err := c.auditBroker.LogRequest(auth, req, c.auditedHeaders, nil); 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 @@ -1606,3 +1611,7 @@ func (c *Core) BarrierKeyLength() (min, max int) { max += shamir.ShareOverhead return } + +func (c *Core) AuditedHeadersConfig() *audit.AuditedHeadersConfig { + return c.auditedHeaders +} diff --git a/vault/logical_system.go b/vault/logical_system.go index 534651d52aad..14d7e8dc54b8 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -40,6 +40,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "audit/*", "raw/*", "rotate", + "config/*", }, Unauthenticated: []string{ @@ -621,6 +622,28 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen HelpSynopsis: strings.TrimSpace(sysHelp["rewrap"][0]), HelpDescription: strings.TrimSpace(sysHelp["rewrap"][1]), }, + + &framework.Path{ + Pattern: "config/audited-headers$", + + Fields: map[string]*framework.FieldSchema{ + "header": &framework.FieldSchema{ + Type: framework.TypeString, + }, + "hmac": &framework.FieldSchema{ + Type: framework.TypeBool, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleAuditedHeadersAdd, + logical.DeleteOperation: b.handleAuditedHeadersRemove, + logical.ReadOperation: b.handleAuditedHeadersRead, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["rewrap"][0]), + HelpDescription: strings.TrimSpace(sysHelp["rewrap"][1]), + }, }, } @@ -635,6 +658,43 @@ type SystemBackend struct { Backend *framework.Backend } +func (b *SystemBackend) handleAuditedHeadersAdd(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + headerConfig := b.Core.AuditedHeadersConfig() + + header := d.Get("header").(string) + hmac := d.Get("hmac").(bool) + if header == "" { + return logical.ErrorResponse("missing header name"), nil + } + + headerConfig.Add(header, hmac) + + return nil, nil +} + +func (b *SystemBackend) handleAuditedHeadersRemove(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + headerConfig := b.Core.AuditedHeadersConfig() + + fmt.Println(d.Get("header")) + header := d.Get("header").(string) + if header == "" { + return logical.ErrorResponse("missing header name"), nil + } + headerConfig.Remove(header) + + return nil, nil +} + +func (b *SystemBackend) handleAuditedHeadersRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + headerConfig := b.Core.AuditedHeadersConfig() + + return &logical.Response{ + Data: map[string]interface{}{ + "headers": headerConfig.Headers, + }, + }, nil +} + // handleCapabilitiesreturns the ACL capabilities of the token for a given path func (b *SystemBackend) handleCapabilities(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { token := d.Get("token").(string) diff --git a/vault/request_handling.go b/vault/request_handling.go index 8f1325df917a..44b8125507f7 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -102,7 +102,7 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err } // Create an audit trail of the response - if auditErr := c.auditBroker.LogResponse(auth, req, auditResp, err); auditErr != nil { + if auditErr := c.auditBroker.LogResponse(auth, req, auditResp, c.auditedHeaders, err); auditErr != nil { c.logger.Error("core: failed to audit response", "request_path", req.Path, "error", auditErr) return nil, ErrInternalError } @@ -162,7 +162,7 @@ func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, r errType = logical.ErrInvalidRequest } - if err := c.auditBroker.LogRequest(auth, req, ctErr); err != nil { + if err := c.auditBroker.LogRequest(auth, req, c.auditedHeaders, ctErr); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) } @@ -176,7 +176,7 @@ func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, r req.DisplayName = auth.DisplayName // Create an audit trail of the request - if err := c.auditBroker.LogRequest(auth, req, nil); err != nil { + if err := c.auditBroker.LogRequest(auth, req, c.auditedHeaders, nil); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) retErr = multierror.Append(retErr, ErrInternalError) return nil, auth, retErr @@ -317,7 +317,7 @@ func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *log defer metrics.MeasureSince([]string{"core", "handle_login_request"}, time.Now()) // Create an audit trail of the request, auth is not available on login requests - if err := c.auditBroker.LogRequest(nil, req, nil); err != nil { + if err := c.auditBroker.LogRequest(nil, req, c.auditedHeaders, nil); err != nil { c.logger.Error("core: failed to audit request", "path", req.Path, "error", err) return nil, nil, ErrInternalError } diff --git a/vault/router.go b/vault/router.go index cbec432fd7e9..13fcb6b748ae 100644 --- a/vault/router.go +++ b/vault/router.go @@ -283,6 +283,10 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica // Cache the identifier of the request originalReqID := req.ID + // Cache the headers and hide them from backends + headers := req.Headers + req.Headers = nil + // Cache the wrap info of the request var wrapInfo *logical.RequestWrapInfo if req.WrapInfo != nil { @@ -301,6 +305,7 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica req.Storage = nil req.ClientToken = clientToken req.WrapInfo = wrapInfo + req.Headers = headers }() // Invoke the backend diff --git a/vault/testing.go b/vault/testing.go index b287941a9953..8715b53ba0ea 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -347,11 +347,11 @@ func (n *noopAudit) GetHash(data string) string { return n.Config.Salt.GetIdentifiedHMAC(data) } -func (n *noopAudit) LogRequest(a *logical.Auth, r *logical.Request, e error) error { +func (n *noopAudit) LogRequest(a *logical.Auth, r *logical.Request, h *audit.AuditedHeadersConfig, e error) error { return nil } -func (n *noopAudit) LogResponse(a *logical.Auth, r *logical.Request, re *logical.Response, err error) error { +func (n *noopAudit) LogResponse(a *logical.Auth, r *logical.Request, re *logical.Response, h *audit.AuditedHeadersConfig, err error) error { return nil } From e3dba9a8f615d2a4c321d0a49f14be98424b41b8 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Fri, 27 Jan 2017 17:20:21 -0800 Subject: [PATCH 02/20] Remove some debug lines --- audit/auditedheaders.go | 4 ---- http/logical.go | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/audit/auditedheaders.go b/audit/auditedheaders.go index f993c03d13c8..6ca45f9b2850 100644 --- a/audit/auditedheaders.go +++ b/audit/auditedheaders.go @@ -1,7 +1,6 @@ package audit import ( - "fmt" "sync" "github.com/hashicorp/vault/helper/salt" @@ -39,8 +38,6 @@ func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, salt *sa a.RLock() defer a.RUnlock() - fmt.Println(a.Headers) - result = make(map[string][]string) for key, val := range headers { hVals := make([]string, len(val)) @@ -48,7 +45,6 @@ func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, salt *sa if settings, ok := a.Headers[key]; ok { if settings.HMAC { - fmt.Println("HMAC'ING") if err := Hash(salt, hVals); err != nil { return nil, err } diff --git a/http/logical.go b/http/logical.go index 2d1b024aff7b..f350bf0c06ea 100644 --- a/http/logical.go +++ b/http/logical.go @@ -1,7 +1,6 @@ package http import ( - "fmt" "io" "net" "net/http" @@ -144,7 +143,7 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa return } } - fmt.Println(r.Header) + // Build the proper response respondLogical(w, r, req, dataOnly, resp) }) From ad212d3806723469ea552591fae46177d04b35bb Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Tue, 31 Jan 2017 12:56:43 -0800 Subject: [PATCH 03/20] Add a persistant layer and refactor a bit --- audit/audit.go | 4 +- audit/auditedheaders.go | 58 ---------------- audit/format.go | 16 +---- audit/formatter.go | 4 +- builtin/audit/file/backend.go | 7 +- builtin/audit/syslog/backend.go | 8 +-- vault/audit.go | 24 +++++-- vault/auditedheaders.go | 114 ++++++++++++++++++++++++++++++++ vault/core.go | 8 ++- vault/logical_system.go | 4 +- vault/testing.go | 4 +- 11 files changed, 156 insertions(+), 95 deletions(-) delete mode 100644 audit/auditedheaders.go create mode 100644 vault/auditedheaders.go diff --git a/audit/audit.go b/audit/audit.go index 0d69538966d4..dffa8eee54da 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -14,13 +14,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(*logical.Auth, *logical.Request, *AuditedHeadersConfig, error) error + LogRequest(*logical.Auth, *logical.Request, error) 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(*logical.Auth, *logical.Request, *logical.Response, *AuditedHeadersConfig, error) error + LogResponse(*logical.Auth, *logical.Request, *logical.Response, error) 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 diff --git a/audit/auditedheaders.go b/audit/auditedheaders.go deleted file mode 100644 index 6ca45f9b2850..000000000000 --- a/audit/auditedheaders.go +++ /dev/null @@ -1,58 +0,0 @@ -package audit - -import ( - "sync" - - "github.com/hashicorp/vault/helper/salt" -) - -type auditedHeaderSettings struct { - HMAC bool -} - -type AuditedHeadersConfig struct { - Headers map[string]*auditedHeaderSettings - - sync.RWMutex -} - -func NewAuditedHeadersConfig() *AuditedHeadersConfig { - return &AuditedHeadersConfig{ - Headers: make(map[string]*auditedHeaderSettings), - } -} - -func (a *AuditedHeadersConfig) Add(header string, hmac bool) { - a.Lock() - a.Headers[header] = &auditedHeaderSettings{hmac} - a.Unlock() -} - -func (a *AuditedHeadersConfig) Remove(header string) { - a.Lock() - delete(a.Headers, header) - a.Unlock() -} - -func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, salt *salt.Salt) (result map[string][]string, err error) { - a.RLock() - defer a.RUnlock() - - result = make(map[string][]string) - for key, val := range headers { - hVals := make([]string, len(val)) - copy(hVals, val) - - if settings, ok := a.Headers[key]; ok { - if settings.HMAC { - if err := Hash(salt, hVals); err != nil { - return nil, err - } - } - - result[key] = hVals - } - } - - return -} diff --git a/audit/format.go b/audit/format.go index 5f4ffa5f6395..4eb4f22824eb 100644 --- a/audit/format.go +++ b/audit/format.go @@ -25,7 +25,6 @@ type AuditFormatter struct { func (f *AuditFormatter) FormatRequest( w io.Writer, config FormatterConfig, - headersConfig *AuditedHeadersConfig, auth *logical.Auth, req *logical.Request, err error) error { @@ -90,11 +89,6 @@ func (f *AuditFormatter) FormatRequest( errString = err.Error() } - headers, err := headersConfig.ApplyConfig(req.Headers, config.Salt) - if err != nil { - return err - } - reqEntry := &AuditRequestEntry{ Type: "request", Error: errString, @@ -113,7 +107,7 @@ func (f *AuditFormatter) FormatRequest( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), - Headers: headers, + Headers: req.Headers, }, } @@ -131,7 +125,6 @@ func (f *AuditFormatter) FormatRequest( func (f *AuditFormatter) FormatResponse( w io.Writer, config FormatterConfig, - headersConfig *AuditedHeadersConfig, auth *logical.Auth, req *logical.Request, resp *logical.Response, @@ -265,11 +258,6 @@ func (f *AuditFormatter) FormatResponse( } } - headers, err := headersConfig.ApplyConfig(req.Headers, config.Salt) - if err != nil { - return err - } - respEntry := &AuditResponseEntry{ Type: "response", Error: errString, @@ -288,7 +276,7 @@ func (f *AuditFormatter) FormatResponse( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), - Headers: headers, + Headers: req.Headers, }, Response: AuditResponse{ diff --git a/audit/formatter.go b/audit/formatter.go index 2e5635ce8d79..318bd1bc5976 100644 --- a/audit/formatter.go +++ b/audit/formatter.go @@ -13,8 +13,8 @@ import ( // // It is recommended that you pass data through Hash prior to formatting it. type Formatter interface { - FormatRequest(io.Writer, FormatterConfig, *AuditedHeadersConfig, *logical.Auth, *logical.Request, error) error - FormatResponse(io.Writer, FormatterConfig, *AuditedHeadersConfig, *logical.Auth, *logical.Request, *logical.Response, error) error + FormatRequest(io.Writer, FormatterConfig, *logical.Auth, *logical.Request, error) error + FormatResponse(io.Writer, FormatterConfig, *logical.Auth, *logical.Request, *logical.Response, error) error } type FormatterConfig struct { diff --git a/builtin/audit/file/backend.go b/builtin/audit/file/backend.go index efba004fa15f..0318052a4be8 100644 --- a/builtin/audit/file/backend.go +++ b/builtin/audit/file/backend.go @@ -111,7 +111,7 @@ func (b *Backend) GetHash(data string) string { return audit.HashString(b.formatConfig.Salt, data) } -func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headerConfig *audit.AuditedHeadersConfig, outerErr error) error { +func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr error) error { b.fileLock.Lock() defer b.fileLock.Unlock() @@ -119,14 +119,13 @@ func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headerCon return err } - return b.formatter.FormatRequest(b.f, b.formatConfig, headerConfig, auth, req, outerErr) + return b.formatter.FormatRequest(b.f, b.formatConfig, auth, req, outerErr) } func (b *Backend) LogResponse( auth *logical.Auth, req *logical.Request, resp *logical.Response, - headerConfig *audit.AuditedHeadersConfig, err error) error { b.fileLock.Lock() @@ -136,7 +135,7 @@ func (b *Backend) LogResponse( return err } - return b.formatter.FormatResponse(b.f, b.formatConfig, headerConfig, auth, req, resp, err) + return b.formatter.FormatResponse(b.f, b.formatConfig, auth, req, resp, err) } // The file lock must be held before calling this diff --git a/builtin/audit/syslog/backend.go b/builtin/audit/syslog/backend.go index 1902706a9fcf..3056a64c916c 100644 --- a/builtin/audit/syslog/backend.go +++ b/builtin/audit/syslog/backend.go @@ -94,9 +94,9 @@ func (b *Backend) GetHash(data string) string { return audit.HashString(b.formatConfig.Salt, data) } -func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headersConfig *audit.AuditedHeadersConfig, outerErr error) error { +func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, outerErr error) error { var buf bytes.Buffer - if err := b.formatter.FormatRequest(&buf, b.formatConfig, headersConfig, auth, req, outerErr); err != nil { + if err := b.formatter.FormatRequest(&buf, b.formatConfig, auth, req, outerErr); err != nil { return err } @@ -105,9 +105,9 @@ func (b *Backend) LogRequest(auth *logical.Auth, req *logical.Request, headersCo return err } -func (b *Backend) LogResponse(auth *logical.Auth, req *logical.Request, resp *logical.Response, headersConfig *audit.AuditedHeadersConfig, err error) error { +func (b *Backend) LogResponse(auth *logical.Auth, req *logical.Request, resp *logical.Response, err error) error { var buf bytes.Buffer - if err := b.formatter.FormatResponse(&buf, b.formatConfig, headersConfig, auth, req, resp, err); err != nil { + if err := b.formatter.FormatResponse(&buf, b.formatConfig, auth, req, resp, err); err != nil { return err } diff --git a/vault/audit.go b/vault/audit.go index 8c0daec0ef81..3df4cd96b1a2 100644 --- a/vault/audit.go +++ b/vault/audit.go @@ -408,7 +408,7 @@ func (a *AuditBroker) GetHash(name string, input string) (string, error) { // 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(auth *logical.Auth, req *logical.Request, headersConfig *audit.AuditedHeadersConfig, outerErr error) (retErr error) { +func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, headersConfig *AuditedHeadersConfig, outerErr error) (retErr error) { defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now()) a.RLock() defer a.RUnlock() @@ -426,11 +426,19 @@ func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, heade // 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 + req.Headers = headersConfig.ApplyConfig(headers, be.backend.GetHash) + start := time.Now() - err := be.backend.LogRequest(auth, req, headersConfig, outerErr) + err := be.backend.LogRequest(auth, req, outerErr) metrics.MeasureSince([]string{"audit", name, "log_request"}, start) if err != nil { a.logger.Error("audit: backend failed to log request", "backend", name, "error", err) @@ -448,7 +456,7 @@ func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, heade // 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(auth *logical.Auth, req *logical.Request, - resp *logical.Response, headersConfig *audit.AuditedHeadersConfig, err error) (reterr error) { + resp *logical.Response, headersConfig *AuditedHeadersConfig, err error) (reterr error) { defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now()) a.RLock() defer a.RUnlock() @@ -459,11 +467,19 @@ func (a *AuditBroker) LogResponse(auth *logical.Auth, req *logical.Request, } }() + 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 + req.Headers = headersConfig.ApplyConfig(headers, be.backend.GetHash) + start := time.Now() - err := be.backend.LogResponse(auth, req, resp, headersConfig, err) + err := be.backend.LogResponse(auth, req, resp, err) metrics.MeasureSince([]string{"audit", name, "log_response"}, start) if err != nil { a.logger.Error("audit: backend failed to log response", "backend", name, "error", err) diff --git a/vault/auditedheaders.go b/vault/auditedheaders.go new file mode 100644 index 000000000000..1f1aebaf71b9 --- /dev/null +++ b/vault/auditedheaders.go @@ -0,0 +1,114 @@ +package vault + +import ( + "fmt" + "sync" + + "github.com/hashicorp/vault/logical" +) + +const ( + auditedHeadersEntry = "audited_headers" + auditedHeadersSubPath = "auditedHeadersConfig/" +) + +type auditedHeaderSettings struct { + HMAC bool +} + +type AuditedHeadersConfig struct { + Headers map[string]*auditedHeaderSettings + + view *BarrierView + sync.RWMutex +} + +func NewAuditedHeadersConfig() *AuditedHeadersConfig { + return &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + } +} + +func (a *AuditedHeadersConfig) add(header string, hmac bool) error { + a.Lock() + defer a.Unlock() + + a.Headers[header] = &auditedHeaderSettings{hmac} + entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.Headers) + if err != nil { + return fmt.Errorf("failed to persist audited headers config: %v", err) + } + + if err := a.view.Put(entry); err != nil { + return fmt.Errorf("failed to persist audited headers config: %v", err) + } + + return nil +} + +func (a *AuditedHeadersConfig) remove(header string) error { + a.Lock() + defer a.Unlock() + + delete(a.Headers, header) + entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.Headers) + if err != nil { + return fmt.Errorf("failed to persist audited headers config: %v", err) + } + + if err := a.view.Put(entry); err != nil { + return fmt.Errorf("failed to persist audited headers config: %v", err) + } + + return nil +} + +func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, hashFunc func(string) string) (result map[string][]string) { + a.RLock() + defer a.RUnlock() + + result = make(map[string][]string) + for key, settings := range a.Headers { + if val, ok := headers[key]; ok { + hVals := make([]string, len(val)) + copy(hVals, val) + + if settings.HMAC { + for i, el := range hVals { + hVals[i] = hashFunc(el) + } + } + + result[key] = hVals + } + } + + return +} + +func (c *Core) setupAuditedHeadersConfig() error { + // Create a sub-view + view := c.systemBarrierView.SubView(auditedHeadersSubPath) + + // Create the config + + out, err := view.Get(auditedHeadersEntry) + if err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + headers := make(map[string]*auditedHeaderSettings) + if out != nil { + err = out.DecodeJSON(&headers) + if err != nil { + return err + } + } + + c.auditedHeaders = &AuditedHeadersConfig{ + Headers: headers, + view: view, + } + + return nil +} diff --git a/vault/core.go b/vault/core.go index e240c356817e..b587a7213081 100644 --- a/vault/core.go +++ b/vault/core.go @@ -220,7 +220,7 @@ type Core struct { // auditedHeaders is used to configure which http headers // can be output in the audit logs - auditedHeaders *audit.AuditedHeadersConfig + auditedHeaders *AuditedHeadersConfig // systemBarrierView is the barrier view for the system backend systemBarrierView *BarrierView @@ -430,7 +430,6 @@ func NewCore(conf *CoreConfig) (*Core, error) { clusterCertPool: x509.NewCertPool(), clusterListenerShutdownCh: make(chan struct{}), clusterListenerShutdownSuccessCh: make(chan struct{}), - auditedHeaders: audit.NewAuditedHeadersConfig(), } // Wrap the backend in a cache unless disabled @@ -1220,6 +1219,9 @@ func (c *Core) postUnseal() (retErr error) { if err := c.setupAudits(); err != nil { return err } + if err := c.setupAuditedHeadersConfig(); err != nil { + return err + } if c.ha != nil { if err := c.startClusterListener(); err != nil { return err @@ -1612,6 +1614,6 @@ func (c *Core) BarrierKeyLength() (min, max int) { return } -func (c *Core) AuditedHeadersConfig() *audit.AuditedHeadersConfig { +func (c *Core) AuditedHeadersConfig() *AuditedHeadersConfig { return c.auditedHeaders } diff --git a/vault/logical_system.go b/vault/logical_system.go index 14d7e8dc54b8..348007e1c26d 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -667,7 +667,7 @@ func (b *SystemBackend) handleAuditedHeadersAdd(req *logical.Request, d *framewo return logical.ErrorResponse("missing header name"), nil } - headerConfig.Add(header, hmac) + headerConfig.add(header, hmac) return nil, nil } @@ -680,7 +680,7 @@ func (b *SystemBackend) handleAuditedHeadersRemove(req *logical.Request, d *fram if header == "" { return logical.ErrorResponse("missing header name"), nil } - headerConfig.Remove(header) + headerConfig.remove(header) return nil, nil } diff --git a/vault/testing.go b/vault/testing.go index 8715b53ba0ea..b287941a9953 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -347,11 +347,11 @@ func (n *noopAudit) GetHash(data string) string { return n.Config.Salt.GetIdentifiedHMAC(data) } -func (n *noopAudit) LogRequest(a *logical.Auth, r *logical.Request, h *audit.AuditedHeadersConfig, e error) error { +func (n *noopAudit) LogRequest(a *logical.Auth, r *logical.Request, e error) error { return nil } -func (n *noopAudit) LogResponse(a *logical.Auth, r *logical.Request, re *logical.Response, h *audit.AuditedHeadersConfig, err error) error { +func (n *noopAudit) LogResponse(a *logical.Auth, r *logical.Request, re *logical.Response, err error) error { return nil } From 365d24cc42409cb61065c376142fed4e94e75ea0 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Tue, 31 Jan 2017 15:28:54 -0800 Subject: [PATCH 04/20] update the api endpoints to be more restful --- vault/logical_system.go | 42 +++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/vault/logical_system.go b/vault/logical_system.go index 348007e1c26d..42b82d20af08 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -624,7 +624,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "config/audited-headers$", + Pattern: "config/audited-headers/(?P
.+)", Fields: map[string]*framework.FieldSchema{ "header": &framework.FieldSchema{ @@ -636,9 +636,19 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.handleAuditedHeadersAdd, - logical.DeleteOperation: b.handleAuditedHeadersRemove, - logical.ReadOperation: b.handleAuditedHeadersRead, + logical.UpdateOperation: b.handleAuditedHeaderUpdate, + logical.DeleteOperation: b.handleAuditedHeaderDelete, + logical.ReadOperation: b.handleAuditedHeaderRead, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["rewrap"][0]), + HelpDescription: strings.TrimSpace(sysHelp["rewrap"][1]), + }, + &framework.Path{ + Pattern: "config/audited-headers$", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.handleAuditedHeadersRead, }, HelpSynopsis: strings.TrimSpace(sysHelp["rewrap"][0]), @@ -658,7 +668,7 @@ type SystemBackend struct { Backend *framework.Backend } -func (b *SystemBackend) handleAuditedHeadersAdd(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() header := d.Get("header").(string) @@ -672,7 +682,7 @@ func (b *SystemBackend) handleAuditedHeadersAdd(req *logical.Request, d *framewo return nil, nil } -func (b *SystemBackend) handleAuditedHeadersRemove(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *SystemBackend) handleAuditedHeaderDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() fmt.Println(d.Get("header")) @@ -685,6 +695,26 @@ func (b *SystemBackend) handleAuditedHeadersRemove(req *logical.Request, d *fram return nil, nil } +func (b *SystemBackend) handleAuditedHeaderRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + headerConfig := b.Core.AuditedHeadersConfig() + + header := d.Get("header").(string) + if header == "" { + return logical.ErrorResponse("missing header name"), nil + } + + settings, ok := headerConfig.Headers[header] + if !ok { + return logical.ErrorResponse("Could not find header in config"), nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + header: settings, + }, + }, nil +} + func (b *SystemBackend) handleAuditedHeadersRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() From 209eba91ccb7a52a7f18ff01016f63b02d2d6e98 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Tue, 31 Jan 2017 16:32:42 -0800 Subject: [PATCH 05/20] Add comments and clean up a few functions --- vault/auditedheaders.go | 20 +++++++++++--------- vault/logical_system.go | 5 ++++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/vault/auditedheaders.go b/vault/auditedheaders.go index 1f1aebaf71b9..129747c3f7e7 100644 --- a/vault/auditedheaders.go +++ b/vault/auditedheaders.go @@ -8,14 +8,18 @@ import ( ) const ( - auditedHeadersEntry = "audited_headers" + // Key used in the BarrierView to store and retrieve the header config + auditedHeadersEntry = "audited_headers" + // Path used to create a sub view off of BarrierView auditedHeadersSubPath = "auditedHeadersConfig/" ) type auditedHeaderSettings struct { - HMAC bool + HMAC bool `json:"hmac"` } +// AuditedHeadersConfig is used by the Audit Broker to write only approved +// headers to the audit logs. It uses a BarrierView to persist the settings. type AuditedHeadersConfig struct { Headers map[string]*auditedHeaderSettings @@ -23,12 +27,7 @@ type AuditedHeadersConfig struct { sync.RWMutex } -func NewAuditedHeadersConfig() *AuditedHeadersConfig { - return &AuditedHeadersConfig{ - Headers: make(map[string]*auditedHeaderSettings), - } -} - +// add adds or overwrites a header in the config and updates the barrier view func (a *AuditedHeadersConfig) add(header string, hmac bool) error { a.Lock() defer a.Unlock() @@ -46,6 +45,7 @@ func (a *AuditedHeadersConfig) add(header string, hmac bool) error { return nil } +// remove deletes a header out of the header config and updates the barrier view func (a *AuditedHeadersConfig) remove(header string) error { a.Lock() defer a.Unlock() @@ -63,6 +63,8 @@ func (a *AuditedHeadersConfig) remove(header string) error { return nil } +// ApplyConfig returns a map of approved headers and their values, either +// hmac'ed or plaintext func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, hashFunc func(string) string) (result map[string][]string) { a.RLock() defer a.RUnlock() @@ -86,12 +88,12 @@ func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, hashFunc return } +// Initalize the headers config by loading from the barrier view func (c *Core) setupAuditedHeadersConfig() error { // Create a sub-view view := c.systemBarrierView.SubView(auditedHeadersSubPath) // Create the config - out, err := view.Get(auditedHeadersEntry) if err != nil { return fmt.Errorf("failed to read config: %v", err) diff --git a/vault/logical_system.go b/vault/logical_system.go index 42b82d20af08..b1cea37a3f5d 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -668,6 +668,7 @@ type SystemBackend struct { Backend *framework.Backend } +// handleAuditedHeaderUpdate creates or overwrites a header entry func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() @@ -682,10 +683,10 @@ func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *frame return nil, nil } +// handleAudtedHeaderDelete deletes the header with the given name func (b *SystemBackend) handleAuditedHeaderDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() - fmt.Println(d.Get("header")) header := d.Get("header").(string) if header == "" { return logical.ErrorResponse("missing header name"), nil @@ -695,6 +696,7 @@ func (b *SystemBackend) handleAuditedHeaderDelete(req *logical.Request, d *frame return nil, nil } +// handleAuditedHeaderRead returns the header configuration for the given header name func (b *SystemBackend) handleAuditedHeaderRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() @@ -715,6 +717,7 @@ func (b *SystemBackend) handleAuditedHeaderRead(req *logical.Request, d *framewo }, nil } +// handleAuditedHeadersRead returns the whole audited headers config func (b *SystemBackend) handleAuditedHeadersRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { headerConfig := b.Core.AuditedHeadersConfig() From bde581a3acff3f61d49d98f18e0bfcfde352cc51 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Tue, 31 Jan 2017 17:06:35 -0800 Subject: [PATCH 06/20] Remove unneeded hash structure functionaility --- audit/hashstructure.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/audit/hashstructure.go b/audit/hashstructure.go index b5d70129a6ec..8d0fd7c6c7c6 100644 --- a/audit/hashstructure.go +++ b/audit/hashstructure.go @@ -94,11 +94,6 @@ func Hash(salter *salt.Salt, raw interface{}) error { if s.WrappedAccessor != "" { s.WrappedAccessor = fn(s.WrappedAccessor) } - - case []string: - for i, el := range s { - s[i] = fn(el) - } } return nil From 3da24efad7d6beaeafd56322dc80707fdaa783e8 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Tue, 31 Jan 2017 18:19:39 -0800 Subject: [PATCH 07/20] Fix existing tests --- audit/format_json_test.go | 5 ++++- audit/format_jsonx_test.go | 5 ++++- vault/audit_test.go | 20 ++++++++++++++------ vault/logical_system_test.go | 1 + 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/audit/format_json_test.go b/audit/format_json_test.go index 7fb7a8a15fdf..5a70f7d70483 100644 --- a/audit/format_json_test.go +++ b/audit/format_json_test.go @@ -32,6 +32,9 @@ func TestFormatJSON_formatRequest(t *testing.T) { WrapInfo: &logical.RequestWrapInfo{ TTL: 60 * time.Second, }, + Headers: map[string][]string{ + "foo": []string{"bar"}, + }, }, errors.New("this is an error"), testFormatJSONReqBasicStr, @@ -76,5 +79,5 @@ func TestFormatJSON_formatRequest(t *testing.T) { } } -const testFormatJSONReqBasicStr = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"display_name":"","policies":["root"],"metadata":null},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1"},"error":"this is an error"} +const testFormatJSONReqBasicStr = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"display_name":"","policies":["root"],"metadata":null},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1","headers":{"foo":["bar"]}},"error":"this is an error"} ` diff --git a/audit/format_jsonx_test.go b/audit/format_jsonx_test.go index 40d0bc572be8..be63ae39d7cc 100644 --- a/audit/format_jsonx_test.go +++ b/audit/format_jsonx_test.go @@ -31,10 +31,13 @@ func TestFormatJSONx_formatRequest(t *testing.T) { WrapInfo: &logical.RequestWrapInfo{ TTL: 60 * time.Second, }, + Headers: map[string][]string{ + "foo": []string{"bar"}, + }, }, errors.New("this is an error"), "", - `rootthis is an errorupdate/foo127.0.0.160request`, + `rootthis is an errorbarupdate/foo127.0.0.160request`, }, } diff --git a/vault/audit_test.go b/vault/audit_test.go index 3c438d89920a..157dd775283f 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -296,7 +296,11 @@ func TestAuditBroker_LogRequest(t *testing.T) { reqErrs := errors.New("errs") - err = b.LogRequest(auth, req, reqErrs) + headersConf := &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + } + + err = b.LogRequest(auth, req, headersConf, reqErrs) if err != nil { t.Fatalf("err: %v", err) } @@ -315,13 +319,13 @@ func TestAuditBroker_LogRequest(t *testing.T) { // Should still work with one failing backend a1.ReqErr = fmt.Errorf("failed") - if err := b.LogRequest(auth, req, nil); err != nil { + if err := b.LogRequest(auth, req, headersConf, nil); err != nil { t.Fatalf("err: %v", err) } // Should FAIL work with both failing backends a2.ReqErr = fmt.Errorf("failed") - if err := b.LogRequest(auth, req, nil); !errwrap.Contains(err, "no audit backend succeeded in logging the request") { + if err := b.LogRequest(auth, req, headersConf, nil); !errwrap.Contains(err, "no audit backend succeeded in logging the request") { t.Fatalf("err: %v", err) } } @@ -359,7 +363,11 @@ func TestAuditBroker_LogResponse(t *testing.T) { } respErr := fmt.Errorf("permission denied") - err := b.LogResponse(auth, req, resp, respErr) + headersConf := &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + } + + err := b.LogResponse(auth, req, resp, headersConf, respErr) if err != nil { t.Fatalf("err: %v", err) } @@ -381,14 +389,14 @@ func TestAuditBroker_LogResponse(t *testing.T) { // Should still work with one failing backend a1.RespErr = fmt.Errorf("failed") - err = b.LogResponse(auth, req, resp, respErr) + err = b.LogResponse(auth, req, resp, headersConf, respErr) if err != nil { t.Fatalf("err: %v", err) } // Should FAIL work with both failing backends a2.RespErr = fmt.Errorf("failed") - err = b.LogResponse(auth, req, resp, respErr) + err = b.LogResponse(auth, req, resp, headersConf, respErr) if err.Error() != "no audit backend succeeded in logging the response" { t.Fatalf("err: %v", err) } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index d6bca56f656a..1717a437fb9d 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -22,6 +22,7 @@ func TestSystemBackend_RootPaths(t *testing.T) { "audit/*", "raw/*", "rotate", + "config/*", } b := testSystemBackend(t) From 78ec29fc1d853172fafad23302ce694bb2da4625 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 12:51:03 -0800 Subject: [PATCH 08/20] Add tests --- .../{auditedheaders.go => audited_headers.go} | 0 vault/audited_headers_test.go | 154 ++++++++++++++++++ 2 files changed, 154 insertions(+) rename vault/{auditedheaders.go => audited_headers.go} (100%) create mode 100644 vault/audited_headers_test.go diff --git a/vault/auditedheaders.go b/vault/audited_headers.go similarity index 100% rename from vault/auditedheaders.go rename to vault/audited_headers.go diff --git a/vault/audited_headers_test.go b/vault/audited_headers_test.go new file mode 100644 index 000000000000..2abd279eb640 --- /dev/null +++ b/vault/audited_headers_test.go @@ -0,0 +1,154 @@ +package vault + +import ( + "reflect" + "testing" +) + +func mockAuditedHeaderConfig(t *testing.T) *AuditedHeadersConfig { + _, barrier, _ := mockBarrier(t) + view := NewBarrierView(barrier, "foo/") + return &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + view: view, + } +} + +func TestAuditedHeaderConfig_CRUD(t *testing.T) { + conf := mockAuditedHeaderConfig(t) + + testAuditedHeaderConfig_Add(t, conf) + testAuditedHeaderConfig_Remove(t, conf) +} + +func testAuditedHeaderConfig_Add(t *testing.T, conf *AuditedHeadersConfig) { + err := conf.add("X-Test-Header", false) + if err != nil { + t.Fatalf("Error when adding header to config: %s", err) + } + + settings, ok := conf.Headers["X-Test-Header"] + if !ok { + t.Fatal("Expected header to be found in config") + } + + if settings.HMAC { + t.Fatal("Expected HMAC to be set to false, got true") + } + + out, err := conf.view.Get(auditedHeadersEntry) + if err != nil { + t.Fatalf("Could not retrieve headers entry from config: %s", err) + } + + headers := make(map[string]*auditedHeaderSettings) + err = out.DecodeJSON(&headers) + if err != nil { + t.Fatalf("Error decoding header view: %s", err) + } + + expected := map[string]*auditedHeaderSettings{ + "X-Test-Header": &auditedHeaderSettings{ + HMAC: false, + }, + } + + if !reflect.DeepEqual(headers, expected) { + t.Fatalf("Expected config didn't match actual. Expected: %#v, Got: %#v", expected, headers) + } + + err = conf.add("X-Vault-Header", true) + if err != nil { + t.Fatalf("Error when adding header to config: %s", err) + } + + settings, ok = conf.Headers["X-Vault-Header"] + if !ok { + t.Fatal("Expected header to be found in config") + } + + if !settings.HMAC { + t.Fatal("Expected HMAC to be set to true, got false") + } + + out, err = conf.view.Get(auditedHeadersEntry) + if err != nil { + t.Fatalf("Could not retrieve headers entry from config: %s", err) + } + + headers = make(map[string]*auditedHeaderSettings) + err = out.DecodeJSON(&headers) + if err != nil { + t.Fatalf("Error decoding header view: %s", err) + } + + expected["X-Vault-Header"] = &auditedHeaderSettings{ + HMAC: true, + } + + if !reflect.DeepEqual(headers, expected) { + t.Fatalf("Expected config didn't match actual. Expected: %#v, Got: %#v", expected, headers) + } + +} + +func testAuditedHeaderConfig_Remove(t *testing.T, conf *AuditedHeadersConfig) { + err := conf.remove("X-Test-Header") + if err != nil { + t.Fatalf("Error when adding header to config: %s", err) + } + + _, ok := conf.Headers["X-Test-Header"] + if ok { + t.Fatal("Expected header to not be found in config") + } + + out, err := conf.view.Get(auditedHeadersEntry) + if err != nil { + t.Fatalf("Could not retrieve headers entry from config: %s", err) + } + + headers := make(map[string]*auditedHeaderSettings) + err = out.DecodeJSON(&headers) + if err != nil { + t.Fatalf("Error decoding header view: %s", err) + } + + expected := map[string]*auditedHeaderSettings{ + "X-Vault-Header": &auditedHeaderSettings{ + HMAC: true, + }, + } + + if !reflect.DeepEqual(headers, expected) { + t.Fatalf("Expected config didn't match actual. Expected: %#v, Got: %#v", expected, headers) + } + + err = conf.remove("X-Vault-Header") + if err != nil { + t.Fatalf("Error when adding header to config: %s", err) + } + + _, ok = conf.Headers["X-Vault-Header"] + if ok { + t.Fatal("Expected header to not be found in config") + } + + out, err = conf.view.Get(auditedHeadersEntry) + if err != nil { + t.Fatalf("Could not retrieve headers entry from config: %s", err) + } + + headers = make(map[string]*auditedHeaderSettings) + err = out.DecodeJSON(&headers) + if err != nil { + t.Fatalf("Error decoding header view: %s", err) + } + + expected = make(map[string]*auditedHeaderSettings) + + if !reflect.DeepEqual(headers, expected) { + t.Fatalf("Expected config didn't match actual. Expected: %#v, Got: %#v", expected, headers) + } + +} From b336bec24acd98631f3a5496f00b7a8ccba6023d Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 13:10:24 -0800 Subject: [PATCH 09/20] Add test for Applying the header config --- vault/audited_headers_test.go | 53 ++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/vault/audited_headers_test.go b/vault/audited_headers_test.go index 2abd279eb640..aa12648b21f1 100644 --- a/vault/audited_headers_test.go +++ b/vault/audited_headers_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func mockAuditedHeaderConfig(t *testing.T) *AuditedHeadersConfig { +func mockAuditedHeadersConfig(t *testing.T) *AuditedHeadersConfig { _, barrier, _ := mockBarrier(t) view := NewBarrierView(barrier, "foo/") return &AuditedHeadersConfig{ @@ -14,14 +14,14 @@ func mockAuditedHeaderConfig(t *testing.T) *AuditedHeadersConfig { } } -func TestAuditedHeaderConfig_CRUD(t *testing.T) { - conf := mockAuditedHeaderConfig(t) +func TestAuditedHeadersConfig_CRUD(t *testing.T) { + conf := mockAuditedHeadersConfig(t) - testAuditedHeaderConfig_Add(t, conf) - testAuditedHeaderConfig_Remove(t, conf) + testAuditedHeadersConfig_Add(t, conf) + testAuditedHeadersConfig_Remove(t, conf) } -func testAuditedHeaderConfig_Add(t *testing.T, conf *AuditedHeadersConfig) { +func testAuditedHeadersConfig_Add(t *testing.T, conf *AuditedHeadersConfig) { err := conf.add("X-Test-Header", false) if err != nil { t.Fatalf("Error when adding header to config: %s", err) @@ -92,7 +92,7 @@ func testAuditedHeaderConfig_Add(t *testing.T, conf *AuditedHeadersConfig) { } -func testAuditedHeaderConfig_Remove(t *testing.T, conf *AuditedHeadersConfig) { +func testAuditedHeadersConfig_Remove(t *testing.T, conf *AuditedHeadersConfig) { err := conf.remove("X-Test-Header") if err != nil { t.Fatalf("Error when adding header to config: %s", err) @@ -150,5 +150,44 @@ func testAuditedHeaderConfig_Remove(t *testing.T, conf *AuditedHeadersConfig) { if !reflect.DeepEqual(headers, expected) { t.Fatalf("Expected config didn't match actual. Expected: %#v, Got: %#v", expected, headers) } +} + +func TestAuditedHeadersConfig_ApplyConfig(t *testing.T) { + conf := mockAuditedHeadersConfig(t) + + conf.Headers = map[string]*auditedHeaderSettings{ + "X-Test-Header": &auditedHeaderSettings{false}, + "X-Vault-Header": &auditedHeaderSettings{true}, + } + + reqHeaders := map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"bar", "bar"}, + "Content-Type": []string{"json"}, + } + + hashFunc := func(s string) string { return "hashed" } + + result := conf.ApplyConfig(reqHeaders, hashFunc) + + expected := map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"hashed", "hashed"}, + } + + if !reflect.DeepEqual(result, expected) { + t.Fatalf("Expected headers did not match actual: Expected %#v\n Got %#v\n", expected, result) + } + + //Make sure we didn't edit the reqHeaders map + reqHeadersCopy := map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"bar", "bar"}, + "Content-Type": []string{"json"}, + } + + if !reflect.DeepEqual(reqHeaders, reqHeadersCopy) { + t.Fatalf("Req headers were changed, expected %#v\n got %#v", reqHeadersCopy, reqHeaders) + } } From e2987ea6462b2658420003c56ed77b8b72da1359 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 13:28:51 -0800 Subject: [PATCH 10/20] Add Benchmark for the ApplyConfig method --- vault/audited_headers_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/vault/audited_headers_test.go b/vault/audited_headers_test.go index aa12648b21f1..8fdbcf5f5795 100644 --- a/vault/audited_headers_test.go +++ b/vault/audited_headers_test.go @@ -3,6 +3,8 @@ package vault import ( "reflect" "testing" + + "github.com/hashicorp/vault/helper/salt" ) func mockAuditedHeadersConfig(t *testing.T) *AuditedHeadersConfig { @@ -191,3 +193,32 @@ func TestAuditedHeadersConfig_ApplyConfig(t *testing.T) { } } + +func BenchmarkAuditedHeaderConfig_ApplyConfig(b *testing.B) { + conf := &AuditedHeadersConfig{ + Headers: make(map[string]*auditedHeaderSettings), + view: nil, + } + + conf.Headers = map[string]*auditedHeaderSettings{ + "X-Test-Header": &auditedHeaderSettings{false}, + "X-Vault-Header": &auditedHeaderSettings{true}, + } + + reqHeaders := map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"bar", "bar"}, + "Content-Type": []string{"json"}, + } + + salter, err := salt.NewSalt(nil, nil) + if err != nil { + b.Fatal(err) + } + + hashFunc := func(s string) string { return salter.GetIdentifiedHMAC(s) } + + for i := 0; i < b.N; i++ { + conf.ApplyConfig(reqHeaders, hashFunc) + } +} From 29ba94a0c0de6cb35a5488b2ae0825539e5acc5d Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 13:32:54 -0800 Subject: [PATCH 11/20] ResetTimer on the benchmark: --- vault/audited_headers_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vault/audited_headers_test.go b/vault/audited_headers_test.go index 8fdbcf5f5795..07da7c9c5349 100644 --- a/vault/audited_headers_test.go +++ b/vault/audited_headers_test.go @@ -218,6 +218,8 @@ func BenchmarkAuditedHeaderConfig_ApplyConfig(b *testing.B) { hashFunc := func(s string) string { return salter.GetIdentifiedHMAC(s) } + // Reset the timer since we did a lot above + b.ResetTimer() for i := 0; i < b.N; i++ { conf.ApplyConfig(reqHeaders, hashFunc) } From 6c50928848925b127b8fdc2903560fe8266efa2b Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 14:15:31 -0800 Subject: [PATCH 12/20] Update the headers comment --- logical/request.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logical/request.go b/logical/request.go index b0f93a588500..f352b9ea8933 100644 --- a/logical/request.go +++ b/logical/request.go @@ -48,8 +48,9 @@ type Request struct { // to represent the auth that was returned prior. Auth *Auth `json:"auth" structs:"auth" mapstructure:"auth"` - // Auth will be non-nil only for Renew operations - // to represent the auth that was returned prior. + // Headers will contain the http headers from the request. This value will + // be used in the audit broker to ensure we are auditing only the allowed + // headers. Headers map[string][]string `json:"headers" structs:"headers" mapstructure:"headers"` // Connection will be non-nil only for credential providers to From f6b153f01dca56fbfc46ecca15986c6e803b2227 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 16:18:19 -0800 Subject: [PATCH 13/20] Add test for audit broker --- vault/audit_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/vault/audit_test.go b/vault/audit_test.go index 157dd775283f..51cd124438b1 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/vault/helper/logformat" "github.com/hashicorp/vault/logical" log "github.com/mgutz/logxi/v1" + "github.com/mitchellh/copystructure" ) type NoopAudit struct { @@ -31,8 +32,13 @@ type NoopAudit struct { } func (n *NoopAudit) LogRequest(a *logical.Auth, r *logical.Request, err error) error { + copy, err := copystructure.Copy(r) + if err != nil { + return err + } + n.ReqAuth = append(n.ReqAuth, a) - n.Req = append(n.Req, r) + n.Req = append(n.Req, copy.(*logical.Request)) n.ReqErrs = append(n.ReqErrs, err) return n.ReqErr } @@ -401,3 +407,68 @@ func TestAuditBroker_LogResponse(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestAuditBroker_AuditHeaders(t *testing.T) { + l := logformat.NewVaultLogger(log.LevelTrace) + b := NewAuditBroker(l) + a1 := &NoopAudit{} + a2 := &NoopAudit{} + b.Register("foo", a1, nil) + b.Register("bar", a2, nil) + + auth := &logical.Auth{ + ClientToken: "foo", + Policies: []string{"dev", "ops"}, + Metadata: map[string]string{ + "user": "armon", + "source": "github", + }, + } + req := &logical.Request{ + Operation: logical.ReadOperation, + Path: "sys/mounts", + Headers: map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"bar"}, + "Content-Type": []string{"baz"}, + }, + } + respErr := fmt.Errorf("permission denied") + + headersConf := &AuditedHeadersConfig{ + Headers: map[string]*auditedHeaderSettings{ + "X-Test-Header": &auditedHeaderSettings{false}, + "X-Vault-Header": &auditedHeaderSettings{false}, + }, + } + + err := b.LogRequest(auth, req, headersConf, respErr) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := map[string][]string{ + "X-Test-Header": []string{"foo"}, + "X-Vault-Header": []string{"bar"}, + } + + for _, a := range []*NoopAudit{a1, a2} { + if !reflect.DeepEqual(a.Req[0].Headers, expected) { + t.Fatalf("Bad audited headers: %#v", a.Req[0].Headers) + } + } + + // Should still work with one failing backend + a1.ReqErr = fmt.Errorf("failed") + err = b.LogRequest(auth, req, headersConf, respErr) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should FAIL work with both failing backends + a2.ReqErr = fmt.Errorf("failed") + err = b.LogRequest(auth, req, headersConf, respErr) + if !errwrap.Contains(err, "no audit backend succeeded in logging the request") { + t.Fatalf("err: %v", err) + } +} From 31060305b348c2fedc0c673eed56db548a8bd42a Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 16:44:07 -0800 Subject: [PATCH 14/20] Use hyphens instead of camel case --- vault/audited_headers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/audited_headers.go b/vault/audited_headers.go index 129747c3f7e7..21fead60aa85 100644 --- a/vault/audited_headers.go +++ b/vault/audited_headers.go @@ -9,9 +9,9 @@ import ( const ( // Key used in the BarrierView to store and retrieve the header config - auditedHeadersEntry = "audited_headers" + auditedHeadersEntry = "audited-headers" // Path used to create a sub view off of BarrierView - auditedHeadersSubPath = "auditedHeadersConfig/" + auditedHeadersSubPath = "audited-headers-config/" ) type auditedHeaderSettings struct { From 1a3b64a5e33f7863b11c36b63716493794c3dd52 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 16:49:25 -0800 Subject: [PATCH 15/20] Add size paramater to the allocation of the result map --- vault/audited_headers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/audited_headers.go b/vault/audited_headers.go index 21fead60aa85..f52eb6c85d65 100644 --- a/vault/audited_headers.go +++ b/vault/audited_headers.go @@ -69,7 +69,7 @@ func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, hashFunc a.RLock() defer a.RUnlock() - result = make(map[string][]string) + result = make(map[string][]string, len(a.Headers)) for key, settings := range a.Headers { if val, ok := headers[key]; ok { hVals := make([]string, len(val)) From 1e3494f6b671f6b5f04f7c9c738538772c302c9d Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 17:21:43 -0800 Subject: [PATCH 16/20] Fix the tests for the audit broker --- vault/audit_test.go | 70 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/vault/audit_test.go b/vault/audit_test.go index 51cd124438b1..74c5e7df9823 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -18,11 +18,12 @@ import ( ) type NoopAudit struct { - Config *audit.BackendConfig - ReqErr error - ReqAuth []*logical.Auth - Req []*logical.Request - ReqErrs []error + Config *audit.BackendConfig + ReqErr error + ReqAuth []*logical.Auth + Req []*logical.Request + ReqHeaders []map[string][]string + ReqErrs []error RespErr error RespAuth []*logical.Auth @@ -32,13 +33,9 @@ type NoopAudit struct { } func (n *NoopAudit) LogRequest(a *logical.Auth, r *logical.Request, err error) error { - copy, err := copystructure.Copy(r) - if err != nil { - return err - } - n.ReqAuth = append(n.ReqAuth, a) - n.Req = append(n.Req, copy.(*logical.Request)) + n.Req = append(n.Req, r) + n.ReqHeaders = append(n.ReqHeaders, r.Headers) n.ReqErrs = append(n.ReqErrs, err) return n.ReqErr } @@ -293,12 +290,25 @@ func TestAuditBroker_LogRequest(t *testing.T) { Path: "sys/mounts", } + // Copy so we can verify nothing canged + authCopyRaw, err := copystructure.Copy(auth) + if err != nil { + t.Fatal(err) + } + authCopy := authCopyRaw.(*logical.Auth) + + reqCopyRaw, err := copystructure.Copy(req) + if err != nil { + t.Fatal(err) + } + reqCopy := reqCopyRaw.(*logical.Request) + // Create an identifier for the request to verify against - var err error req.ID, err = uuid.GenerateUUID() if err != nil { t.Fatalf("failed to generate identifier for the request: path%s err: %v", req.Path, err) } + reqCopy.ID = req.ID reqErrs := errors.New("errs") @@ -306,7 +316,7 @@ func TestAuditBroker_LogRequest(t *testing.T) { Headers: make(map[string]*auditedHeaderSettings), } - err = b.LogRequest(auth, req, headersConf, reqErrs) + err = b.LogRequest(authCopy, reqCopy, headersConf, reqErrs) if err != nil { t.Fatalf("err: %v", err) } @@ -316,7 +326,7 @@ func TestAuditBroker_LogRequest(t *testing.T) { t.Fatalf("Bad: %#v", a.ReqAuth[0]) } if !reflect.DeepEqual(a.Req[0], req) { - t.Fatalf("Bad: %#v", a.Req[0]) + t.Fatalf("Bad: %#v\n wanted %#v", a.Req[0], req) } if !reflect.DeepEqual(a.ReqErrs[0], reqErrs) { t.Fatalf("Bad: %#v", a.ReqErrs[0]) @@ -369,11 +379,30 @@ func TestAuditBroker_LogResponse(t *testing.T) { } respErr := fmt.Errorf("permission denied") + // Copy so we can verify nothing canged + authCopyRaw, err := copystructure.Copy(auth) + if err != nil { + t.Fatal(err) + } + authCopy := authCopyRaw.(*logical.Auth) + + reqCopyRaw, err := copystructure.Copy(req) + if err != nil { + t.Fatal(err) + } + reqCopy := reqCopyRaw.(*logical.Request) + + respCopyRaw, err := copystructure.Copy(resp) + if err != nil { + t.Fatal(err) + } + respCopy := respCopyRaw.(*logical.Response) + headersConf := &AuditedHeadersConfig{ Headers: make(map[string]*auditedHeaderSettings), } - err := b.LogResponse(auth, req, resp, headersConf, respErr) + err = b.LogResponse(authCopy, reqCopy, respCopy, headersConf, respErr) if err != nil { t.Fatalf("err: %v", err) } @@ -435,6 +464,13 @@ func TestAuditBroker_AuditHeaders(t *testing.T) { } respErr := fmt.Errorf("permission denied") + // Copy so we can verify nothing canged + reqCopyRaw, err := copystructure.Copy(req) + if err != nil { + t.Fatal(err) + } + reqCopy := reqCopyRaw.(*logical.Request) + headersConf := &AuditedHeadersConfig{ Headers: map[string]*auditedHeaderSettings{ "X-Test-Header": &auditedHeaderSettings{false}, @@ -442,7 +478,7 @@ func TestAuditBroker_AuditHeaders(t *testing.T) { }, } - err := b.LogRequest(auth, req, headersConf, respErr) + err = b.LogRequest(auth, reqCopy, headersConf, respErr) if err != nil { t.Fatalf("err: %v", err) } @@ -453,7 +489,7 @@ func TestAuditBroker_AuditHeaders(t *testing.T) { } for _, a := range []*NoopAudit{a1, a2} { - if !reflect.DeepEqual(a.Req[0].Headers, expected) { + if !reflect.DeepEqual(a.ReqHeaders[0], expected) { t.Fatalf("Bad audited headers: %#v", a.Req[0].Headers) } } From 31ddc2747253fbeed8b6f7000501be6849cef5f2 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Wed, 1 Feb 2017 23:18:49 -0800 Subject: [PATCH 17/20] PR feedback --- vault/audited_headers.go | 13 +++++++++++++ vault/logical_system.go | 20 ++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/vault/audited_headers.go b/vault/audited_headers.go index f52eb6c85d65..18b4aea3ca60 100644 --- a/vault/audited_headers.go +++ b/vault/audited_headers.go @@ -29,6 +29,11 @@ type AuditedHeadersConfig struct { // add adds or overwrites a header in the config and updates the barrier view func (a *AuditedHeadersConfig) add(header string, hmac bool) error { + if header == "" { + return fmt.Errorf("header value cannot be empty") + } + + // Grab a write lock a.Lock() defer a.Unlock() @@ -47,6 +52,11 @@ func (a *AuditedHeadersConfig) add(header string, hmac bool) error { // remove deletes a header out of the header config and updates the barrier view func (a *AuditedHeadersConfig) remove(header string) error { + if header == "" { + return fmt.Errorf("header value cannot be empty") + } + + // Grab a write lock a.Lock() defer a.Unlock() @@ -66,15 +76,18 @@ func (a *AuditedHeadersConfig) remove(header string) error { // ApplyConfig returns a map of approved headers and their values, either // hmac'ed or plaintext func (a *AuditedHeadersConfig) ApplyConfig(headers map[string][]string, hashFunc func(string) string) (result map[string][]string) { + // Grab a read lock a.RLock() defer a.RUnlock() result = make(map[string][]string, len(a.Headers)) for key, settings := range a.Headers { if val, ok := headers[key]; ok { + // copy the header values so we don't overwrite them hVals := make([]string, len(val)) copy(hVals, val) + // Optionally hmac the values if settings.HMAC { for i, el := range hVals { hVals[i] = hashFunc(el) diff --git a/vault/logical_system.go b/vault/logical_system.go index b1cea37a3f5d..6c8807d7ba79 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -670,41 +670,45 @@ type SystemBackend struct { // handleAuditedHeaderUpdate creates or overwrites a header entry func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - headerConfig := b.Core.AuditedHeadersConfig() - header := d.Get("header").(string) hmac := d.Get("hmac").(bool) if header == "" { return logical.ErrorResponse("missing header name"), nil } - headerConfig.add(header, hmac) + headerConfig := b.Core.AuditedHeadersConfig() + err := headerConfig.add(header, hmac) + if err != nil { + return nil, err + } return nil, nil } // handleAudtedHeaderDelete deletes the header with the given name func (b *SystemBackend) handleAuditedHeaderDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - headerConfig := b.Core.AuditedHeadersConfig() - header := d.Get("header").(string) if header == "" { return logical.ErrorResponse("missing header name"), nil } - headerConfig.remove(header) + + headerConfig := b.Core.AuditedHeadersConfig() + err := headerConfig.remove(header) + if err != nil { + return nil, err + } return nil, nil } // handleAuditedHeaderRead returns the header configuration for the given header name func (b *SystemBackend) handleAuditedHeaderRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - headerConfig := b.Core.AuditedHeadersConfig() - header := d.Get("header").(string) if header == "" { return logical.ErrorResponse("missing header name"), nil } + headerConfig := b.Core.AuditedHeadersConfig() settings, ok := headerConfig.Headers[header] if !ok { return logical.ErrorResponse("Could not find header in config"), nil From fe455bc14c14482a10c5ea76d9cb813006a0662f Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Thu, 2 Feb 2017 10:24:52 -0800 Subject: [PATCH 18/20] update the path and permissions on config/* paths --- vault/logical_system.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vault/logical_system.go b/vault/logical_system.go index 6c8807d7ba79..0ea2c31fc494 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -40,7 +40,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "audit/*", "raw/*", "rotate", - "config/*", + "config/auditing/*", }, Unauthenticated: []string{ @@ -624,7 +624,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "config/audited-headers/(?P
.+)", + Pattern: "config/auditing/request-headers/(?P
.+)", Fields: map[string]*framework.FieldSchema{ "header": &framework.FieldSchema{ @@ -645,7 +645,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen HelpDescription: strings.TrimSpace(sysHelp["rewrap"][1]), }, &framework.Path{ - Pattern: "config/audited-headers$", + Pattern: "config/auditing/request-headers$", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.handleAuditedHeadersRead, From 35bf2007950b58886a313ff45203daa0f7f6275d Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Thu, 2 Feb 2017 11:12:30 -0800 Subject: [PATCH 19/20] Add docs file --- .../docs/http/sys-config-auditing.html.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 website/source/docs/http/sys-config-auditing.html.md diff --git a/website/source/docs/http/sys-config-auditing.html.md b/website/source/docs/http/sys-config-auditing.html.md new file mode 100644 index 000000000000..ca69422619d4 --- /dev/null +++ b/website/source/docs/http/sys-config-auditing.html.md @@ -0,0 +1,133 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/config/auditing" +sidebar_current: "docs-http-audits-audits" +description: |- + The `/sys/config/auditing` endpoint is used to configure auditing settings. +--- + +# /sys/config/auditing/request-headers + +## GET + +
+
Description
+
+ List the request headers that are configured to be audited. _This endpoint requires `sudo` + capability._ +
+ +
Method
+
GET
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "headers":{ + "X-Forwarded-For": { + "hmac":true + } + } + } + ``` + +
+
+ +# /sys/config/auditing/request-headers/ + +## GET + +
+
Description
+
+ List the information for the given request header. _This endpoint requires `sudo` + capability._ +
+ +
Method
+
GET
+ +
URL
+
`/sys/config/auditing/request-headers/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "X-Forwarded-For":{ + "hmac":true + } + } + ``` + +
+
+ +## PUT + +
+
Description
+
+ Enable auditing of a header. _This endpoint requires `sudo` capability._ +
+ +
Method
+
PUT
+ +
URL
+
`/sys/config/auditing/request-headers/`
+ +
Parameters
+
+
    +
  • + hmac + optional + Bool, if this header's value should be hmac'ed in the audit logs. + Defaults to false. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ +## DELETE + +
+
Description
+
+ Disable auditing of the given request header. _This endpoint requires `sudo` + capability._ +
+ +
Method
+
DELETE
+ +
URL
+
`/sys/config/auditing/request-headers/`
+ +
Parameters
+
None +
+ +
Returns
+
`204` response code. +
+
From f02177aa9d5d56a80773e7b255518825a2cad921 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Thu, 2 Feb 2017 11:19:57 -0800 Subject: [PATCH 20/20] Fix TestSystemBackend_RootPaths test --- vault/logical_system_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 1717a437fb9d..62737744b21f 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -22,7 +22,7 @@ func TestSystemBackend_RootPaths(t *testing.T) { "audit/*", "raw/*", "rotate", - "config/*", + "config/auditing/*", } b := testSystemBackend(t)