audit: log invalid wrapping token request/response (#6541)

* audit: log invalid wrapping token request/response

* Update helper/consts/error.go

Co-Authored-By: calvn <cleung2010@gmail.com>

* update error comments

* Update vault/wrapping.go

Co-Authored-By: calvn <cleung2010@gmail.com>

* update comment

* move validateWrappingToken out of http and into logical

* minor refactor, add test cases

* comment rewording

* refactor validateWrappingToken to perform audit logging

* move ValidateWrappingToken back to wrappingVerificationFunc

* Fix tests

* Review feedback
This commit is contained in:
Calvin Leung Huang
2019-07-05 14:15:14 -07:00
committed by Brian Kassouf
parent 3cc7f4a68c
commit c3f0f96e7e
9 changed files with 232 additions and 100 deletions

View File

@@ -308,7 +308,7 @@ func wrappingVerificationFunc(ctx context.Context, core *vault.Core, req *logica
return errwrap.Wrapf("error validating wrapping token: {{err}}", err)
}
if !valid {
return fmt.Errorf("wrapping token is not valid or does not exist")
return consts.ErrInvalidWrappingToken
}
return nil

View File

@@ -22,32 +22,36 @@ func testHttpGet(t *testing.T, token string, addr string) *http.Response {
loggedToken = "<empty>"
}
t.Logf("Token is %s", loggedToken)
return testHttpData(t, "GET", token, addr, nil, false)
return testHttpData(t, "GET", token, addr, nil, false, 0)
}
func testHttpDelete(t *testing.T, token string, addr string) *http.Response {
return testHttpData(t, "DELETE", token, addr, nil, false)
return testHttpData(t, "DELETE", token, addr, nil, false, 0)
}
// Go 1.8+ clients redirect automatically which breaks our 307 standby testing
func testHttpDeleteDisableRedirect(t *testing.T, token string, addr string) *http.Response {
return testHttpData(t, "DELETE", token, addr, nil, true)
return testHttpData(t, "DELETE", token, addr, nil, true, 0)
}
func testHttpPostWrapped(t *testing.T, token string, addr string, body interface{}, wrapTTL time.Duration) *http.Response {
return testHttpData(t, "POST", token, addr, body, false, wrapTTL)
}
func testHttpPost(t *testing.T, token string, addr string, body interface{}) *http.Response {
return testHttpData(t, "POST", token, addr, body, false)
return testHttpData(t, "POST", token, addr, body, false, 0)
}
func testHttpPut(t *testing.T, token string, addr string, body interface{}) *http.Response {
return testHttpData(t, "PUT", token, addr, body, false)
return testHttpData(t, "PUT", token, addr, body, false, 0)
}
// Go 1.8+ clients redirect automatically which breaks our 307 standby testing
func testHttpPutDisableRedirect(t *testing.T, token string, addr string, body interface{}) *http.Response {
return testHttpData(t, "PUT", token, addr, body, true)
return testHttpData(t, "PUT", token, addr, body, true, 0)
}
func testHttpData(t *testing.T, method string, token string, addr string, body interface{}, disableRedirect bool) *http.Response {
func testHttpData(t *testing.T, method string, token string, addr string, body interface{}, disableRedirect bool, wrapTTL time.Duration) *http.Response {
bodyReader := new(bytes.Buffer)
if body != nil {
enc := json.NewEncoder(bodyReader)
@@ -68,6 +72,10 @@ func testHttpData(t *testing.T, method string, token string, addr string, body i
req.Header.Set("Content-Type", "application/json")
if wrapTTL > 0 {
req.Header.Set("X-Vault-Wrap-TTL", wrapTTL.String())
}
if len(token) != 0 {
req.Header.Set(consts.AuthHeaderName, token)
}

View File

@@ -12,7 +12,7 @@ import (
"time"
"github.com/hashicorp/errwrap"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
@@ -208,6 +208,7 @@ func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool) http.H
}
}
switch req.Path {
// Route the token wrapping request to its respective sys NS
case "sys/wrapping/lookup", "sys/wrapping/rewrap", "sys/wrapping/unwrap":
r = r.WithContext(newCtx)
if err := wrappingVerificationFunc(r.Context(), core, req); err != nil {

View File

@@ -2,6 +2,7 @@ package http
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
@@ -16,6 +17,7 @@ import (
"github.com/go-test/deep"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/logging"
@@ -347,3 +349,91 @@ func TestLogical_RespondWithStatusCode(t *testing.T) {
t.Fatalf("bad response: %s", string(bodyRaw[:]))
}
}
func TestLogical_Audit_invalidWrappingToken(t *testing.T) {
// Create a noop audit backend
var noop *vault.NoopAudit
c, _, root := vault.TestCoreUnsealedWithConfig(t, &vault.CoreConfig{
AuditBackends: map[string]audit.Factory{
"noop": func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) {
noop = &vault.NoopAudit{
Config: config,
}
return noop, nil
},
},
})
ln, addr := TestServer(t, c)
defer ln.Close()
// Enable the audit backend
resp := testHttpPost(t, root, addr+"/v1/sys/audit/noop", map[string]interface{}{
"type": "noop",
})
testResponseStatus(t, resp, 204)
{
// Make a wrapping/unwrap request with an invalid token
resp := testHttpPost(t, root, addr+"/v1/sys/wrapping/unwrap", map[string]interface{}{
"token": "foo",
})
testResponseStatus(t, resp, 400)
body := map[string][]string{}
testResponseBody(t, resp, &body)
if body["errors"][0] != "wrapping token is not valid or does not exist" {
t.Fatal(body)
}
// Check the audit trail on request and response
if len(noop.ReqAuth) != 1 {
t.Fatalf("bad: %#v", noop)
}
auth := noop.ReqAuth[0]
if auth.ClientToken != root {
t.Fatalf("bad client token: %#v", auth)
}
if len(noop.Req) != 1 || noop.Req[0].Path != "sys/wrapping/unwrap" {
t.Fatalf("bad:\ngot:\n%#v", noop.Req[0])
}
if len(noop.ReqErrs) != 1 {
t.Fatalf("bad: %#v", noop.RespErrs)
}
if noop.ReqErrs[0] != consts.ErrInvalidWrappingToken {
t.Fatalf("bad: %#v", noop.ReqErrs)
}
}
{
resp := testHttpPostWrapped(t, root, addr+"/v1/auth/token/create", nil, 10*time.Second)
testResponseStatus(t, resp, 200)
body := map[string]interface{}{}
testResponseBody(t, resp, &body)
wrapToken := body["wrap_info"].(map[string]interface{})["token"].(string)
// Make a wrapping/unwrap request with an invalid token
resp = testHttpPost(t, root, addr+"/v1/sys/wrapping/unwrap", map[string]interface{}{
"token": wrapToken,
})
testResponseStatus(t, resp, 200)
// Check the audit trail on request and response
if len(noop.ReqAuth) != 3 {
t.Fatalf("bad: %#v", noop)
}
auth := noop.ReqAuth[2]
if auth.ClientToken != root {
t.Fatalf("bad client token: %#v", auth)
}
if len(noop.Req) != 3 || noop.Req[2].Path != "sys/wrapping/unwrap" {
t.Fatalf("bad:\ngot:\n%#v", noop.Req[2])
}
// Make sure there is only one error in the logs
if noop.ReqErrs[1] != nil || noop.ReqErrs[2] != nil {
t.Fatalf("bad: %#v", noop.RespErrs)
}
}
}

View File

@@ -11,6 +11,11 @@ var (
// No operation is expected to succeed until active.
ErrStandby = errors.New("Vault is in standby mode")
// Used when .. is used in a path
// ErrPathContainsParentReferences is returned when a path contains parent
// references.
ErrPathContainsParentReferences = errors.New("path cannot contain parent references")
// ErrInvalidWrappingToken is returned when checking for the validity of
// a wrapping token that turns out to be invalid.
ErrInvalidWrappingToken = errors.New("wrapping token is not valid or does not exist")
)

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"reflect"
"strings"
"sync"
"testing"
"time"
@@ -18,93 +17,10 @@ import (
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
)
type NoopAudit struct {
Config *audit.BackendConfig
ReqErr error
ReqAuth []*logical.Auth
Req []*logical.Request
ReqHeaders []map[string][]string
ReqNonHMACKeys []string
ReqErrs []error
RespErr error
RespAuth []*logical.Auth
RespReq []*logical.Request
Resp []*logical.Response
RespNonHMACKeys []string
RespReqNonHMACKeys []string
RespErrs []error
salt *salt.Salt
saltMutex sync.RWMutex
}
func (n *NoopAudit) LogRequest(ctx context.Context, in *logical.LogInput) error {
n.ReqAuth = append(n.ReqAuth, in.Auth)
n.Req = append(n.Req, in.Request)
n.ReqHeaders = append(n.ReqHeaders, in.Request.Headers)
n.ReqNonHMACKeys = in.NonHMACReqDataKeys
n.ReqErrs = append(n.ReqErrs, in.OuterErr)
return n.ReqErr
}
func (n *NoopAudit) LogResponse(ctx context.Context, in *logical.LogInput) error {
n.RespAuth = append(n.RespAuth, in.Auth)
n.RespReq = append(n.RespReq, in.Request)
n.Resp = append(n.Resp, in.Response)
n.RespErrs = append(n.RespErrs, in.OuterErr)
if in.Response != nil {
n.RespNonHMACKeys = in.NonHMACRespDataKeys
n.RespReqNonHMACKeys = in.NonHMACReqDataKeys
}
return n.RespErr
}
func (n *NoopAudit) Salt(ctx context.Context) (*salt.Salt, error) {
n.saltMutex.RLock()
if n.salt != nil {
defer n.saltMutex.RUnlock()
return n.salt, nil
}
n.saltMutex.RUnlock()
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
if n.salt != nil {
return n.salt, nil
}
salt, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig)
if err != nil {
return nil, err
}
n.salt = salt
return salt, nil
}
func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) {
salt, err := n.Salt(ctx)
if err != nil {
return "", err
}
return salt.GetIdentifiedHMAC(data), nil
}
func (n *NoopAudit) Reload(ctx context.Context) error {
return nil
}
func (n *NoopAudit) Invalidate(ctx context.Context) {
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
n.salt = nil
}
func TestAudit_ReadOnlyViewDuringMount(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) {

View File

@@ -9,7 +9,7 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/consts"
@@ -904,7 +904,6 @@ func TestCore_HandleRequest_AuditTrail_noHMACKeys(t *testing.T) {
}
}
// Ensure we get a client token
func TestCore_HandleLogin_AuditTrail(t *testing.T) {
// Create a badass credential backend that always logs in as armon
noop := &NoopAudit{}

View File

@@ -1739,3 +1739,85 @@ func (m *mockBuiltinRegistry) Keys(pluginType consts.PluginType) []string {
func (m *mockBuiltinRegistry) Contains(name string, pluginType consts.PluginType) bool {
return false
}
type NoopAudit struct {
Config *audit.BackendConfig
ReqErr error
ReqAuth []*logical.Auth
Req []*logical.Request
ReqHeaders []map[string][]string
ReqNonHMACKeys []string
ReqErrs []error
RespErr error
RespAuth []*logical.Auth
RespReq []*logical.Request
Resp []*logical.Response
RespNonHMACKeys []string
RespReqNonHMACKeys []string
RespErrs []error
salt *salt.Salt
saltMutex sync.RWMutex
}
func (n *NoopAudit) LogRequest(ctx context.Context, in *logical.LogInput) error {
n.ReqAuth = append(n.ReqAuth, in.Auth)
n.Req = append(n.Req, in.Request)
n.ReqHeaders = append(n.ReqHeaders, in.Request.Headers)
n.ReqNonHMACKeys = in.NonHMACReqDataKeys
n.ReqErrs = append(n.ReqErrs, in.OuterErr)
return n.ReqErr
}
func (n *NoopAudit) LogResponse(ctx context.Context, in *logical.LogInput) error {
n.RespAuth = append(n.RespAuth, in.Auth)
n.RespReq = append(n.RespReq, in.Request)
n.Resp = append(n.Resp, in.Response)
n.RespErrs = append(n.RespErrs, in.OuterErr)
if in.Response != nil {
n.RespNonHMACKeys = in.NonHMACRespDataKeys
n.RespReqNonHMACKeys = in.NonHMACReqDataKeys
}
return n.RespErr
}
func (n *NoopAudit) Salt(ctx context.Context) (*salt.Salt, error) {
n.saltMutex.RLock()
if n.salt != nil {
defer n.saltMutex.RUnlock()
return n.salt, nil
}
n.saltMutex.RUnlock()
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
if n.salt != nil {
return n.salt, nil
}
salt, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig)
if err != nil {
return nil, err
}
n.salt = salt
return salt, nil
}
func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) {
salt, err := n.Salt(ctx)
if err != nil {
return "", err
}
return salt.GetIdentifiedHMAC(data), nil
}
func (n *NoopAudit) Reload(ctx context.Context) error {
return nil
}
func (n *NoopAudit) Invalidate(ctx context.Context) {
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
n.salt = nil
}

View File

@@ -309,16 +309,46 @@ DONELISTHANDLING:
return nil, nil
}
// ValidateWrappingToken checks whether a token is a wrapping token.
func (c *Core) ValidateWrappingToken(ctx context.Context, req *logical.Request) (bool, error) {
// validateWrappingToken checks whether a token is a wrapping token. The passed
// in logical request will be updated if the wrapping token was provided within
// a JWT token.
func (c *Core) ValidateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) {
defer func() {
// Perform audit logging before returning if there's an issue with checking
// the wrapping token
if err != nil || !valid {
// We log the Auth object like so here since the wrapping token can
// come from the header, which gets set as the ClientToken
auth := &logical.Auth{
ClientToken: req.ClientToken,
Accessor: req.ClientTokenAccessor,
}
logInput := &logical.LogInput{
Auth: auth,
Request: req,
}
if err != nil {
logInput.OuterErr = errors.New("error validating wrapping token")
}
if !valid {
logInput.OuterErr = consts.ErrInvalidWrappingToken
}
if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil {
c.logger.Error("failed to audit request", "path", req.Path, "error", err)
}
}
}()
if req == nil {
return false, fmt.Errorf("invalid request")
}
var err error
var token string
var thirdParty bool
// Check if the wrapping token is coming from the request body, and if not
// assume that req.ClientToken is the wrapping token
if req.Data != nil && req.Data["token"] != nil {
thirdParty = true
if tokenStr, ok := req.Data["token"].(string); !ok {
@@ -365,6 +395,7 @@ func (c *Core) ValidateWrappingToken(ctx context.Context, req *logical.Request)
} else {
req.Data["token"] = claims.ID
}
token = claims.ID
}