Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure the request headers that are output to the audit log #2321

Merged
merged 20 commits into from
Feb 2, 2017
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions audit/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (f *AuditFormatter) FormatRequest(
Path: req.Path,
Data: req.Data,
RemoteAddr: getRemoteAddr(req),
Headers: req.Headers,
},
}

Expand Down Expand Up @@ -275,6 +276,7 @@ func (f *AuditFormatter) FormatResponse(
Path: req.Path,
Data: req.Data,
RemoteAddr: getRemoteAddr(req),
Headers: req.Headers,
},

Response: AuditResponse{
Expand Down Expand Up @@ -325,6 +327,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 {
Expand Down
5 changes: 4 additions & 1 deletion audit/format_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"}
`
5 changes: 4 additions & 1 deletion audit/format_jsonx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
"",
`<json:object name="auth"><json:string name="accessor"></json:string><json:string name="client_token"></json:string><json:string name="display_name"></json:string><json:null name="metadata" /><json:array name="policies"><json:string>root</json:string></json:array></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token"></json:string><json:string name="client_token_accessor"></json:string><json:null name="data" /><json:string name="id"></json:string><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
`<json:object name="auth"><json:string name="accessor"></json:string><json:string name="client_token"></json:string><json:string name="display_name"></json:string><json:null name="metadata" /><json:array name="policies"><json:string>root</json:string></json:array></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token"></json:string><json:string name="client_token_accessor"></json:string><json:null name="data" /><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id"></json:string><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
},
}

Expand Down
3 changes: 1 addition & 2 deletions builtin/audit/syslog/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ 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, err error) error {
var buf bytes.Buffer
if err := b.formatter.FormatResponse(&buf, b.formatConfig, auth, req, resp, err); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions http/logical.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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)
Expand Down
5 changes: 5 additions & 0 deletions logical/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ type Request struct {
// to represent the auth that was returned prior.
Auth *Auth `json:"auth" structs:"auth" mapstructure:"auth"`

// 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
// inspect the connection information and potentially use it for
// authentication/protection.
Expand Down
20 changes: 18 additions & 2 deletions vault/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 *AuditedHeadersConfig, outerErr error) (retErr error) {
defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now())
a.RLock()
defer a.RUnlock()
Expand All @@ -426,9 +426,17 @@ func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, outer
// 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, outerErr)
metrics.MeasureSince([]string{"audit", name, "log_request"}, start)
Expand All @@ -448,7 +456,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 *AuditedHeadersConfig, err error) (reterr error) {
defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now())
a.RLock()
defer a.RUnlock()
Expand All @@ -459,9 +467,17 @@ 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, err)
metrics.MeasureSince([]string{"audit", name, "log_response"}, start)
Expand Down
141 changes: 128 additions & 13 deletions vault/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ 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 {
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
Expand All @@ -33,6 +35,7 @@ type NoopAudit struct {
func (n *NoopAudit) LogRequest(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)
return n.ReqErr
}
Expand Down Expand Up @@ -287,16 +290,33 @@ 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")

err = b.LogRequest(auth, req, reqErrs)
headersConf := &AuditedHeadersConfig{
Headers: make(map[string]*auditedHeaderSettings),
}

err = b.LogRequest(authCopy, reqCopy, headersConf, reqErrs)
if err != nil {
t.Fatalf("err: %v", err)
}
Expand All @@ -306,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])
Expand All @@ -315,13 +335,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)
}
}
Expand Down Expand Up @@ -359,7 +379,30 @@ func TestAuditBroker_LogResponse(t *testing.T) {
}
respErr := fmt.Errorf("permission denied")

err := b.LogResponse(auth, req, resp, respErr)
// 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(authCopy, reqCopy, respCopy, headersConf, respErr)
if err != nil {
t.Fatalf("err: %v", err)
}
Expand All @@ -381,15 +424,87 @@ 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)
}
}

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

// 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},
"X-Vault-Header": &auditedHeaderSettings{false},
},
}

err = b.LogRequest(auth, reqCopy, 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.ReqHeaders[0], 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)
}
}
Loading