From 67a746be30323b98a2d3eda9433955bb19e293d5 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 11 May 2016 16:51:18 -0400 Subject: [PATCH] Add explicit maximum TTLs to token store roles. --- http/logical_test.go | 21 +- http/sys_generate_root_test.go | 42 ++-- vault/token_store.go | 190 +++++++++++---- vault/token_store_test.go | 324 +++++++++++++++++++++---- website/source/docs/auth/token.html.md | 12 +- 5 files changed, 460 insertions(+), 129 deletions(-) diff --git a/http/logical_test.go b/http/logical_test.go index c99f0461da..da23f70819 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -128,16 +128,17 @@ func TestLogical_StandbyRedirect(t *testing.T) { "renewable": false, "lease_duration": float64(0), "data": map[string]interface{}{ - "meta": nil, - "num_uses": float64(0), - "path": "auth/token/root", - "policies": []interface{}{"root"}, - "display_name": "root", - "orphan": true, - "id": root, - "ttl": float64(0), - "creation_ttl": float64(0), - "role": "", + "meta": nil, + "num_uses": float64(0), + "path": "auth/token/root", + "policies": []interface{}{"root"}, + "display_name": "root", + "orphan": true, + "id": root, + "ttl": float64(0), + "creation_ttl": float64(0), + "role": "", + "explicit_max_ttl": float64(0), }, "warnings": nilWarnings, "auth": nil, diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index 70cc3fed3b..768c0ef16e 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -293,16 +293,17 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) { actual = map[string]interface{}{} expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": float64(0), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": float64(0), - "ttl": float64(0), - "path": "auth/token/root", - "role": "", + "id": newRootToken, + "display_name": "root", + "meta": interface{}(nil), + "num_uses": float64(0), + "policies": []interface{}{"root"}, + "orphan": true, + "creation_ttl": float64(0), + "ttl": float64(0), + "path": "auth/token/root", + "role": "", + "explicit_max_ttl": float64(0), } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") @@ -375,16 +376,17 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { actual = map[string]interface{}{} expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": float64(0), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": float64(0), - "ttl": float64(0), - "path": "auth/token/root", - "role": "", + "id": newRootToken, + "display_name": "root", + "meta": interface{}(nil), + "num_uses": float64(0), + "policies": []interface{}{"root"}, + "orphan": true, + "creation_ttl": float64(0), + "ttl": float64(0), + "path": "auth/token/root", + "role": "", + "explicit_max_ttl": float64(0), } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") diff --git a/vault/token_store.go b/vault/token_store.go index 2c8631763f..535dc2245c 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -9,8 +9,8 @@ import ( "time" "github.com/armon/go-metrics" - "github.com/fatih/structs" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" @@ -140,6 +140,12 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) Default: "", Description: tokenPathSuffixHelp + pathSuffixSanitize.String(), }, + + "explicit_max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: tokenExplicitMaxTTLHelp, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -394,17 +400,18 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) // TokenEntry is used to represent a given token type TokenEntry struct { - ID string // ID of this entry, generally a random UUID - Accessor string // Accessor for this token, a random UUID - Parent string // Parent token, used for revocation trees - Policies []string // Which named policies should be used - Path string // Used for audit trails, this is something like "auth/user/login" - Meta map[string]string // Used for auditing. This could include things like "source", "user", "ip" - DisplayName string // Used for operators to be able to associate with the source - NumUses int // Used to restrict the number of uses (zero is unlimited). This is to support one-time-tokens (generalized). - CreationTime int64 // Time of token creation - TTL time.Duration // Duration set when token was created - Role string // If set, the role that was used for parameters at creation time + ID string // ID of this entry, generally a random UUID + Accessor string // Accessor for this token, a random UUID + Parent string // Parent token, used for revocation trees + Policies []string // Which named policies should be used + Path string // Used for audit trails, this is something like "auth/user/login" + Meta map[string]string // Used for auditing. This could include things like "source", "user", "ip" + DisplayName string // Used for operators to be able to associate with the source + NumUses int // Used to restrict the number of uses (zero is unlimited). This is to support one-time-tokens (generalized). + CreationTime int64 // Time of token creation + TTL time.Duration // Duration set when token was created + ExplicitMaxTTL time.Duration // Explicit maximum TTL on the token + Role string // If set, the role that was used for parameters at creation time } // tsRoleEntry contains token store role information @@ -424,8 +431,12 @@ type tsRoleEntry struct { Period time.Duration `json:"period" mapstructure:"period" structs:"period"` // If set, a suffix will be set on the token path, making it easier to - // revoke using 'revoke-prefix'. + // revoke using 'revoke-prefix' PathSuffix string `json:"path_suffix" mapstructure:"path_suffix" structs:"path_suffix"` + + // If set, the token entry will have an explicit maximum TTL set, rather + // than deferring to role/mount values + ExplicitMaxTTL time.Duration `json:"explicit_max_ttl" mapstructure:"explicit_max_ttl" structs:"explicit_max_ttl"` } // SetExpirationManager is used to provide the token store with @@ -926,8 +937,12 @@ func (ts *TokenStore) handleCreateCommon( if len(data.Policies) == 0 { data.Policies = role.AllowedPolicies } else { - if !strutil.StrListSubset(role.AllowedPolicies, data.Policies) { - return logical.ErrorResponse("token policies must be subset of the role's allowed policies"), logical.ErrInvalidRequest + // Sanitize passed-in and role policies before comparison + sanitizedInputPolicies := policyutil.SanitizePolicies(data.Policies) + sanitizedRolePolicies := policyutil.SanitizePolicies(role.AllowedPolicies) + + if !strutil.StrListSubset(sanitizedRolePolicies, sanitizedInputPolicies) { + return logical.ErrorResponse(fmt.Sprintf("token policies (%v) must be subset of the role's allowed policies (%v)", sanitizedInputPolicies, sanitizedRolePolicies)), logical.ErrInvalidRequest } } @@ -936,8 +951,14 @@ func (ts *TokenStore) handleCreateCommon( // When a role is not in use, only permit policies to be a subset unless // the client has root or sudo privileges - case !isSudo && !strutil.StrListSubset(parent.Policies, data.Policies): - return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest + case !isSudo: + // Sanitize passed-in and parent policies before comparison + sanitizedInputPolicies := policyutil.SanitizePolicies(data.Policies) + sanitizedParentPolicies := policyutil.SanitizePolicies(parent.Policies) + + if !strutil.StrListSubset(sanitizedParentPolicies, sanitizedInputPolicies) { + return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest + } } // Use a map to filter out/prevent duplicates @@ -996,6 +1017,7 @@ func (ts *TokenStore) handleCreateCommon( } te.TTL = dur } else if data.Lease != "" { + // This block is compatibility dur, err := time.ParseDuration(data.Lease) if err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest @@ -1019,24 +1041,44 @@ func (ts *TokenStore) handleCreateCommon( } } + resp := &logical.Response{} + + if role != nil && role.ExplicitMaxTTL != 0 { + sysView := ts.System() + + // Limit the lease duration + if sysView.MaxLeaseTTL() != time.Duration(0) && te.ExplicitMaxTTL > sysView.MaxLeaseTTL() { + return logical.ErrorResponse(fmt.Sprintf( + "role explicit max TTL of %d is greater than system/mount allowed value of %d seconds", + te.ExplicitMaxTTL.Seconds(), sysView.MaxLeaseTTL().Seconds())), logical.ErrInvalidRequest + } + + te.ExplicitMaxTTL = role.ExplicitMaxTTL + + if te.TTL > te.ExplicitMaxTTL { + resp.AddWarning(fmt.Sprintf( + "Requested TTL higher than role explicit max TTL; value being capped to %d seconds", + te.ExplicitMaxTTL.Seconds())) + te.TTL = te.ExplicitMaxTTL + } + } + // Create the token if err := ts.create(&te); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } // Generate the response - resp := &logical.Response{ - Auth: &logical.Auth{ - DisplayName: te.DisplayName, - Policies: te.Policies, - Metadata: te.Meta, - LeaseOptions: logical.LeaseOptions{ - TTL: te.TTL, - Renewable: true, - }, - ClientToken: te.ID, - Accessor: te.Accessor, + resp.Auth = &logical.Auth{ + DisplayName: te.DisplayName, + Policies: te.Policies, + Metadata: te.Meta, + LeaseOptions: logical.LeaseOptions{ + TTL: te.TTL, + Renewable: true, }, + ClientToken: te.ID, + Accessor: te.Accessor, } if ts.policyLookupFunc != nil { @@ -1159,18 +1201,19 @@ func (ts *TokenStore) handleLookup( // you could escalate your privileges. resp := &logical.Response{ Data: map[string]interface{}{ - "id": out.ID, - "accessor": out.Accessor, - "policies": out.Policies, - "path": out.Path, - "meta": out.Meta, - "display_name": out.DisplayName, - "num_uses": out.NumUses, - "orphan": false, - "creation_time": int64(out.CreationTime), - "creation_ttl": int64(out.TTL.Seconds()), - "ttl": int64(0), - "role": out.Role, + "id": out.ID, + "accessor": out.Accessor, + "policies": out.Policies, + "path": out.Path, + "meta": out.Meta, + "display_name": out.DisplayName, + "num_uses": out.NumUses, + "orphan": false, + "creation_time": int64(out.CreationTime), + "creation_ttl": int64(out.TTL.Seconds()), + "ttl": int64(0), + "role": out.Role, + "explicit_max_ttl": int64(out.ExplicitMaxTTL.Seconds()), }, } @@ -1246,8 +1289,6 @@ func (ts *TokenStore) authRenew( return nil, fmt.Errorf("request auth is nil") } - f := framework.LeaseExtend(req.Auth.Increment, 0, ts.System()) - te, err := ts.Lookup(req.Auth.ClientToken) if err != nil { return nil, fmt.Errorf("error looking up token: %s", err) @@ -1256,6 +1297,8 @@ func (ts *TokenStore) authRenew( return nil, fmt.Errorf("no token entry found during lookup") } + f := framework.LeaseExtend(req.Auth.Increment, te.ExplicitMaxTTL, ts.System()) + // No role? Use normal LeaseExtend semantics if te.Role == "" { return f(req, d) @@ -1274,7 +1317,13 @@ func (ts *TokenStore) authRenew( // periodic token is always the same (the role's period value). It is not // subject to normal maximum TTL checks that would come from calling // LeaseExtend, so we fast path it. - if role.Period != 0 { + // + // The one wrinkle here is if the token has an explicit max TTL. Roles + // don't support having both configured, but they could be changed. We + // don't support tokens that are both periodic and have an explicit max + // TTL, so if the token has one, we treat it as a regular token even if the + // role is periodic. + if role.Period != 0 && te.ExplicitMaxTTL == 0 { req.Auth.TTL = role.Period return &logical.Response{Auth: req.Auth}, nil } @@ -1335,12 +1384,14 @@ func (ts *TokenStore) tokenStoreRoleRead( } resp := &logical.Response{ - Data: structs.New(role).Map(), - } - - // Make the period nicer - if role.Period != 0 { - resp.Data["period"] = role.Period.Seconds() + Data: map[string]interface{}{ + "period": int64(role.Period.Seconds()), + "explicit_max_ttl": int64(role.ExplicitMaxTTL.Seconds()), + "allowed_policies": role.AllowedPolicies, + "name": role.Name, + "orphan": role.Orphan, + "path_suffix": role.PathSuffix, + }, } return resp, nil @@ -1396,13 +1447,36 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate( entry.Period = time.Second * time.Duration(data.Get("period").(int)) } + var resp *logical.Response + + explicitMaxTTLInt, ok := data.GetOk("explicit_max_ttl") + if ok { + entry.ExplicitMaxTTL = time.Second * time.Duration(explicitMaxTTLInt.(int)) + } else if req.Operation == logical.CreateOperation { + entry.ExplicitMaxTTL = time.Second * time.Duration(data.Get("explicit_max_ttl").(int)) + } + if entry.ExplicitMaxTTL != 0 { + sysView := ts.System() + + if sysView.MaxLeaseTTL() != time.Duration(0) && entry.ExplicitMaxTTL > sysView.MaxLeaseTTL() { + if resp == nil { + resp = &logical.Response{} + } + resp.AddWarning(fmt.Sprintf( + "Given explicit max TTL of %d is greater than system/mount allowed value of %d seconds; until this is fixed attempting to create tokens against this role will result in an error", + entry.ExplicitMaxTTL.Seconds(), sysView.MaxLeaseTTL().Seconds())) + } + } + pathSuffixInt, ok := data.GetOk("path_suffix") if ok { pathSuffix := pathSuffixInt.(string) if pathSuffix != "" { matched := pathSuffixSanitize.MatchString(pathSuffix) if !matched { - return logical.ErrorResponse(fmt.Sprintf("given role path suffix contains invalid characters; must match %s", pathSuffixSanitize.String())), nil + return logical.ErrorResponse(fmt.Sprintf( + "given role path suffix contains invalid characters; must match %s", + pathSuffixSanitize.String())), nil } entry.PathSuffix = pathSuffix } @@ -1420,6 +1494,12 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate( entry.AllowedPolicies = strings.Split(data.Get("allowed_policies").(string), ",") } + // Explicit max TTLs and periods cannot be used at the same time since the + // purpose of a periodic token is to escape max TTL semantics + if entry.Period > 0 && entry.ExplicitMaxTTL > 0 { + return logical.ErrorResponse("a role cannot be used to issue both periodic tokens and tokens with explicit max TTLs"), logical.ErrInvalidRequest + } + // Store it jsonEntry, err := logical.StorageEntryJSON(fmt.Sprintf("%s%s", rolesPrefix, name), entry) if err != nil { @@ -1429,7 +1509,7 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate( return nil, err } - return nil, nil + return resp, nil } const ( @@ -1468,5 +1548,11 @@ will contain the given suffix as a part of their path. This can be used to assist use of the 'revoke-prefix' endpoint later on. The given suffix must match the regular -expression ` +expression.` + tokenExplicitMaxTTLHelp = `If set, tokens created via this role +carry an explicit maximum TTL. During renewal, +the current maximum TTL values of the role +and the mount are not checked for changes, +and any updates to these values will have +no effect on the token being renewed.` ) diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 8d3817ba3a..449875da19 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -955,17 +955,18 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } exp := map[string]interface{}{ - "id": root, - "accessor": resp.Data["accessor"].(string), - "policies": []string{"root"}, - "path": "auth/token/root", - "meta": map[string]string(nil), - "display_name": "root", - "orphan": true, - "num_uses": 0, - "creation_ttl": int64(0), - "ttl": int64(0), - "role": "", + "id": root, + "accessor": resp.Data["accessor"].(string), + "policies": []string{"root"}, + "path": "auth/token/root", + "meta": map[string]string(nil), + "display_name": "root", + "orphan": true, + "num_uses": 0, + "creation_ttl": int64(0), + "ttl": int64(0), + "role": "", + "explicit_max_ttl": int64(0), } if resp.Data["creation_time"].(int64) == 0 { @@ -990,17 +991,18 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } exp = map[string]interface{}{ - "id": "client", - "accessor": resp.Data["accessor"], - "policies": []string{"default", "foo"}, - "path": "auth/token/create", - "meta": map[string]string(nil), - "display_name": "token", - "orphan": false, - "num_uses": 0, - "creation_ttl": int64(3600), - "ttl": int64(3600), - "role": "", + "id": "client", + "accessor": resp.Data["accessor"], + "policies": []string{"default", "foo"}, + "path": "auth/token/create", + "meta": map[string]string(nil), + "display_name": "token", + "orphan": false, + "num_uses": 0, + "creation_ttl": int64(3600), + "ttl": int64(3600), + "role": "", + "explicit_max_ttl": int64(0), } if resp.Data["creation_time"].(int64) == 0 { @@ -1031,17 +1033,18 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } exp = map[string]interface{}{ - "id": "client", - "accessor": resp.Data["accessor"], - "policies": []string{"default", "foo"}, - "path": "auth/token/create", - "meta": map[string]string(nil), - "display_name": "token", - "orphan": false, - "num_uses": 0, - "creation_ttl": int64(3600), - "ttl": int64(3600), - "role": "", + "id": "client", + "accessor": resp.Data["accessor"], + "policies": []string{"default", "foo"}, + "path": "auth/token/create", + "meta": map[string]string(nil), + "display_name": "token", + "orphan": false, + "num_uses": 0, + "creation_ttl": int64(3600), + "ttl": int64(3600), + "role": "", + "explicit_max_ttl": int64(0), } if resp.Data["creation_time"].(int64) == 0 { @@ -1095,17 +1098,18 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { } exp := map[string]interface{}{ - "id": root, - "accessor": resp.Data["accessor"], - "policies": []string{"root"}, - "path": "auth/token/root", - "meta": map[string]string(nil), - "display_name": "root", - "orphan": true, - "num_uses": 0, - "creation_ttl": int64(0), - "ttl": int64(0), - "role": "", + "id": root, + "accessor": resp.Data["accessor"], + "policies": []string{"root"}, + "path": "auth/token/root", + "meta": map[string]string(nil), + "display_name": "root", + "orphan": true, + "num_uses": 0, + "creation_ttl": int64(0), + "ttl": int64(0), + "role": "", + "explicit_max_ttl": int64(0), } if resp.Data["creation_time"].(int64) == 0 { @@ -1252,9 +1256,10 @@ func TestTokenStore_RoleCRUD(t *testing.T) { expected := map[string]interface{}{ "name": "test", "orphan": true, - "period": float64(259200), + "period": int64(259200), "allowed_policies": []string{"test1", "test2"}, "path_suffix": "happenin", + "explicit_max_ttl": int64(0), } if !reflect.DeepEqual(expected, resp.Data) { @@ -1292,9 +1297,57 @@ func TestTokenStore_RoleCRUD(t *testing.T) { expected = map[string]interface{}{ "name": "test", "orphan": true, - "period": float64(284400), + "period": int64(284400), "allowed_policies": []string{"test3"}, "path_suffix": "happenin", + "explicit_max_ttl": int64(0), + } + + if !reflect.DeepEqual(expected, resp.Data) { + t.Fatalf("expected:\n%v\nactual:\n%v\n", expected, resp.Data) + } + + // Now test setting explicit max ttl at the same time as period, which + // should be an error + req.Operation = logical.CreateOperation + req.Data = map[string]interface{}{ + "explicit_max_ttl": "5", + } + + resp, err = core.HandleRequest(req) + if err == nil { + t.Fatalf("expected error") + } + + // Now set explicit max ttl and clear the period + req.Operation = logical.CreateOperation + req.Data = map[string]interface{}{ + "explicit_max_ttl": "5", + "period": "0s", + } + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + + req.Operation = logical.ReadOperation + req.Data = map[string]interface{}{} + + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp == nil { + t.Fatalf("got a nil response") + } + + expected = map[string]interface{}{ + "name": "test", + "orphan": true, + "explicit_max_ttl": int64(5), + "allowed_policies": []string{"test3"}, + "path_suffix": "happenin", + "period": int64(0), } if !reflect.DeepEqual(expected, resp.Data) { @@ -1610,3 +1663,182 @@ func TestTokenStore_RolePeriod(t *testing.T) { } } } + +func TestTokenStore_RoleExplicitMaxTTL(t *testing.T) { + core, _, _, root := TestCoreWithTokenStore(t) + + core.defaultLeaseTTL = 5 * time.Second + core.maxLeaseTTL = 5 * time.Hour + + // Note: these requests are sent to Core since Core handles registration + // with the expiration manager and we need the storage to be consistent + + req := logical.TestRequest(t, logical.UpdateOperation, "auth/token/roles/test") + req.ClientToken = root + req.Data = map[string]interface{}{ + "explicit_max_ttl": "6s", + } + + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp != nil { + t.Fatalf("expected a nil response") + } + + // This first set of logic is to verify that a normal non-root token will + // be given a TTL of 5 seconds, and that renewing will cause the TTL to + // increase + { + req.Path = "auth/token/create" + req.Data = map[string]interface{}{ + "policies": []string{"default"}, + } + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + + req.ClientToken = resp.Auth.ClientToken + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + ttl := resp.Data["ttl"].(int64) + if ttl > 5 { + t.Fatalf("TTL too large") + } + + // Let the TTL go down a bit to 3 seconds + time.Sleep(2 * time.Second) + + req.Operation = logical.UpdateOperation + req.Path = "auth/token/renew-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + ttl = resp.Data["ttl"].(int64) + if ttl < 4 { + t.Fatalf("TTL too small after renewal") + } + } + + // Now we create a token against the role. After renew our max should still + // be the same. + { + req.ClientToken = root + req.Operation = logical.UpdateOperation + req.Path = "auth/token/create/test" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp == nil { + t.Fatal("response was nil") + } + if resp.Auth == nil { + t.Fatal(fmt.Sprintf("response auth was nil, resp is %#v", *resp)) + } + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + + req.ClientToken = resp.Auth.ClientToken + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + ttl := resp.Data["ttl"].(int64) + if ttl > 6 { + t.Fatalf("TTL too big") + } + maxTTL := resp.Data["explicit_max_ttl"].(int64) + if maxTTL != 6 { + t.Fatalf("expected 6 for explicit max TTL, got %d", maxTTL) + } + + // Let the TTL go down a bit to 3 seconds (4 against explicit max) + time.Sleep(2 * time.Second) + + req.Operation = logical.UpdateOperation + req.Path = "auth/token/renew-self" + req.Data = map[string]interface{}{ + "increment": 300, + } + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + ttl = resp.Data["ttl"].(int64) + if ttl > 4 { + t.Fatalf("TTL too big") + } + + // Let the TTL go down a bit more to 2 seconds (2 against explicit max) + time.Sleep(2 * time.Second) + + req.Operation = logical.UpdateOperation + req.Path = "auth/token/renew-self" + req.Data = map[string]interface{}{ + "increment": 300, + } + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + ttl = resp.Data["ttl"].(int64) + if ttl > 2 { + t.Fatalf("TTL too big") + } + + // It should expire + time.Sleep(3 * time.Second) + + req.Operation = logical.UpdateOperation + req.Path = "auth/token/renew-self" + req.Data = map[string]interface{}{ + "increment": 300, + } + resp, err = core.HandleRequest(req) + if err == nil { + t.Fatalf("expected error") + } + + req.Operation = logical.ReadOperation + req.Path = "auth/token/lookup-self" + resp, err = core.HandleRequest(req) + if err == nil { + t.Fatalf("expected error") + } + } +} diff --git a/website/source/docs/auth/token.html.md b/website/source/docs/auth/token.html.md index a3c65e4c55..97e53aefdd 100644 --- a/website/source/docs/auth/token.html.md +++ b/website/source/docs/auth/token.html.md @@ -601,7 +601,7 @@ of the header should be "X-Vault-Token" and the value should be the token. each renewal. So long as they continue to be renewed, they will never expire. The parameter is an integer duration of seconds. Tokens issued track updates to the role value; the new period takes effect upon next - renew. + renew. This cannot be used in conjunction with `explicit_max_ttl`.
  • path_suffix @@ -614,6 +614,16 @@ of the header should be "X-Vault-Token" and the value should be the token. part of their path, and then tokens with the old suffix can be revoked via `sys/revoke-prefix`.
  • +
  • + explicit_max_ttl + optional + If set, tokens created with this role have an explicit max TTL set upon + them. This maximum token TTL *cannot* be changed later, and unlike with + normal tokens, updates to the role or the system/mount max TTL value + will have no effect at renewal time -- the token will never be able to + be renewed or used past the value set at issue time. This cannot be + used in conjunction with `period`. +