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

core: revoke the proper token on partial failures from token-related requests #7835

Merged
merged 4 commits into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 45 additions & 0 deletions http/logical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,48 @@ func TestLogical_Audit_invalidWrappingToken(t *testing.T) {
}
}
}

func TestLogical_CreateToken_Failure(t *testing.T) {
core, _, root := vault.TestCoreUnsealed(t)

ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, root)

// Create a root token and use that for subsequent requests
resp := testHttpPut(t, root, addr+"/v1/auth/token/create", map[string]interface{}{
"policies": []string{"root"},
})
testResponseStatus(t, resp, 200)

var actual map[string]interface{}
testResponseBody(t, resp, &actual)
tokenWithRootPolicy := actual["auth"].(map[string]interface{})["client_token"].(string)
if tokenWithRootPolicy == "" {
t.Fatalf("expected a client_token string but not found\n%#v", actual)
}

// Use new token to create yet a new token, this should succeed
resp = testHttpPut(t, tokenWithRootPolicy, addr+"/v1/auth/token/create", nil)
testResponseStatus(t, resp, 200)

// Try again but force failure on RegisterAuth to simulate a network failure
// when registering the lease (e.g. a storage failure). This should trigger
// an expiration manager cleanup on the newly created token
core.SetTestRegisterAuthFailure(true)
resp = testHttpPut(t, tokenWithRootPolicy, addr+"/v1/auth/token/create", nil)
testResponseStatus(t, resp, 500)
core.SetTestRegisterAuthFailure(false)

// Do a lookup against the client token that we used for the failed request.
// It should still be present.
resp = testHttpPost(t, root, addr+"/v1/auth/token/lookup", map[string]interface{}{
"token": tokenWithRootPolicy,
})
testResponseStatus(t, resp, 200)

// Do a token creation request with the token to ensure that it's still
// valid, should succeed.
resp = testHttpPut(t, tokenWithRootPolicy, addr+"/v1/auth/token/create", nil)
testResponseStatus(t, resp, 200)
}
14 changes: 14 additions & 0 deletions vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/hashicorp/vault/vault/seal"
shamirseal "github.com/hashicorp/vault/vault/seal/shamir"
cache "github.com/patrickmn/go-cache"
uberAtomic "go.uber.org/atomic"
"google.golang.org/grpc"
)

Expand Down Expand Up @@ -478,6 +479,11 @@ type Core struct {
secureRandomReader io.Reader

recoveryMode bool

// testRegisterAuthFailure, if set to true, triggers an explicit failure on
// RegisterAuth to simulate a partial failure during a token creation
// request. This value should only be set by tests.
testRegisterAuthFailure uberAtomic.Bool
}

// CoreConfig is used to parameterize a core
Expand Down Expand Up @@ -2175,6 +2181,14 @@ func (c *Core) MetricsHelper() *metricsutil.MetricsHelper {
return c.metricsHelper
}

// SetTestRegisterAuthFailure sets the an internal atomic bool within
calvn marked this conversation as resolved.
Show resolved Hide resolved
// core to trigger failure on its expiration manager's RegisterAuth
// method to simulate partial failure during a token creation request.
// This method should only be used by tests.
func (c *Core) SetTestRegisterAuthFailure(active bool) {
c.testRegisterAuthFailure.Store(active)
}

// BuiltinRegistry is an interface that allows the "vault" package to use
// the registry of builtin plugins without getting an import cycle. It
// also allows for mocking the registry easily.
Expand Down
8 changes: 7 additions & 1 deletion vault/expiration.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const (
// defaultLeaseDuration is the default lease duration used when no lease is specified
defaultLeaseTTL = maxLeaseTTL

//maxLeaseThreshold is the maximum lease count before generating log warning
// maxLeaseThreshold is the maximum lease count before generating log warning
maxLeaseThreshold = 256000
)

Expand Down Expand Up @@ -1147,6 +1147,12 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request,
func (m *ExpirationManager) RegisterAuth(ctx context.Context, te *logical.TokenEntry, auth *logical.Auth) error {
defer metrics.MeasureSince([]string{"expire", "register-auth"}, time.Now())

// Triggers failure of RegisterAuth. This should only be triggered by tests
// to simulate partial failure during a token creation request.
if m.core.testRegisterAuthFailure.Load() {
return fmt.Errorf("failing explicitly on RegisterAuth")
}

authExpirationTime := auth.ExpirationTime()

if te.TTL == 0 && authExpirationTime.IsZero() && (len(te.Policies) != 1 || te.Policies[0] != "root") {
Expand Down
16 changes: 13 additions & 3 deletions vault/request_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,11 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp

_, identityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, tokenNS, resp.Auth.EntityID)
if err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably should log the error here as well?

// Best-effort clean up on error, so we log the cleanup error as a
// warning but still return as internal error.
if err := c.tokenStore.revokeOrphan(ctx, resp.Auth.ClientToken); err != nil {
c.logger.Warn("failed to clean up token lease", "request_path", req.Path, "error", err)
}
return nil, nil, ErrInternalError
}

Expand All @@ -874,7 +878,11 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp
Path: resp.Auth.CreationPath,
NamespaceID: ns.ID,
}, resp.Auth); err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
// Best-effort clean up on error, so we log the cleanup error as
// a warning but still return as internal error.
if err := c.tokenStore.revokeOrphan(ctx, resp.Auth.ClientToken); err != nil {
c.logger.Warn("failed to clean up token lease", "request_path", req.Path, "error", err)
}
c.logger.Error("failed to register token lease", "request_path", req.Path, "error", err)
retErr = multierror.Append(retErr, ErrInternalError)
return nil, auth, retErr
Expand Down Expand Up @@ -1209,7 +1217,9 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st
case logical.TokenTypeService:
// Register with the expiration manager
if err := c.expiration.RegisterAuth(ctx, &te, auth); err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
if err := c.tokenStore.revokeOrphan(ctx, te.ID); err != nil {
c.logger.Warn("failed to clean up token lease", "request_path", path, "error", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps failing here should produce a slightly different log message to make orphan revocation by ID distinct from revocation by client token lease. Something like failed to clean up token lease - id not found vs failed to clean up token lease - not found. If err will be sufficiently unique and the logger in fact captures that output, then this isn't necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to the difference between using te.ID here and resp.Auth.ClientToken above? The revoke orphan call here ultimately points to to the same thing (i.e. the token ID that we're trying to revoke) since the auth that we pass into this func is from resp.Auth and we set auth.ClientToken = te.ID up in L1208. The revocation is not by client token lease ID.

The reason why we're able to use te.ID is because we instantiate a new instance of te within the func, and in all three revoke orphan cases we're now referencing the token ID that's being created instead of the request's client token.

Copy link

@roberteckert roberteckert Nov 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was indeed referring to te.ID vs resp.Auth.ClientToken. If it's all the same value and call then the messaging is sensible. Thanks.

}
c.logger.Error("failed to register token lease", "request_path", path, "error", err)
return ErrInternalError
}
Expand Down