Skip to content

Commit

Permalink
Track renewal expiration properly
Browse files Browse the repository at this point in the history
  • Loading branch information
Mahmood Ali committed Nov 9, 2018
1 parent 70ed23f commit e105d17
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 71 deletions.
99 changes: 59 additions & 40 deletions nomad/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ type VaultStats struct {

// TokenTTL is the time-to-live duration for the current token
TokenTTL time.Duration

//T TokenExpiry Time is the recoreded expiry time of the current token
TokenExpiry time.Time
}

// PurgeVaultAccessor is called to remove VaultAccessors from the system. If
Expand All @@ -154,13 +157,16 @@ type PurgeVaultAccessorFn func(accessors []*structs.VaultAccessor) error
// tokenData holds the relevant information about the Vault token passed to the
// client.
type tokenData struct {
CreationTTL int `mapstructure:"creation_ttl"`
CreationTime int `mapstructure:"creation_time"`
TTL int `mapstructure:"ttl"`
Renewable bool `mapstructure:"renewable"`
Policies []string `mapstructure:"policies"`
Role string `mapstructure:"role"`
Root bool
CreationTTL int `mapstructure:"creation_ttl"`
TTL int `mapstructure:"ttl"`
Renewable bool `mapstructure:"renewable"`
Policies []string `mapstructure:"policies"`
Role string `mapstructure:"role"`
ExpireTimeString string `mapstructure:"expire_time"`

// computed fields
Root bool
ExpireTime time.Time
}

