open-nomad/nomad/vault_test.go
Luiz Aoqui ab7eb5de6e
Support Vault entity aliases (#12449)
Move some common Vault API data struct decoding out of the Vault client
so it can be reused in other situations.

Make Vault job validation its own function so it's easier to expand it.

Rename the `Job.VaultPolicies` method to just `Job.Vault` since it
returns the full Vault block, not just their policies.

Set `ChangeMode` on `Vault.Canonicalize`.

Add some missing tests.

Allows specifying an entity alias that will be used by Nomad when
deriving the task Vault token.

An entity alias assigns an indentity to a token, allowing better control
and management of Vault clients since all tokens with the same indentity
alias will now be considered the same client. This helps track Nomad
activity in Vault's audit logs and better control over Vault billing.

Add support for a new Nomad server configuration to define a default
entity alias to be used when deriving Vault tokens. This default value
will be used if the task doesn't have an entity alias defined.
2022-04-05 14:18:10 -04:00

2109 lines
54 KiB
Go

package nomad
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/hashicorp/nomad/ci"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/time/rate"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/testutil"
vapi "github.com/hashicorp/vault/api"
vaultconsts "github.com/hashicorp/vault/sdk/helper/consts"
)
const (
// nomadRoleManagementPolicy is a policy that allows nomad to manage tokens
nomadRoleManagementPolicy = `
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "auth/token/lookup" {
capabilities = ["update"]
}
path "auth/token/roles/test" {
capabilities = ["read"]
}
path "auth/token/revoke-accessor" {
capabilities = ["update"]
}
`
// tokenLookupPolicy allows a token to be looked up
tokenLookupPolicy = `
path "auth/token/lookup" {
capabilities = ["update"]
}
`
// nomadRoleCreatePolicy gives the ability to create the role and derive tokens
// from the test role
nomadRoleCreatePolicy = `
path "auth/token/create/test" {
capabilities = ["create", "update"]
}
`
// secretPolicy gives access to the secret mount
secretPolicy = `
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
`
)
// defaultTestVaultAllowlistRoleAndToken creates a test Vault role and returns a token
// created in that role
func defaultTestVaultAllowlistRoleAndToken(v *testutil.TestVault, t *testing.T, rolePeriod int) string {
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
}
d := make(map[string]interface{}, 2)
d["allowed_policies"] = "nomad-role-create,nomad-role-management"
d["period"] = rolePeriod
d["allowed_entity_aliases"] = []string{"valid-entity-alias"}
return testVaultRoleAndToken(v, t, vaultPolicies, d,
[]string{"nomad-role-create", "nomad-role-management"})
}
// defaultTestVaultDenylistRoleAndToken creates a test Vault role using
// disallowed_policies and returns a token created in that role
func defaultTestVaultDenylistRoleAndToken(v *testutil.TestVault, t *testing.T, rolePeriod int) string {
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
"secrets": secretPolicy,
}
// Create the role
d := make(map[string]interface{}, 2)
d["disallowed_policies"] = "nomad-role-create"
d["period"] = rolePeriod
testVaultRoleAndToken(v, t, vaultPolicies, d, []string{"default"})
// Create a token that can use the role
a := v.Client.Auth().Token()
req := &vapi.TokenCreateRequest{
Policies: []string{"nomad-role-create", "nomad-role-management"},
}
s, err := a.Create(req)
if err != nil {
t.Fatalf("failed to create child token: %v", err)
}
if s == nil || s.Auth == nil {
t.Fatalf("bad secret response: %+v", s)
}
return s.Auth.ClientToken
}
// testVaultRoleAndToken writes the vaultPolicies to vault and then creates a
// test role with the passed data. After that it derives a token from the role
// with the tokenPolicies
func testVaultRoleAndToken(v *testutil.TestVault, t *testing.T, vaultPolicies map[string]string,
data map[string]interface{}, tokenPolicies []string) string {
// Write the policies
sys := v.Client.Sys()
for p, data := range vaultPolicies {
if err := sys.PutPolicy(p, data); err != nil {
t.Fatalf("failed to create %q policy: %v", p, err)
}
}
// Build a role
l := v.Client.Logical()
l.Write("auth/token/roles/test", data)
// Create a new token with the role
a := v.Client.Auth().Token()
req := vapi.TokenCreateRequest{
Policies: tokenPolicies,
}
s, err := a.CreateWithRole(&req, "test")
if err != nil {
t.Fatalf("failed to create child token: %v", err)
}
// Get the client token
if s == nil || s.Auth == nil {
t.Fatalf("bad secret response: %+v", s)
}
return s.Auth.ClientToken
}
func TestVaultClient_BadConfig(t *testing.T) {
ci.Parallel(t)
conf := &config.VaultConfig{}
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
_, err := NewVaultClient(nil, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "valid") {
t.Fatalf("expected config error: %v", err)
}
tr := true
conf.Enabled = &tr
_, err = NewVaultClient(conf, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "token must be set") {
t.Fatalf("Expected token unset error: %v", err)
}
conf.Token = "123"
_, err = NewVaultClient(conf, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "address must be set") {
t.Fatalf("Expected address unset error: %v", err)
}
}
// TestVaultClient_WithNamespaceSupport tests that the Vault namespace config, if present, will result in the
// namespace header being set on the created Vault client.
func TestVaultClient_WithNamespaceSupport(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
tr := true
testNs := "test-namespace"
conf := &config.VaultConfig{
Addr: "https://vault.service.consul:8200",
Enabled: &tr,
Token: "testvaulttoken",
Namespace: testNs,
}
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
c, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
require.Equal(testNs, c.client.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal("", c.clientSys.Headers().Get(vaultconsts.NamespaceHeaderName))
require.NotEqual(c.clientSys, c.client)
}
// TestVaultClient_WithoutNamespaceSupport tests that the Vault namespace config, if present, will result in the
// namespace header being set on the created Vault client.
func TestVaultClient_WithoutNamespaceSupport(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
tr := true
conf := &config.VaultConfig{
Addr: "https://vault.service.consul:8200",
Enabled: &tr,
Token: "testvaulttoken",
Namespace: "",
}
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
c, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
require.Equal("", c.client.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal("", c.clientSys.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal(c.clientSys, c.client)
}
// started separately.
// Test that the Vault Client can establish a connection even if it is started
// before Vault is available.
func TestVaultClient_EstablishConnection(t *testing.T) {
ci.Parallel(t)
for i := 10; i >= 0; i-- {
v := testutil.NewTestVaultDelayed(t)
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
// Sleep a little while and check that no connection has been established.
time.Sleep(100 * time.Duration(testutil.TestMultiplier()) * time.Millisecond)
if established, _ := client.ConnectionEstablished(); established {
t.Fatalf("ConnectionEstablished() returned true before Vault server started")
}
// Start Vault
if err := v.Start(); err != nil {
v.Stop()
client.Stop()
if i == 0 {
t.Fatalf("Failed to start vault: %v", err)
}
wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
time.Sleep(wait)
continue
}
var waitErr error
testutil.WaitForResult(func() (bool, error) {
return client.ConnectionEstablished()
}, func(err error) {
waitErr = err
})
v.Stop()
client.Stop()
if waitErr != nil {
if i == 0 {
t.Fatalf("Failed to start vault: %v", err)
}
wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
time.Sleep(wait)
continue
}
break
}
}
func TestVaultClient_ValidateRole(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
}
data := map[string]interface{}{
"allowed_policies": "default,root",
"orphan": true,
"renewable": true,
"token_explicit_max_ttl": 10,
}
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, nil)
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr == nil {
return false, fmt.Errorf("expect an error")
}
return true, nil
}, func(err error) {
require.NoError(t, err)
})
require.Contains(t, connErr.Error(), "explicit max ttl")
require.Contains(t, connErr.Error(), "non-zero period")
}
// TestVaultClient_ValidateRole_Success asserts that a valid token role
// gets marked as valid
func TestVaultClient_ValidateRole_Success(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
}
data := map[string]interface{}{
"allowed_policies": "default,root",
"orphan": true,
"renewable": true,
"token_period": 1000,
}
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, nil)
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr != nil {
return false, connErr
}
return true, nil
}, func(err error) {
require.NoError(t, err)
})
}
// TestVaultClient_ValidateRole_Deprecated_Success asserts that a valid token
// role gets marked as valid, even if it uses deprecated field, period
func TestVaultClient_ValidateRole_Deprecated_Success(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
}
data := map[string]interface{}{
"allowed_policies": "default,root",
"orphan": true,
"renewable": true,
"period": 1000,
}
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, nil)
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr != nil {
return false, connErr
}
return true, nil
}, func(err error) {
require.NoError(t, err)
})
}
func TestVaultClient_ValidateRole_NonExistent(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
v.Config.Token = v.RootToken
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
v.Config.Role = "test-nonexistent"
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr == nil {
return false, fmt.Errorf("expect an error")
}
return true, nil
}, func(err error) {
t.Fatalf("bad: %v", err)
})
errStr := connErr.Error()
if !strings.Contains(errStr, "does not exist") {
t.Fatalf("Expect does not exist error")
}
}
func TestVaultClient_ValidateRole_EntityAlias(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
testCases := []struct {
name string
allowedEntityAlises []string
serverEntityAlias string
expectError string
}{
{
name: "success",
allowedEntityAlises: []string{"valid-entity-alias"},
serverEntityAlias: "valid-entity-alias",
},
{
name: "no default alias",
allowedEntityAlises: []string{"valid-entity-alias"},
serverEntityAlias: "",
},
{
name: "no allowed alias and no default",
allowedEntityAlises: []string{},
serverEntityAlias: "",
},
{
name: "no allowed alias with default",
allowedEntityAlises: []string{},
serverEntityAlias: "valid-entity-alias",
expectError: "Role must allow entity alias valid-entity-alias to be used.",
},
{
name: "default entity alias not allowed",
allowedEntityAlises: []string{"valid-entity-alias"},
serverEntityAlias: "not-valid-entity-alias",
expectError: "Role must allow entity alias not-valid-entity-alias to be used.",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set the configs token in a new test role
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"nomad-role-management": nomadRoleManagementPolicy,
}
data := map[string]interface{}{
"allowed_policies": "default,root",
"allowed_entity_aliases": tc.allowedEntityAlises,
"orphan": true,
"renewable": true,
"token_period": 1000,
}
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, nil)
v.Config.EntityAlias = tc.serverEntityAlias
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr != nil {
return false, connErr
}
return true, nil
}, func(err error) {
if tc.expectError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectError)
} else {
require.NoError(t, err)
}
})
})
}
}
func TestVaultClient_ValidateToken(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
vaultPolicies := map[string]string{
"nomad-role-create": nomadRoleCreatePolicy,
"token-lookup": tokenLookupPolicy,
}
data := map[string]interface{}{
"allowed_policies": "token-lookup,nomad-role-create",
"period": 10,
}
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, []string{"token-lookup", "nomad-role-create"})
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
// Wait for an error
var conn bool
var connErr error
testutil.WaitForResult(func() (bool, error) {
conn, connErr = client.ConnectionEstablished()
if !conn {
return false, fmt.Errorf("Should connect")
}
if connErr == nil {
return false, fmt.Errorf("expect an error")
}
return true, nil
}, func(err error) {
t.Fatalf("bad: %v", err)
})
errStr := connErr.Error()
if !strings.Contains(errStr, vaultTokenRevokePath) {
t.Fatalf("Expect revoke error")
}
if !strings.Contains(errStr, fmt.Sprintf(vaultRoleLookupPath, "test")) {
t.Fatalf("Expect explicit max ttl error")
}
if !strings.Contains(errStr, "token must have one of the following") {
t.Fatalf("Expect explicit max ttl error")
}
}
func TestVaultClient_SetActive(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
waitForConnection(client, t)
// Do a lookup and expect an error about not being active
_, err = client.LookupToken(context.Background(), "123")
if err == nil || !strings.Contains(err.Error(), "not active") {
t.Fatalf("Expected not-active error: %v", err)
}
client.SetActive(true)
// Do a lookup of ourselves
_, err = client.LookupToken(context.Background(), v.RootToken)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
// Test that we can update the config and things keep working
func TestVaultClient_SetConfig(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
v2 := testutil.NewTestVault(t)
defer v2.Stop()
// Set the configs token in a new test role
v2.Config.Token = defaultTestVaultAllowlistRoleAndToken(v2, t, 20)
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
waitForConnection(client, t)
if client.tokenData == nil || len(client.tokenData.Policies) != 1 {
t.Fatalf("unexpected token: %v", client.tokenData)
}
// Update the config
if err := client.SetConfig(v2.Config); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
waitForConnection(client, t)
if client.tokenData == nil || len(client.tokenData.Policies) != 3 {
t.Fatalf("unexpected token: %v", client.tokenData)
}
// Test that when SetConfig is called with the same configuration, it is a
// no-op
failCh := make(chan struct{}, 1)
go func() {
tomb := client.tomb
select {
case <-tomb.Dying():
close(failCh)
case <-time.After(1 * time.Second):
return
}
}()
// Update the config
if err := client.SetConfig(v2.Config); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
select {
case <-failCh:
t.Fatalf("Tomb shouldn't have exited")
case <-time.After(1 * time.Second):
return
}
}
// TestVaultClient_SetConfig_Deadlock asserts that calling SetConfig
// concurrently with establishConnection does not deadlock.
func TestVaultClient_SetConfig_Deadlock(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
v2 := testutil.NewTestVault(t)
defer v2.Stop()
// Set the configs token in a new test role
v2.Config.Token = defaultTestVaultAllowlistRoleAndToken(v2, t, 20)
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
for i := 0; i < 100; i++ {
// Alternate configs to cause updates
conf := v.Config
if i%2 == 0 {
conf = v2.Config
}
if err := client.SetConfig(conf); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
}
}
// Test that we can disable vault
func TestVaultClient_SetConfig_Disable(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
waitForConnection(client, t)
if client.tokenData == nil || len(client.tokenData.Policies) != 1 {
t.Fatalf("unexpected token: %v", client.tokenData)
}
// Disable vault
f := false
config := config.VaultConfig{
Enabled: &f,
}
// Update the config
if err := client.SetConfig(&config); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
if client.Enabled() || client.Running() {
t.Fatalf("SetConfig should have stopped client")
}
}
func TestVaultClient_RenewalLoop(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
// Sleep 8 seconds and ensure we have a non-zero TTL
time.Sleep(8 * time.Second)
// Get the current TTL
a := v.Client.Auth().Token()
s2, err := a.Lookup(v.Config.Token)
if err != nil {
t.Fatalf("failed to lookup token: %v", err)
}
ttl := parseTTLFromLookup(s2, t)
if ttl == 0 {
t.Fatalf("token renewal failed; ttl %v", ttl)
}
if client.currentExpiration.Before(time.Now()) {
t.Fatalf("found current expiration to be in past %s", time.Until(client.currentExpiration))
}
}
func TestVaultClientRenewUpdatesExpiration(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
// Get the current TTL
a := v.Client.Auth().Token()
s2, err := a.Lookup(v.Config.Token)
if err != nil {
t.Fatalf("failed to lookup token: %v", err)
}
exp0 := time.Now().Add(time.Duration(parseTTLFromLookup(s2, t)) * time.Second)
time.Sleep(1 * time.Second)
_, err = client.renew()
require.NoError(t, err)
exp1 := client.currentExpiration
require.True(t, exp0.Before(exp1))
time.Sleep(1 * time.Second)
_, err = client.renew()
require.NoError(t, err)
exp2 := client.currentExpiration
require.True(t, exp1.Before(exp2))
}
func TestVaultClient_StopsAfterPermissionError(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 2)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
time.Sleep(500 * time.Millisecond)
assert.True(t, client.isRenewLoopActive())
// Get the current TTL
a := v.Client.Auth().Token()
assert.NoError(t, a.RevokeSelf(""))
testutil.WaitForResult(func() (bool, error) {
if !client.isRenewLoopActive() {
return true, nil
} else {
return false, errors.New("renew loop should terminate after token is revoked")
}
}, func(err error) {
t.Fatalf("err: %v", err)
})
}
func TestVaultClient_LoopsUntilCannotRenew(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
defer client.Stop()
// Sleep 8 seconds and ensure we have a non-zero TTL
time.Sleep(8 * time.Second)
// Get the current TTL
a := v.Client.Auth().Token()
s2, err := a.Lookup(v.Config.Token)
if err != nil {
t.Fatalf("failed to lookup token: %v", err)
}
ttl := parseTTLFromLookup(s2, t)
if ttl == 0 {
t.Fatalf("token renewal failed; ttl %v", ttl)
}
if client.currentExpiration.Before(time.Now()) {
t.Fatalf("found current expiration to be in past %s", time.Until(client.currentExpiration))
}
}
func parseTTLFromLookup(s *vapi.Secret, t *testing.T) int64 {
if s == nil {
t.Fatalf("nil secret")
} else if s.Data == nil {
t.Fatalf("nil data block in secret")
}
ttlRaw, ok := s.Data["ttl"]
if !ok {
t.Fatalf("no ttl")
}
ttlNumber, ok := ttlRaw.(json.Number)
if !ok {
t.Fatalf("failed to convert ttl %q to json Number", ttlRaw)
}
ttl, err := ttlNumber.Int64()
if err != nil {
t.Fatalf("Failed to get ttl from json.Number: %v", err)
}
return ttl
}
func TestVaultClient_LookupToken_Invalid(t *testing.T) {
ci.Parallel(t)
tr := true
conf := &config.VaultConfig{
Enabled: &tr,
Addr: "http://foobar:12345",
Token: uuid.Generate(),
}
// Enable vault but use a bad address so it never establishes a conn
logger := testlog.HCLogger(t)
client, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
_, err = client.LookupToken(context.Background(), "foo")
if err == nil || !strings.Contains(err.Error(), "established") {
t.Fatalf("Expected error because connection to Vault hasn't been made: %v", err)
}
}
func TestVaultClient_LookupToken_Root(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Lookup ourselves
s, err := client.LookupToken(context.Background(), v.Config.Token)
if err != nil {
t.Fatalf("self lookup failed: %v", err)
}
policies, err := s.TokenPolicies()
if err != nil {
t.Fatalf("failed to parse policies: %v", err)
}
expected := []string{"root"}
if !reflect.DeepEqual(policies, expected) {
t.Fatalf("Unexpected policies; got %v; want %v", policies, expected)
}
// Create a token with a different set of policies
expected = []string{"default"}
req := vapi.TokenCreateRequest{
Policies: expected,
}
s, err = v.Client.Auth().Token().Create(&req)
if err != nil {
t.Fatalf("failed to create child token: %v", err)
}
// Get the client token
if s == nil || s.Auth == nil {
t.Fatalf("bad secret response: %+v", s)
}
// Lookup new child
s, err = client.LookupToken(context.Background(), s.Auth.ClientToken)
if err != nil {
t.Fatalf("self lookup failed: %v", err)
}
policies, err = s.TokenPolicies()
if err != nil {
t.Fatalf("failed to parse policies: %v", err)
}
if !reflect.DeepEqual(policies, expected) {
t.Fatalf("Unexpected policies; got %v; want %v", policies, expected)
}
}
func TestVaultClient_LookupToken_Role(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Lookup ourselves
s, err := client.LookupToken(context.Background(), v.Config.Token)
if err != nil {
t.Fatalf("self lookup failed: %v", err)
}
policies, err := s.TokenPolicies()
if err != nil {
t.Fatalf("failed to parse policies: %v", err)
}
expected := []string{"default", "nomad-role-create", "nomad-role-management"}
if !reflect.DeepEqual(policies, expected) {
t.Fatalf("Unexpected policies; got %v; want %v", policies, expected)
}
// Create a token with a different set of policies
expected = []string{"default"}
req := vapi.TokenCreateRequest{
Policies: expected,
}
s, err = v.Client.Auth().Token().Create(&req)
if err != nil {
t.Fatalf("failed to create child token: %v", err)
}
// Get the client token
if s == nil || s.Auth == nil {
t.Fatalf("bad secret response: %+v", s)
}
// Lookup new child
s, err = client.LookupToken(context.Background(), s.Auth.ClientToken)
if err != nil {
t.Fatalf("self lookup failed: %v", err)
}
policies, err = s.TokenPolicies()
if err != nil {
t.Fatalf("failed to parse policies: %v", err)
}
if !reflect.DeepEqual(policies, expected) {
t.Fatalf("Unexpected policies; got %v; want %v", policies, expected)
}
}
func TestVaultClient_LookupToken_RateLimit(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
client.setLimit(rate.Limit(1.0))
testRateLimit(t, 20, client, func(ctx context.Context) error {
// Lookup ourselves
_, err := client.LookupToken(ctx, v.Config.Token)
return err
})
}
func TestVaultClient_LookupTokenRole(t *testing.T) {
// ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
// Create test role.
_, err := v.Client.Logical().Write("auth/token/roles/nomad", map[string]interface{}{
"name": "nomad",
})
require.NoError(t, err)
testCases := []struct {
name string
dontWait bool
config *config.VaultConfig
run func(*testing.T, *vaultClient)
}{
{
name: "read role",
run: func(t *testing.T, client *vaultClient) {
s, err := client.LookupTokenRole(context.Background(), "nomad")
require.NoError(t, err)
require.Equal(t, "nomad", s.Data["name"])
},
},
{
name: "not enabled",
dontWait: true,
config: &config.VaultConfig{
Enabled: helper.BoolToPtr(false),
},
run: func(t *testing.T, client *vaultClient) {
client.SetActive(false)
_, err := client.LookupTokenRole(context.Background(), "nomad")
require.Error(t, err)
require.Contains(t, err.Error(), "disabled")
},
},
{
name: "not active",
run: func(t *testing.T, client *vaultClient) {
client.SetActive(false)
_, err := client.LookupTokenRole(context.Background(), "nomad")
require.Error(t, err)
require.Contains(t, err.Error(), "not active")
},
},
{
name: "fail to establish connection",
dontWait: true,
config: &config.VaultConfig{
Addr: "http://foobar:12345",
Token: uuid.Generate(),
},
run: func(t *testing.T, client *vaultClient) {
_, err := client.LookupTokenRole(context.Background(), "nomad")
require.Error(t, err)
require.Contains(t, err.Error(), "Connection to Vault has not been established")
},
},
{
name: "read non-existing role",
run: func(t *testing.T, client *vaultClient) {
_, err := client.LookupTokenRole(context.Background(), "invalid")
require.Error(t, err)
require.Contains(t, err.Error(), "does not exist")
},
},
{
name: "rate limit",
run: func(t *testing.T, client *vaultClient) {
client.setLimit(rate.Limit(1.0))
testRateLimit(t, 20, client, func(ctx context.Context) error {
// Lookup role
_, err := client.LookupTokenRole(ctx, "nomad")
return err
})
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := v.Config
if tc.config != nil {
config = config.Merge(tc.config)
}
client, err := NewVaultClient(config, logger, nil, nil)
require.NoError(t, err)
client.SetActive(true)
defer client.Stop()
if !tc.dontWait {
waitForConnection(client, t)
}
if tc.run != nil {
tc.run(t, client)
}
})
}
}
func TestVaultClient_CreateToken_Root(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"default"}}
s, err := client.CreateToken(context.Background(), a, task.Name)
if err != nil {
t.Fatalf("CreateToken failed: %v", err)
}
// Ensure that created secret is a wrapped token
if s == nil || s.WrapInfo == nil {
t.Fatalf("Bad secret: %#v", s)
}
d, err := time.ParseDuration(vaultTokenCreateTTL)
if err != nil {
t.Fatalf("bad: %v", err)
}
if s.WrapInfo.WrappedAccessor == "" {
t.Fatalf("Bad accessor: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.Token == "" {
t.Fatalf("Bad token: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.TTL != int(d.Seconds()) {
t.Fatalf("Bad ttl: %v", s.WrapInfo.WrappedAccessor)
}
}
func TestVaultClient_CreateToken_Allowlist_Role(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"default"}}
s, err := client.CreateToken(context.Background(), a, task.Name)
if err != nil {
t.Fatalf("CreateToken failed: %v", err)
}
// Ensure that created secret is a wrapped token
if s == nil || s.WrapInfo == nil {
t.Fatalf("Bad secret: %#v", s)
}
d, err := time.ParseDuration(vaultTokenCreateTTL)
if err != nil {
t.Fatalf("bad: %v", err)
}
if s.WrapInfo.WrappedAccessor == "" {
t.Fatalf("Bad accessor: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.Token == "" {
t.Fatalf("Bad token: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.TTL != int(d.Seconds()) {
t.Fatalf("Bad ttl: %v", s.WrapInfo.WrappedAccessor)
}
}
func TestVaultClient_CreateToken_Root_Target_Role(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Create the test role
defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Target the test role
v.Config.Role = "test"
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"default"}}
s, err := client.CreateToken(context.Background(), a, task.Name)
if err != nil {
t.Fatalf("CreateToken failed: %v", err)
}
// Ensure that created secret is a wrapped token
if s == nil || s.WrapInfo == nil {
t.Fatalf("Bad secret: %#v", s)
}
d, err := time.ParseDuration(vaultTokenCreateTTL)
if err != nil {
t.Fatalf("bad: %v", err)
}
if s.WrapInfo.WrappedAccessor == "" {
t.Fatalf("Bad accessor: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.Token == "" {
t.Fatalf("Bad token: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.TTL != int(d.Seconds()) {
t.Fatalf("Bad ttl: %v", s.WrapInfo.WrappedAccessor)
}
}
func TestVaultClient_CreateToken_Denylist_Role(t *testing.T) {
ci.Parallel(t)
// Need to skip if test is 0.6.4
version, err := testutil.VaultVersion()
if err != nil {
t.Fatalf("failed to determine version: %v", err)
}
if strings.Contains(version, "v0.6.4") {
t.Skipf("Vault has a regression in v0.6.4 that this test hits")
}
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultDenylistRoleAndToken(v, t, 5)
v.Config.Role = "test"
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"secrets"}}
s, err := client.CreateToken(context.Background(), a, task.Name)
if err != nil {
t.Fatalf("CreateToken failed: %v", err)
}
// Ensure that created secret is a wrapped token
if s == nil || s.WrapInfo == nil {
t.Fatalf("Bad secret: %#v", s)
}
d, err := time.ParseDuration(vaultTokenCreateTTL)
if err != nil {
t.Fatalf("bad: %v", err)
}
if s.WrapInfo.WrappedAccessor == "" {
t.Fatalf("Bad accessor: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.Token == "" {
t.Fatalf("Bad token: %v", s.WrapInfo.WrappedAccessor)
} else if s.WrapInfo.TTL != int(d.Seconds()) {
t.Fatalf("Bad ttl: %v", s.WrapInfo.WrappedAccessor)
}
}
func TestVaultClient_CreateToken_Role_InvalidToken(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
defaultTestVaultAllowlistRoleAndToken(v, t, 5)
v.Config.Token = "foo-bar"
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
testutil.WaitForResult(func() (bool, error) {
established, err := client.ConnectionEstablished()
if !established {
return false, fmt.Errorf("Should establish")
}
return err != nil, nil
}, func(err error) {
t.Fatalf("Connection not established")
})
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"default"}}
_, err = client.CreateToken(context.Background(), a, task.Name)
if err == nil || !strings.Contains(err.Error(), "failed to establish connection to Vault") {
t.Fatalf("CreateToken should have failed: %v", err)
}
}
func TestVaultClient_CreateToken_Role_Unrecoverable(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Start the client
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"unknown_policy"}}
_, err = client.CreateToken(context.Background(), a, task.Name)
if err == nil {
t.Fatalf("CreateToken should have failed: %v", err)
}
_, ok := err.(structs.Recoverable)
if ok {
t.Fatalf("CreateToken should not be a recoverable error type: %v (%T)", err, err)
}
}
func TestVaultClient_CreateToken_Prestart(t *testing.T) {
ci.Parallel(t)
vconfig := &config.VaultConfig{
Enabled: helper.BoolToPtr(true),
Token: uuid.Generate(),
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
// Create an allocation that requires a Vault policy
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{Policies: []string{"default"}}
_, err = client.CreateToken(context.Background(), a, task.Name)
if err == nil {
t.Fatalf("CreateToken should have failed: %v", err)
}
if rerr, ok := err.(*structs.RecoverableError); !ok {
t.Fatalf("Err should have been type recoverable error")
} else if ok && !rerr.IsRecoverable() {
t.Fatalf("Err should have been recoverable")
}
}
func TestVaultClient_MarkForRevocation(t *testing.T) {
vconfig := &config.VaultConfig{
Enabled: helper.BoolToPtr(true),
Token: uuid.Generate(),
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(vconfig, logger, nil, nil)
require.NoError(t, err)
client.SetActive(true)
defer client.Stop()
// Create some VaultAccessors
vas := []*structs.VaultAccessor{
mock.VaultAccessor(),
mock.VaultAccessor(),
}
err = client.MarkForRevocation(vas)
require.NoError(t, err)
// Wasn't committed
require.Len(t, client.revoking, 2)
require.Equal(t, 2, client.stats().TrackedForRevoke)
}
func TestVaultClient_RevokeTokens_PreEstablishs(t *testing.T) {
ci.Parallel(t)
vconfig := &config.VaultConfig{
Enabled: helper.BoolToPtr(true),
Token: uuid.Generate(),
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
// Create some VaultAccessors
vas := []*structs.VaultAccessor{
mock.VaultAccessor(),
mock.VaultAccessor(),
}
if err := client.RevokeTokens(context.Background(), vas, false); err != nil {
t.Fatalf("RevokeTokens failed: %v", err)
}
// Wasn't committed
if len(client.revoking) != 0 {
t.Fatalf("didn't add to revoke loop")
}
if err := client.RevokeTokens(context.Background(), vas, true); err != nil {
t.Fatalf("RevokeTokens failed: %v", err)
}
// Was committed
if len(client.revoking) != 2 {
t.Fatalf("didn't add to revoke loop")
}
if client.stats().TrackedForRevoke != 2 {
t.Fatalf("didn't add to revoke loop")
}
}
func TestVaultClient_CreateToken_EntityAlias(t *testing.T) {
ci.Parallel(t)
logger := testlog.HCLogger(t)
v := testutil.NewTestVault(t)
defer v.Stop()
testCases := []struct {
name string
entityAlias string
serverEntityAlias string
noRole bool
expectError string
requireEntityID bool
}{
{
name: "success",
entityAlias: "valid-entity-alias",
requireEntityID: true,
},
{
name: "invalid entity alias",
entityAlias: "not-valid-entity-alias",
expectError: "invalid 'entity_alias'",
},
{
name: "token without role",
noRole: true,
requireEntityID: false,
},
{
name: "use server entity alias",
entityAlias: "",
serverEntityAlias: "valid-entity-alias",
requireEntityID: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if !tc.noRole {
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
}
if tc.serverEntityAlias != "" {
v.Config.EntityAlias = tc.serverEntityAlias
}
client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create test alloc and set vault block.
alloc := mock.Alloc()
task := alloc.Job.TaskGroups[0].Tasks[0]
task.Vault = &structs.Vault{
Policies: []string{"default"},
EntityAlias: tc.entityAlias,
}
s, err := client.CreateToken(context.Background(), alloc, task.Name)
if tc.expectError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectError)
} else {
require.NoError(t, err)
// Unwrap token from its cubbyhole.
unwrapToken, err := client.client.Logical().Unwrap(s.WrapInfo.Token)
require.NoError(t, err)
if tc.requireEntityID {
require.NotEmpty(t, unwrapToken.Auth.EntityID)
}
}
})
}
}
// TestVaultClient_RevokeTokens_Failures_TTL asserts that
// the registered TTL doesn't get extended on retries
func TestVaultClient_RevokeTokens_Failures_TTL(t *testing.T) {
ci.Parallel(t)
vconfig := &config.VaultConfig{
Enabled: helper.BoolToPtr(true),
Token: uuid.Generate(),
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
// Create some VaultAccessors
vas := []*structs.VaultAccessor{
mock.VaultAccessor(),
mock.VaultAccessor(),
}
err = client.RevokeTokens(context.Background(), vas, true)
require.NoError(t, err)
// Was committed
require.Len(t, client.revoking, 2)
// set TTL
ttl := time.Now().Add(50 * time.Second)
client.revoking[vas[0]] = ttl
client.revoking[vas[1]] = ttl
// revoke again and ensure that TTL isn't extended
err = client.RevokeTokens(context.Background(), vas, true)
require.NoError(t, err)
require.Len(t, client.revoking, 2)
expected := map[*structs.VaultAccessor]time.Time{
vas[0]: ttl,
vas[1]: ttl,
}
require.Equal(t, expected, client.revoking)
}
func TestVaultClient_RevokeTokens_Root(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
purged := 0
purge := func(accessors []*structs.VaultAccessor) error {
purged += len(accessors)
return nil
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create some vault tokens
auth := v.Client.Auth().Token()
req := vapi.TokenCreateRequest{
Policies: []string{"default"},
}
t1, err := auth.Create(&req)
if err != nil {
t.Fatalf("Failed to create vault token: %v", err)
}
if t1 == nil || t1.Auth == nil {
t.Fatalf("bad secret response: %+v", t1)
}
t2, err := auth.Create(&req)
if err != nil {
t.Fatalf("Failed to create vault token: %v", err)
}
if t2 == nil || t2.Auth == nil {
t.Fatalf("bad secret response: %+v", t2)
}
// Create two VaultAccessors
vas := []*structs.VaultAccessor{
{Accessor: t1.Auth.Accessor},
{Accessor: t2.Auth.Accessor},
}
// Issue a token revocation
if err := client.RevokeTokens(context.Background(), vas, true); err != nil {
t.Fatalf("RevokeTokens failed: %v", err)
}
// Lookup the token and make sure we get an error
if s, err := auth.Lookup(t1.Auth.ClientToken); err == nil {
t.Fatalf("Revoked token lookup didn't fail: %+v", s)
}
if s, err := auth.Lookup(t2.Auth.ClientToken); err == nil {
t.Fatalf("Revoked token lookup didn't fail: %+v", s)
}
if purged != 2 {
t.Fatalf("Expected purged 2; got %d", purged)
}
}
func TestVaultClient_RevokeTokens_Role(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
purged := 0
purge := func(accessors []*structs.VaultAccessor) error {
purged += len(accessors)
return nil
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create some vault tokens
auth := v.Client.Auth().Token()
req := vapi.TokenCreateRequest{
Policies: []string{"default"},
}
t1, err := auth.Create(&req)
if err != nil {
t.Fatalf("Failed to create vault token: %v", err)
}
if t1 == nil || t1.Auth == nil {
t.Fatalf("bad secret response: %+v", t1)
}
t2, err := auth.Create(&req)
if err != nil {
t.Fatalf("Failed to create vault token: %v", err)
}
if t2 == nil || t2.Auth == nil {
t.Fatalf("bad secret response: %+v", t2)
}
// Create two VaultAccessors
vas := []*structs.VaultAccessor{
{Accessor: t1.Auth.Accessor},
{Accessor: t2.Auth.Accessor},
}
// Issue a token revocation
if err := client.RevokeTokens(context.Background(), vas, true); err != nil {
t.Fatalf("RevokeTokens failed: %v", err)
}
// Lookup the token and make sure we get an error
if purged != 2 {
t.Fatalf("Expected purged 2; got %d", purged)
}
if s, err := auth.Lookup(t1.Auth.ClientToken); err == nil {
t.Fatalf("Revoked token lookup didn't fail: %+v", s)
}
if s, err := auth.Lookup(t2.Auth.ClientToken); err == nil {
t.Fatalf("Revoked token lookup didn't fail: %+v", s)
}
}
// TestVaultClient_RevokeTokens_Idempotent asserts that token revocation
// is idempotent, and can cope with cases if token was deleted out of band.
func TestVaultClient_RevokeTokens_Idempotent(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
purged := map[string]struct{}{}
purge := func(accessors []*structs.VaultAccessor) error {
for _, accessor := range accessors {
purged[accessor.Accessor] = struct{}{}
}
return nil
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create some vault tokens
auth := v.Client.Auth().Token()
req := vapi.TokenCreateRequest{
Policies: []string{"default"},
}
t1, err := auth.Create(&req)
require.NoError(t, err)
require.NotNil(t, t1)
require.NotNil(t, t1.Auth)
t2, err := auth.Create(&req)
require.NoError(t, err)
require.NotNil(t, t2)
require.NotNil(t, t2.Auth)
t3, err := auth.Create(&req)
require.NoError(t, err)
require.NotNil(t, t3)
require.NotNil(t, t3.Auth)
// revoke t3 out of band
err = auth.RevokeAccessor(t3.Auth.Accessor)
require.NoError(t, err)
// Create two VaultAccessors
vas := []*structs.VaultAccessor{
{Accessor: t1.Auth.Accessor},
{Accessor: t2.Auth.Accessor},
{Accessor: t3.Auth.Accessor},
}
// Issue a token revocation
err = client.RevokeTokens(context.Background(), vas, true)
require.NoError(t, err)
require.Empty(t, client.revoking)
// revoke token again
err = client.RevokeTokens(context.Background(), vas, true)
require.NoError(t, err)
require.Empty(t, client.revoking)
// Lookup the token and make sure we get an error
require.Len(t, purged, 3)
require.Contains(t, purged, t1.Auth.Accessor)
require.Contains(t, purged, t2.Auth.Accessor)
require.Contains(t, purged, t3.Auth.Accessor)
s, err := auth.Lookup(t1.Auth.ClientToken)
require.Errorf(t, err, "failed to purge token: %v", s)
s, err = auth.Lookup(t2.Auth.ClientToken)
require.Errorf(t, err, "failed to purge token: %v", s)
}
// TestVaultClient_RevokeDaemon_Bounded asserts that token revocation
// batches are bounded in size.
func TestVaultClient_RevokeDaemon_Bounded(t *testing.T) {
ci.Parallel(t)
v := testutil.NewTestVault(t)
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultAllowlistRoleAndToken(v, t, 5)
// Disable client until we can change settings for testing
conf := v.Config.Copy()
conf.Enabled = helper.BoolToPtr(false)
const (
batchSize = 100
batches = 3
)
resultCh := make(chan error, batches)
var totalPurges int64
// Purge function asserts batches are always < batchSize
purge := func(vas []*structs.VaultAccessor) error {
if len(vas) > batchSize {
resultCh <- fmt.Errorf("too many Vault accessors in batch: %d > %d", len(vas), batchSize)
} else {
resultCh <- nil
}
atomic.AddInt64(&totalPurges, int64(len(vas)))
return nil
}
logger := testlog.HCLogger(t)
client, err := NewVaultClient(conf, logger, purge, nil)
require.NoError(t, err)
// Override settings for testing and then enable client
client.maxRevokeBatchSize = batchSize
client.revocationIntv = 3 * time.Millisecond
conf = v.Config.Copy()
conf.Enabled = helper.BoolToPtr(true)
require.NoError(t, client.SetConfig(conf))
client.SetActive(true)
defer client.Stop()
waitForConnection(client, t)
// Create more tokens in Nomad than can fit in a batch; they don't need
// to exist in Vault.
accessors := make([]*structs.VaultAccessor, batchSize*batches)
for i := 0; i < len(accessors); i++ {
accessors[i] = &structs.VaultAccessor{Accessor: "abcd"}
}
// Mark for revocation
require.NoError(t, client.MarkForRevocation(accessors))
// Wait for tokens to be revoked
for i := 0; i < batches; i++ {
select {
case err := <-resultCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
// 10 seconds should be plenty long to process 3
// batches at a 3ms tick interval!
t.Errorf("timed out processing %d batches. %d/%d complete in 10s",
batches, i, batches)
}
}
require.Equal(t, int64(len(accessors)), atomic.LoadInt64(&totalPurges))
}
func waitForConnection(v *vaultClient, t *testing.T) {
testutil.WaitForResult(func() (bool, error) {
return v.ConnectionEstablished()
}, func(err error) {
t.Fatalf("Connection not established")
})
}
func TestVaultClient_nextBackoff(t *testing.T) {
ci.Parallel(t)
simpleCases := []struct {
name string
initBackoff float64
// define range of acceptable backoff values accounting for random factor
rangeMin float64
rangeMax float64
}{
{"simple case", 7.0, 8.7, 17.60},
{"too low", 2.0, 5.0, 10.0},
{"too large", 100, 30.0, 60.0},
}
for _, c := range simpleCases {
t.Run(c.name, func(t *testing.T) {
b := nextBackoff(c.initBackoff, time.Now().Add(10*time.Hour))
if !(c.rangeMin <= b && b <= c.rangeMax) {
t.Fatalf("Expected backoff within [%v, %v] but found %v", c.rangeMin, c.rangeMax, b)
}
})
}
// some edge cases
t.Run("close to expiry", func(t *testing.T) {
b := nextBackoff(20, time.Now().Add(1100*time.Millisecond))
if b != 5.0 {
t.Fatalf("Expected backoff is 5 but found %v", b)
}
})
t.Run("past expiry", func(t *testing.T) {
b := nextBackoff(20, time.Now().Add(-1100*time.Millisecond))
if !(60 <= b && b <= 120) {
t.Fatalf("Expected backoff within [%v, %v] but found %v", 60, 120, b)
}
})
}
func testRateLimit(t *testing.T, count int, client *vaultClient, fn func(context.Context) error) {
// Spin up many requests. These should block
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cancels := 0
unblock := make(chan struct{})
for i := 0; i < count; i++ {
go func() {
err := fn(ctx)
if err != nil {
if err == context.Canceled {
cancels += 1
return
}
t.Errorf("request failed: %v", err)
return
}
// Cancel the context
close(unblock)
}()
}
select {
case <-time.After(5 * time.Second):
t.Fatalf("timeout")
case <-unblock:
cancel()
}
desired := count - 1
testutil.WaitForResult(func() (bool, error) {
if desired-cancels > 2 {
return false, fmt.Errorf("Incorrect number of cancels; got %d; want %d", cancels, desired)
}
return true, nil
}, func(err error) {
t.Fatal(err)
})
}