diff --git a/http/handler.go b/http/handler.go index 7a8d2023e871..6d10a5bc113a 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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 diff --git a/http/http_test.go b/http/http_test.go index 1f2b20444762..e37b9c3d7693 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -22,32 +22,36 @@ func testHttpGet(t *testing.T, token string, addr string) *http.Response { loggedToken = "" } 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) } diff --git a/http/logical.go b/http/logical.go index 7f0e9ab73f1c..ac9326af7e5b 100644 --- a/http/logical.go +++ b/http/logical.go @@ -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 { diff --git a/http/logical_test.go b/http/logical_test.go index f7498733ec6d..df385bd05ee4 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -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) + } + } +} diff --git a/sdk/helper/consts/error.go b/sdk/helper/consts/error.go index 06977d5d5a4c..d4e60e54e347 100644 --- a/sdk/helper/consts/error.go +++ b/sdk/helper/consts/error.go @@ -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") ) diff --git a/vault/audit_test.go b/vault/audit_test.go index dd7bba4cf069..c0d1f4149c40 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -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) { diff --git a/vault/core_test.go b/vault/core_test.go index 6e29a89c0129..11ed21b72c86 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -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{} diff --git a/vault/testing.go b/vault/testing.go index e8fc5a2abf17..9e19db4cd631 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -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 +} diff --git a/vault/wrapping.go b/vault/wrapping.go index b6fa0a35156f..a861e14ed26a 100644 --- a/vault/wrapping.go +++ b/vault/wrapping.go @@ -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 }