// vaultClient is the Servers implementation of the VaultClient interface. The
Expand Down Expand Up @@ -474,7 +480,7 @@ func (v *vaultClient) renewalLoop() {
case <-authRenewTimer.C:
// Renew the token and determine the new expiration
err := v.renew()
currentExpiration := v.lastRenewed.Add(time.Duration(v.tokenData.CreationTTL) * time.Second)
currentExpiration := v.tokenData.ExpireTime

// Successfully renewed
if err == nil {
Expand Down Expand Up @@ -595,6 +601,37 @@ func (v *vaultClient) getWrappingFn() func(operation, path string) string {
}
}

const vaultDateLayout = time.RFC3339Nano

func parseTokenData(s map[string]interface{}) (tokenData, error) {
// Read and parse the fields
var data tokenData
if err := mapstructure.WeakDecode(s, &data); err != nil {
return data, fmt.Errorf("failed to parse Vault token's data block: %v", err)
}

root := false
for _, p := range data.Policies {
if p == "root" {
root = true
break
}
}

// Store the token data
data.Root = root

if data.ExpireTimeString != "" {
if d, err := time.Parse(vaultDateLayout, data.ExpireTimeString); err == nil {
data.ExpireTime = d
}
} else if data.TTL != 0 {
data.ExpireTime = time.Now().Add(time.Duration(data.TTL) * time.Second)
}

return data, nil
}

// parseSelfToken looks up the Vault token in Vault and parses its data storing
// it in the client. If the token is not valid for Nomads purposes an error is
// returned.
Expand All @@ -614,22 +651,10 @@ func (v *vaultClient) parseSelfToken() error {
}
self = secret

// Read and parse the fields
var data tokenData
if err := mapstructure.WeakDecode(self.Data, &data); err != nil {
return fmt.Errorf("failed to parse Vault token's data block: %v", err)
}

root := false
for _, p := range data.Policies {
if p == "root" {
root = true
break
}
data, err := parseTokenData(self.Data)
if err != nil {
return err
}

// Store the token data
data.Root = root
v.tokenData = &data

// The criteria that must be met for the token to be valid are as follows:
Expand All @@ -649,17 +674,12 @@ func (v *vaultClient) parseSelfToken() error {

var mErr multierror.Error
role := v.getRole()
if !root {
if !data.Root {
// All non-root tokens must be renewable
if !data.Renewable {
multierror.Append(&mErr, fmt.Errorf("Vault token is not renewable or root"))
}

// All non-root tokens must have creation time
if data.CreationTime == 0 {
multierror.Append(&mErr, fmt.Errorf("invalid lease creation time of zero"))
}

// All non-root tokens must have a lease duration
if data.CreationTTL == 0 {
multierror.Append(&mErr, fmt.Errorf("invalid lease duration of zero"))
Expand All @@ -686,7 +706,7 @@ func (v *vaultClient) parseSelfToken() error {
}

// Check we have the correct capabilities
if err := v.validateCapabilities(role, root); err != nil {
if err := v.validateCapabilities(role, data.Root); err != nil {
multierror.Append(&mErr, err)
}

Expand Down Expand Up @@ -1215,9 +1235,16 @@ func (v *vaultClient) setLimit(l rate.Limit) {
func (v *vaultClient) Stats() map[string]string {
stat := v.stats()

expireTimeStr := ""

if (stat.TokenExpiry != time.Time{}) {
expireTimeStr = stat.TokenExpiry.Format(time.RFC3339)
}

return map[string]string{
"tracked_for_revoked": strconv.Itoa(stat.TrackedForRevoke),
"token_ttl": stat.TokenTTL.String(),
"token_expire_time": expireTimeStr,
}
}

Expand All @@ -1229,7 +1256,8 @@ func (v *vaultClient) stats() *VaultStats {
stats.TrackedForRevoke = len(v.revoking)
v.revLock.Unlock()

stats.TokenTTL = tokenTTL(v.tokenData)
stats.TokenExpiry = v.tokenData.ExpireTime
stats.TokenTTL = time.Until(v.tokenData.ExpireTime)

return stats
}
Expand All @@ -1247,12 +1275,3 @@ func (v *vaultClient) EmitStats(period time.Duration, stopCh chan struct{}) {
}
}
}

func tokenTTL(tokenData *tokenData) time.Duration {
if tokenData == nil {
return time.Duration(0)
}

ttl := int64(tokenData.CreationTime+tokenData.CreationTTL) - time.Now().Unix()
return time.Duration(ttl) * time.Second
}
150 changes: 119 additions & 31 deletions nomad/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"testing"
"time"

"github.com/stretchr/testify/require"

"golang.org/x/time/rate"

"github.com/hashicorp/nomad/helper"
Expand Down Expand Up @@ -1114,7 +1116,7 @@ func TestVaultClient_RevokeTokens_PreEstablishs(t *testing.T) {
t.Fatalf("didn't add to revoke loop")
}

if client.Stats().TrackedForRevoke != 2 {
if client.stats().TrackedForRevoke != 2 {
t.Fatalf("didn't add to revoke loop")
}
}
Expand Down Expand Up @@ -1298,49 +1300,135 @@ func TestVaultClient_nextBackoff(t *testing.T) {
})
}

func TestVaultClient_tokenTTL(t *testing.T) {
t.Parallel()
func TestParseVaultToken(t *testing.T) {
parseDate := func(str string) time.Time {
v, err := time.Parse(vaultDateLayout, str)
if err != nil {
t.Fatalf("unexpected time format for %v: %v", str, err)
}
return v
}

cases := []struct {
name string
creationTime int64
creationTTL int
ttl time.Duration
name string
expected tokenData
input map[string]interface{}
}{
{
"in future",
time.Now().Unix() - 1000,
2500,
1500 * time.Second,
"basic value",
tokenData{
CreationTTL: 2764800,
TTL: 2764790,
Renewable: true,
Policies: []string{"default", "testgroup2-policy"},
Role: "sample-role",
ExpireTimeString: "2018-05-19T11:35:54.466476215-04:00",

Root: false,
ExpireTime: parseDate("2018-05-19T11:35:54.466476215-04:00"),
},
map[string]interface{}{
"accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
"creation_time": 1523979354,
"creation_ttl": 2764800,
"display_name": "ldap2-tesla",
"entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
"expire_time": "2018-05-19T11:35:54.466476215-04:00",
"explicit_max_ttl": 0,
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
"identity_policies": []string{"dev-group-policy"},
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
"meta": map[string]string{
"username": "tesla",
},
"role": "sample-role",
"num_uses": 0,
"orphan": true,
"path": "auth/ldap2/login/tesla",
"policies": []string{
"default",
"testgroup2-policy",
},
"renewable": true,
"ttl": 2764790,
},
},
{
"basic value but without expiry",
tokenData{
CreationTTL: 2764800,
TTL: 2764790,
Renewable: true,
Policies: []string{"default", "testgroup2-policy"},
Role: "sample-role",

Root: false,
ExpireTime: time.Now().Add(2764790 * time.Second),
},
map[string]interface{}{
"accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
"creation_time": 1523979354,
"creation_ttl": 2764800,
"display_name": "ldap2-tesla",
"entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
// "expire_time": "2018-05-19T11:35:54.466476215-04:00",
"explicit_max_ttl": 0,
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
"identity_policies": []string{"dev-group-policy"},
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
"meta": map[string]string{
"username": "tesla",
},
"role": "sample-role",
"num_uses": 0,
"orphan": true,
"path": "auth/ldap2/login/tesla",
"policies": []string{
"default",
"testgroup2-policy",
},
"renewable": true,
"ttl": 2764790,
},
},
{
"in past",
time.Now().Unix() - 2000,
1000,
-1000 * time.Second,
"root token",
tokenData{
Policies: []string{"root"},
Root: true,
},
map[string]interface{}{
"accessor": "2y5UTs1azS4nWDeBCSBXmQsn",
"creation_time": 1541775054,
"creation_ttl": 0,
"display_name": "root",
"entity_id": "",
"expire_time": nil,
"explicit_max_ttl": 0,
"id": "8tR2Io18hX6vQJh3eUjPBy1I",
"meta": nil,
"num_uses": 0,
"orphan": true,
"path": "auth/token/root",
"policies": []string{"root"},
"ttl": 0,
},
},
}

tolerance := 2 * time.Second

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
tokenData := &tokenData{
CreationTTL: c.creationTTL,
CreationTime: int(c.creationTime),
}
parsed, err := parseTokenData(c.input)
require.NoError(t, err)

found := tokenTTL(tokenData)
if !(c.ttl-tolerance <= found && found <= c.ttl+tolerance) {
t.Fatalf("wrong token ttl, expected=%s found=%s", c.ttl, found)
// check expiry
if c.expected.ExpireTime != parsed.ExpireTime {
require.WithinDuration(t, c.expected.ExpireTime, parsed.ExpireTime, 2*time.Second)
}

c.expected.ExpireTime = time.Time{}
parsed.ExpireTime = time.Time{}
require.Equal(t, c.expected, parsed)
})
}

t.Run("nil case", func(t *testing.T) {
found := tokenTTL(nil)
if found != 0 {
t.Fatalf("expected 0 ttl but found=%s", found)
}
})
}

0 comments on commit e105d17

Please sign in to comment.