Merge pull request #2226 from hashicorp/b-vault

Improve Vault integration and validation
This commit is contained in:
Alex Dadgar 2017-01-23 14:59:41 -08:00 committed by GitHub
commit 606bb30863
15 changed files with 646 additions and 105 deletions

View file

@ -121,6 +121,7 @@ vault {
key_file = "/path/to/key/file"
tls_server_name = "foobar"
tls_skip_verify = true
create_from_role = "test_role"
}
tls {
http = true

View file

@ -710,6 +710,7 @@ func parseVaultConfig(result **config.VaultConfig, list *ast.ObjectList) error {
"ca_file",
"ca_path",
"cert_file",
"create_from_role",
"key_file",
"tls_server_name",
"tls_skip_verify",

View file

@ -129,6 +129,7 @@ func TestConfig_Parse(t *testing.T) {
Addr: "127.0.0.1:9500",
AllowUnauthenticated: &trueValue,
Enabled: &falseValue,
Role: "test_role",
TLSCaFile: "/path/to/ca/file",
TLSCaPath: "/path/to/ca",
TLSCertFile: "/path/to/cert/file",

View file

@ -30,6 +30,13 @@ type VaultConfig struct {
// lifetime.
Token string `mapstructure:"token"`
// Role sets the role in which to create tokens from. The Token given to
// Nomad does not have to be created from this role but must have "update"
// capability on "auth/token/create/<create_from_role>". If this value is
// unset and the token is created from a role, the value is defaulted to the
// role the token is from.
Role string `mapstructure:"create_from_role"`
// AllowUnauthenticated allows users to submit jobs requiring Vault tokens
// without providing a Vault token proving they have access to these
// policies.
@ -99,6 +106,9 @@ func (a *VaultConfig) Merge(b *VaultConfig) *VaultConfig {
if b.Token != "" {
result.Token = b.Token
}
if b.Role != "" {
result.Role = b.Role
}
if b.TaskTokenTTL != "" {
result.TaskTokenTTL = b.TaskTokenTTL
}

View file

@ -47,11 +47,68 @@ const (
// vaultRevocationIntv is the interval at which Vault tokens that failed
// initial revocation are retried
vaultRevocationIntv = 5 * time.Minute
// vaultCapabilitiesLookupPath is the path to lookup the capabilities of
// ones token.
vaultCapabilitiesLookupPath = "/sys/capabilities-self"
// vaultTokenRenewPath is the path used to renew our token
vaultTokenRenewPath = "auth/token/renew-self"
// vaultTokenLookupPath is the path used to lookup a token
vaultTokenLookupPath = "auth/token/lookup"
// vaultTokenLookupSelfPath is the path used to lookup self token
vaultTokenLookupSelfPath = "auth/token/lookup-self"
// vaultTokenRevokePath is the path used to revoke a token
vaultTokenRevokePath = "auth/token/revoke-accessor"
// vaultRoleLookupPath is the path to lookup a role
vaultRoleLookupPath = "auth/token/roles/%s"
// vaultRoleCreatePath is the path to create a token from a role
vaultTokenRoleCreatePath = "auth/token/create/%s"
)
var (
// vaultUnrecoverableError matches unrecoverable errors
vaultUnrecoverableError = regexp.MustCompile(`Code:\s+40(0|3|4)`)
// vaultCapabilitiesCapability is the expected capability of Nomad's Vault
// token on the the path. The token must have at least one of the
// capabilities.
vaultCapabilitiesCapability = []string{"update", "root"}
// vaultTokenRenewCapability is the expected capability Nomad's
// Vault token should have on the path. The token must have at least one of
// the capabilities.
vaultTokenRenewCapability = []string{"update", "root"}
// vaultTokenLookupCapability is the expected capability Nomad's
// Vault token should have on the path. The token must have at least one of
// the capabilities.
vaultTokenLookupCapability = []string{"update", "root"}
// vaultTokenLookupSelfCapability is the expected capability Nomad's
// Vault token should have on the path. The token must have at least one of
// the capabilities.
vaultTokenLookupSelfCapability = []string{"update", "root"}
// vaultTokenRevokeCapability is the expected capability Nomad's
// Vault token should have on the path. The token must have at least one of
// the capabilities.
vaultTokenRevokeCapability = []string{"update", "root"}
// vaultRoleLookupCapability is the the expected capability Nomad's Vault
// token should have on the path. The token must have at least one of the
// capabilities.
vaultRoleLookupCapability = []string{"read", "root"}
// vaultTokenRoleCreateCapability is the the expected capability Nomad's Vault
// token should have on the path. The token must have at least one of the
// capabilities.
vaultTokenRoleCreateCapability = []string{"update", "root"}
)
// VaultClient is the Servers interface for interfacing with Vault
@ -417,7 +474,7 @@ func (v *vaultClient) renewalLoop() {
// Set base values and add some backoff
v.logger.Printf("[DEBUG] vault: got error or bad auth, so backing off: %v", err)
v.logger.Printf("[WARN] vault: got error or bad auth, so backing off: %v", err)
switch {
case backoff < 5:
backoff = 5
@ -477,8 +534,9 @@ func (v *vaultClient) renew() error {
// getWrappingFn returns an appropriate wrapping function for Nomad Servers
func (v *vaultClient) getWrappingFn() func(operation, path string) string {
createPath := "auth/token/create"
if !v.tokenData.Root {
createPath = fmt.Sprintf("auth/token/create/%s", v.tokenData.Role)
role := v.getRole()
if role != "" {
createPath = fmt.Sprintf("auth/token/create/%s", role)
}
return func(operation, path string) string {
@ -497,10 +555,18 @@ func (v *vaultClient) getWrappingFn() func(operation, path string) string {
func (v *vaultClient) parseSelfToken() error {
// Get the initial lease duration
auth := v.client.Auth().Token()
self, err := auth.LookupSelf()
var self *vapi.Secret
// Try looking up the token using the self endpoint
secret, err := auth.LookupSelf()
if err != nil {
return fmt.Errorf("failed to lookup Vault periodic token: %v", err)
// Try looking up our token directly
self, err = auth.Lookup(v.client.Token())
if err != nil {
return fmt.Errorf("failed to lookup Vault periodic token: %v", err)
}
}
self = secret
// Read and parse the fields
var data tokenData
@ -516,7 +582,28 @@ func (v *vaultClient) parseSelfToken() error {
}
}
// 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:
// 1) If token is non-root or is but has a creation ttl
// a) The token must be renewable
// b) Token must have a non-zero TTL
// 2) Must have update capability for "auth/token/lookup/" (used to verify incoming tokens)
// 3) Must have update capability for "/auth/token/revoke-accessor/" (used to revoke unneeded tokens)
// 4) If configured to create tokens against a role:
// a) Must have read capability for "auth/token/roles/<role_name" (Can just attemp a read)
// b) Must have update capability for path "auth/token/create/<role_name>"
// c) Role must:
// 1) Not allow orphans
// 2) Must allow tokens to be renewed
// 3) Must not have an explicit max TTL
// 4) Must have non-zero period
// 5) If not configured against a role, the token must be root
var mErr multierror.Error
role := v.getRole()
if !root {
// All non-root tokens must be renewable
if !data.Renewable {
@ -534,7 +621,7 @@ func (v *vaultClient) parseSelfToken() error {
}
// There must be a valid role since we aren't root
if data.Role == "" {
if role == "" {
multierror.Append(&mErr, fmt.Errorf("token role name must be set when not using a root token"))
}
@ -548,18 +635,106 @@ func (v *vaultClient) parseSelfToken() error {
}
}
// Check we have the correct capabilities
if err := v.validateCapabilities(role, root); err != nil {
multierror.Append(&mErr, err)
}
// If given a role validate it
if data.Role != "" {
if err := v.validateRole(data.Role); err != nil {
if role != "" {
if err := v.validateRole(role); err != nil {
multierror.Append(&mErr, err)
}
}
data.Root = root
v.tokenData = &data
return mErr.ErrorOrNil()
}
// getRole returns the role name to be used when creating tokens
func (v *vaultClient) getRole() string {
if v.config.Role != "" {
return v.config.Role
}
return v.tokenData.Role
}
// validateCapabilities checks that Nomad's Vault token has the correct
// capabilities.
func (v *vaultClient) validateCapabilities(role string, root bool) error {
// Check if the token can lookup capabilities.
var mErr multierror.Error
_, _, err := v.hasCapability(vaultCapabilitiesLookupPath, vaultCapabilitiesCapability)
if err != nil {
// Check if there is a permission denied
if vaultUnrecoverableError.MatchString(err.Error()) {
// Since we can't read permissions, we just log a warning that we
// can't tell if the Vault token will work
msg := fmt.Sprintf("Can not lookup token capabilities. "+
"As such certain operations may fail in the future. "+
"Please give Nomad a Vault token with one of the following "+
"capabilities %q on %q so that the required capabilities can be verified",
vaultCapabilitiesCapability, vaultCapabilitiesLookupPath)
v.logger.Printf("[WARN] vault: %s", msg)
return nil
} else {
multierror.Append(&mErr, err)
}
}
// verify is a helper function that verifies the token has one of the
// capabilities on the given path and adds an issue to the error
verify := func(path string, requiredCaps []string) {
ok, caps, err := v.hasCapability(path, requiredCaps)
if err != nil {
multierror.Append(&mErr, err)
} else if !ok {
multierror.Append(&mErr,
fmt.Errorf("token must have one of the following capabilities %q on %q; has %v", requiredCaps, path, caps))
}
}
// Check if we are verifying incoming tokens
if !v.config.AllowsUnauthenticated() {
verify(vaultTokenLookupPath, vaultTokenLookupCapability)
}
// Verify we can renew our selves tokens
verify(vaultTokenRenewPath, vaultTokenRenewCapability)
// Verify we can revoke tokens
verify(vaultTokenRevokePath, vaultTokenRevokeCapability)
// If we are using a role verify the capability
if role != "" {
// Verify we can read the role
verify(fmt.Sprintf(vaultRoleLookupPath, role), vaultRoleLookupCapability)
// Verify we can create from the role
verify(fmt.Sprintf(vaultTokenRoleCreatePath, role), vaultTokenRoleCreateCapability)
}
return mErr.ErrorOrNil()
}
// hasCapability takes a path and returns whether the token has at least one of
// the required capabilities on the given path. It also returns the set of
// capabilities the token does have as well as any error that occured.
func (v *vaultClient) hasCapability(path string, required []string) (bool, []string, error) {
caps, err := v.client.Sys().CapabilitiesSelf(path)
if err != nil {
return false, nil, err
}
for _, c := range caps {
for _, r := range required {
if c == r {
return true, caps, nil
}
}
}
return false, caps, nil
}
// validateRole contacts Vault and checks that the given Vault role is valid for
// the purposes of being used by Nomad
func (v *vaultClient) validateRole(role string) error {
@ -679,12 +854,13 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
// token or a role based token
var secret *vapi.Secret
var err error
if v.tokenData.Root {
role := v.getRole()
if v.tokenData.Root && role == "" {
req.Period = v.childTTL
secret, err = v.auth.Create(req)
} else {
// Make the token using the role
secret, err = v.auth.CreateWithRole(req, v.tokenData.Role)
secret, err = v.auth.CreateWithRole(req, v.getRole())
}
// Determine whether it is unrecoverable

View file

@ -21,25 +21,129 @@ import (
)
const (
// authPolicy is a policy that allows token creation operations
authPolicy = `path "auth/token/create/test" {
capabilities = ["create", "update"]
// nomadRoleManagementPolicy is a policy that allows nomad to manage tokens
nomadRoleManagementPolicy = `
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "auth/token/lookup/*" {
capabilities = ["read"]
path "auth/token/lookup" {
capabilities = ["update"]
}
path "auth/token/roles/test" {
capabilities = ["read"]
}
path "/auth/token/revoke-accessor/*" {
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"]
}
`
)
// defaultTestVaultWhitelistRoleAndToken creates a test Vault role and returns a token
// created in that role
func defaultTestVaultWhitelistRoleAndToken(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
return testVaultRoleAndToken(v, t, vaultPolicies, d,
[]string{"nomad-role-create", "nomad-role-management"})
}
// defaultTestVaultBlacklistRoleAndToken creates a test Vault role using
// disallowed_policies and returns a token created in that role
func defaultTestVaultBlacklistRoleAndToken(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) {
conf := &config.VaultConfig{}
logger := log.New(os.Stderr, "", log.LstdFlags)
@ -96,13 +200,17 @@ func TestVaultClient_ValidateRole(t *testing.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,
"explicit_max_ttl": 10,
}
v.Config.Token = testVaultRoleAndToken(v, t, data)
v.Config.Token = testVaultRoleAndToken(v, t, vaultPolicies, data, nil)
logger := log.New(os.Stderr, "", log.LstdFlags)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
@ -139,6 +247,59 @@ func TestVaultClient_ValidateRole(t *testing.T) {
}
}
func TestVaultClient_ValidateToken(t *testing.T) {
v := testutil.NewTestVault(t).Start()
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 := log.New(os.Stderr, "", log.LstdFlags)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
client, err := NewVaultClient(v.Config, logger, 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 not 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 orphan 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) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
@ -176,7 +337,7 @@ func TestVaultClient_SetConfig(t *testing.T) {
defer v2.Stop()
// Set the configs token in a new test role
v2.Config.Token = defaultTestVaultRoleAndToken(v2, t, 20)
v2.Config.Token = defaultTestVaultWhitelistRoleAndToken(v2, t, 20)
logger := log.New(os.Stderr, "", log.LstdFlags)
client, err := NewVaultClient(v.Config, logger, nil)
@ -198,55 +359,17 @@ func TestVaultClient_SetConfig(t *testing.T) {
waitForConnection(client, t)
if client.tokenData == nil || len(client.tokenData.Policies) != 2 {
if client.tokenData == nil || len(client.tokenData.Policies) != 3 {
t.Fatalf("unexpected token: %v", client.tokenData)
}
}
// defaultTestVaultRoleAndToken creates a test Vault role and returns a token
// created in that role
func defaultTestVaultRoleAndToken(v *testutil.TestVault, t *testing.T, rolePeriod int) string {
d := make(map[string]interface{}, 2)
d["allowed_policies"] = "auth"
d["period"] = rolePeriod
return testVaultRoleAndToken(v, t, d)
}
// testVaultRoleAndToken creates a test Vault role with the specified data and
// returns a token created in that role
func testVaultRoleAndToken(v *testutil.TestVault, t *testing.T, data map[string]interface{}) string {
// Build the auth policy
sys := v.Client.Sys()
if err := sys.PutPolicy("auth", authPolicy); err != nil {
t.Fatalf("failed to create auth policy: %v", 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{}
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_RenewalLoop(t *testing.T) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultRoleAndToken(v, t, 5)
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
// Start the client
logger := log.New(os.Stderr, "", log.LstdFlags)
@ -386,7 +509,7 @@ func TestVaultClient_LookupToken_Role(t *testing.T) {
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultRoleAndToken(v, t, 5)
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
logger := log.New(os.Stderr, "", log.LstdFlags)
client, err := NewVaultClient(v.Config, logger, nil)
@ -409,7 +532,7 @@ func TestVaultClient_LookupToken_Role(t *testing.T) {
t.Fatalf("failed to parse policies: %v", err)
}
expected := []string{"auth", "default"}
expected := []string{"default", "nomad-role-create", "nomad-role-management"}
if !reflect.DeepEqual(policies, expected) {
t.Fatalf("Unexpected policies; got %v; want %v", policies, expected)
}
@ -543,12 +666,12 @@ func TestVaultClient_CreateToken_Root(t *testing.T) {
}
}
func TestVaultClient_CreateToken_Role(t *testing.T) {
func TestVaultClient_CreateToken_Whitelist_Role(t *testing.T) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultRoleAndToken(v, t, 5)
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
// Start the client
logger := log.New(os.Stderr, "", log.LstdFlags)
@ -590,12 +713,110 @@ func TestVaultClient_CreateToken_Role(t *testing.T) {
}
}
func TestVaultClient_CreateToken_Root_Target_Role(t *testing.T) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
// Create the test role
defaultTestVaultWhitelistRoleAndToken(v, t, 5)
// Target the test role
v.Config.Role = "test"
// Start the client
logger := log.New(os.Stderr, "", log.LstdFlags)
client, err := NewVaultClient(v.Config, logger, 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_Blacklist_Role(t *testing.T) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultBlacklistRoleAndToken(v, t, 5)
v.Config.Role = "test"
// Start the client
logger := log.New(os.Stderr, "", log.LstdFlags)
client, err := NewVaultClient(v.Config, logger, 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) {
v := testutil.NewTestVault(t).Start()
defer v.Stop()
// Set the configs token in a new test role
defaultTestVaultRoleAndToken(v, t, 5)
defaultTestVaultWhitelistRoleAndToken(v, t, 5)
v.Config.Token = "foo-bar"
// Start the client
@ -634,7 +855,7 @@ func TestVaultClient_CreateToken_Role_Unrecoverable(t *testing.T) {
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultRoleAndToken(v, t, 5)
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
// Start the client
logger := log.New(os.Stderr, "", log.LstdFlags)
@ -796,7 +1017,7 @@ func TestVaultClient_RevokeTokens_Role(t *testing.T) {
defer v.Stop()
// Set the configs token in a new test role
v.Config.Token = defaultTestVaultRoleAndToken(v, t, 5)
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
purged := 0
purge := func(accessors []*structs.VaultAccessor) error {
@ -846,16 +1067,15 @@ func TestVaultClient_RevokeTokens_Role(t *testing.T) {
}
// 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)
}
if purged != 2 {
t.Fatalf("Expected purged 2; got %d", purged)
}
}
func waitForConnection(v *vaultClient, t *testing.T) {

View file

@ -25,6 +25,21 @@ func (c *TokenAuth) Create(opts *TokenCreateRequest) (*Secret, error) {
return ParseSecret(resp.Body)
}
func (c *TokenAuth) CreateOrphan(opts *TokenCreateRequest) (*Secret, error) {
r := c.c.NewRequest("POST", "/v1/auth/token/create-orphan")
if err := r.SetJSONBody(opts); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ParseSecret(resp.Body)
}
func (c *TokenAuth) CreateWithRole(opts *TokenCreateRequest, roleName string) (*Secret, error) {
r := c.c.NewRequest("POST", "/v1/auth/token/create/"+roleName)
if err := r.SetJSONBody(opts); err != nil {
@ -41,7 +56,12 @@ func (c *TokenAuth) CreateWithRole(opts *TokenCreateRequest, roleName string) (*
}
func (c *TokenAuth) Lookup(token string) (*Secret, error) {
r := c.c.NewRequest("GET", "/v1/auth/token/lookup/"+token)
r := c.c.NewRequest("POST", "/v1/auth/token/lookup")
if err := r.SetJSONBody(map[string]interface{}{
"token": token,
}); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
@ -53,8 +73,12 @@ func (c *TokenAuth) Lookup(token string) (*Secret, error) {
}
func (c *TokenAuth) LookupAccessor(accessor string) (*Secret, error) {
r := c.c.NewRequest("POST", "/v1/auth/token/lookup-accessor/"+accessor)
r := c.c.NewRequest("POST", "/v1/auth/token/lookup-accessor")
if err := r.SetJSONBody(map[string]interface{}{
"accessor": accessor,
}); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
@ -77,10 +101,11 @@ func (c *TokenAuth) LookupSelf() (*Secret, error) {
}
func (c *TokenAuth) Renew(token string, increment int) (*Secret, error) {
r := c.c.NewRequest("PUT", "/v1/auth/token/renew/"+token)
body := map[string]interface{}{"increment": increment}
if err := r.SetJSONBody(body); err != nil {
r := c.c.NewRequest("PUT", "/v1/auth/token/renew")
if err := r.SetJSONBody(map[string]interface{}{
"token": token,
"increment": increment,
}); err != nil {
return nil, err
}
@ -113,7 +138,12 @@ func (c *TokenAuth) RenewSelf(increment int) (*Secret, error) {
// RevokeAccessor revokes a token associated with the given accessor
// along with all the child tokens.
func (c *TokenAuth) RevokeAccessor(accessor string) error {
r := c.c.NewRequest("POST", "/v1/auth/token/revoke-accessor/"+accessor)
r := c.c.NewRequest("POST", "/v1/auth/token/revoke-accessor")
if err := r.SetJSONBody(map[string]interface{}{
"accessor": accessor,
}); err != nil {
return err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return err
@ -126,7 +156,13 @@ func (c *TokenAuth) RevokeAccessor(accessor string) error {
// RevokeOrphan revokes a token without revoking the tree underneath it (so
// child tokens are orphaned rather than revoked)
func (c *TokenAuth) RevokeOrphan(token string) error {
r := c.c.NewRequest("PUT", "/v1/auth/token/revoke-orphan/"+token)
r := c.c.NewRequest("PUT", "/v1/auth/token/revoke-orphan")
if err := r.SetJSONBody(map[string]interface{}{
"token": token,
}); err != nil {
return err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return err
@ -136,7 +172,9 @@ func (c *TokenAuth) RevokeOrphan(token string) error {
return nil
}
// RevokeSelf revokes the token making the call
// RevokeSelf revokes the token making the call. The `token` parameter is kept
// for backwards compatibility but is ignored; only the client's set token has
// an effect.
func (c *TokenAuth) RevokeSelf(token string) error {
r := c.c.NewRequest("PUT", "/v1/auth/token/revoke-self")
resp, err := c.c.RawRequest(r)
@ -152,7 +190,13 @@ func (c *TokenAuth) RevokeSelf(token string) error {
// the entire tree underneath -- all of its child tokens, their child tokens,
// etc.
func (c *TokenAuth) RevokeTree(token string) error {
r := c.c.NewRequest("PUT", "/v1/auth/token/revoke/"+token)
r := c.c.NewRequest("PUT", "/v1/auth/token/revoke")
if err := r.SetJSONBody(map[string]interface{}{
"token": token,
}); err != nil {
return err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return err

View file

@ -48,7 +48,7 @@ type Config struct {
redirectSetup sync.Once
// MaxRetries controls the maximum number of times to retry when a 5xx error
// occurs. Set to 0 or less to disable retrying.
// occurs. Set to 0 or less to disable retrying. Defaults to 0.
MaxRetries int
}
@ -99,12 +99,10 @@ func DefaultConfig() *Config {
config.Address = v
}
config.MaxRetries = pester.DefaultClient.MaxRetries
return config
}
// ConfigureTLS takes a set of TLS configurations and applies those to the HTTP client.
// ConfigureTLS takes a set of TLS configurations and applies those to the the HTTP client.
func (c *Config) ConfigureTLS(t *TLSConfig) error {
if c.HttpClient == nil {
@ -289,6 +287,11 @@ func (c *Client) SetAddress(addr string) error {
return nil
}
// Address returns the Vault URL the client is configured to connect to
func (c *Client) Address() string {
return c.addr.String()
}
// SetWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
// for a given operation and path
func (c *Client) SetWrappingLookupFunc(lookupFunc WrappingLookupFunc) {
@ -327,17 +330,19 @@ func (c *Client) NewRequest(method, path string) *Request {
Params: make(map[string][]string),
}
var lookupPath string
switch {
case strings.HasPrefix(path, "/v1/"):
lookupPath = strings.TrimPrefix(path, "/v1/")
case strings.HasPrefix(path, "v1/"):
lookupPath = strings.TrimPrefix(path, "v1/")
default:
lookupPath = path
}
if c.wrappingLookupFunc != nil {
var lookupPath string
switch {
case strings.HasPrefix(path, "/v1/"):
lookupPath = strings.TrimPrefix(path, "/v1/")
case strings.HasPrefix(path, "v1/"):
lookupPath = strings.TrimPrefix(path, "v1/")
default:
lookupPath = path
}
req.WrapTTL = c.wrappingLookupFunc(method, lookupPath)
} else {
req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath)
}
return req

View file

@ -3,6 +3,8 @@ package api
import (
"bytes"
"fmt"
"net/http"
"os"
"github.com/hashicorp/vault/helper/jsonutil"
)
@ -11,6 +13,26 @@ const (
wrappedResponseLocation = "cubbyhole/response"
)
var (
// The default TTL that will be used with `sys/wrapping/wrap`, can be
// changed
DefaultWrappingTTL = "5m"
// The default function used if no other function is set, which honors the
// env var and wraps `sys/wrapping/wrap`
DefaultWrappingLookupFunc = func(operation, path string) string {
if os.Getenv(EnvVaultWrapTTL) != "" {
return os.Getenv(EnvVaultWrapTTL)
}
if (operation == "PUT" || operation == "POST") && path == "sys/wrapping/wrap" {
return DefaultWrappingTTL
}
return ""
}
)
// Logical is used to perform logical backend operations on Vault.
type Logical struct {
c *Client
@ -38,7 +60,10 @@ func (c *Logical) Read(path string) (*Secret, error) {
}
func (c *Logical) List(path string) (*Secret, error) {
r := c.c.NewRequest("GET", "/v1/"+path)
r := c.c.NewRequest("LIST", "/v1/"+path)
// Set this for broader compatibility, but we use LIST above to be able to
// handle the wrapping lookup function
r.Method = "GET"
r.Params.Set("list", "true")
resp, err := c.c.RawRequest(r)
if resp != nil {
@ -93,10 +118,48 @@ func (c *Logical) Delete(path string) (*Secret, error) {
}
func (c *Logical) Unwrap(wrappingToken string) (*Secret, error) {
origToken := c.c.Token()
defer c.c.SetToken(origToken)
var data map[string]interface{}
if wrappingToken != "" {
if c.c.Token() == "" {
c.c.SetToken(wrappingToken)
} else if wrappingToken != c.c.Token() {
data = map[string]interface{}{
"token": wrappingToken,
}
}
}
c.c.SetToken(wrappingToken)
r := c.c.NewRequest("PUT", "/v1/sys/wrapping/unwrap")
if err := r.SetJSONBody(data); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
if resp != nil && resp.StatusCode != 404 {
return nil, err
}
}
if resp == nil {
return nil, nil
}
switch resp.StatusCode {
case http.StatusOK: // New method is supported
return ParseSecret(resp.Body)
case http.StatusNotFound: // Fall back to old method
default:
return nil, nil
}
if wrappingToken != "" {
origToken := c.c.Token()
defer c.c.SetToken(origToken)
c.c.SetToken(wrappingToken)
}
secret, err := c.Read(wrappedResponseLocation)
if err != nil {

View file

@ -62,6 +62,7 @@ type SSHHelperConfig struct {
AllowedCidrList string `hcl:"allowed_cidr_list"`
AllowedRoles string `hcl:"allowed_roles"`
TLSSkipVerify bool `hcl:"tls_skip_verify"`
TLSServerName string `hcl:"tls_server_name"`
}
// SetTLSParameters sets the TLS parameters for this SSH agent.
@ -70,6 +71,7 @@ func (c *SSHHelperConfig) SetTLSParameters(clientConfig *Config, certPool *x509.
InsecureSkipVerify: c.TLSSkipVerify,
MinVersion: tls.VersionTLS12,
RootCAs: certPool,
ServerName: c.TLSServerName,
}
transport := cleanhttp.DefaultTransport()
@ -77,6 +79,16 @@ func (c *SSHHelperConfig) SetTLSParameters(clientConfig *Config, certPool *x509.
clientConfig.HttpClient.Transport = transport
}
// Returns true if any of the following conditions are true:
// * CA cert is configured
// * CA path is configured
// * configured to skip certificate verification
// * TLS server name is configured
//
func (c *SSHHelperConfig) shouldSetTLSParameters() bool {
return c.CACert != "" || c.CAPath != "" || c.TLSServerName != "" || c.TLSSkipVerify
}
// NewClient returns a new client for the configuration. This client will be used by the
// vault-ssh-helper to communicate with Vault server and verify the OTP entered by user.
// If the configuration supplies Vault SSL certificates, then the client will
@ -89,7 +101,7 @@ func (c *SSHHelperConfig) NewClient() (*Client, error) {
clientConfig.Address = c.VaultAddr
// Check if certificates are provided via config file.
if c.CACert != "" || c.CAPath != "" || c.TLSSkipVerify {
if c.shouldSetTLSParameters() {
rootConfig := &rootcerts.Config{
CAFile: c.CACert,
CAPath: c.CAPath,
@ -145,6 +157,7 @@ func ParseSSHHelperConfig(contents string) (*SSHHelperConfig, error) {
"allowed_cidr_list",
"allowed_roles",
"tls_skip_verify",
"tls_server_name",
}
if err := checkHCLKeys(list, valid); err != nil {
return nil, multierror.Prefix(err, "ssh_helper:")

View file

@ -38,6 +38,7 @@ type InitRequest struct {
RecoveryShares int `json:"recovery_shares"`
RecoveryThreshold int `json:"recovery_threshold"`
RecoveryPGPKeys []string `json:"recovery_pgp_keys"`
RootTokenPGPKey string `json:"root_token_pgp_key"`
}
type InitStatusResponse struct {

6
vendor/vendor.json vendored
View file

@ -828,10 +828,10 @@
"revisionTime": "2016-08-21T23:40:57Z"
},
{
"checksumSHA1": "JH8wmQ8cWdn7mYu1T7gJ3IMIrec=",
"checksumSHA1": "31yBeS6U3xm7VJ7ZvDxRgBxXP0A=",
"path": "github.com/hashicorp/vault/api",
"revision": "182ba68a9589d4cef95234134aaa498a686e3de3",
"revisionTime": "2016-08-21T23:40:57Z"
"revision": "f4adc7fa960ed8e828f94bc6785bcdbae8d1b263",
"revisionTime": "2016-12-16T21:07:16Z"
},
{
"checksumSHA1": "5lR6EdY0ARRdKAq3hZcL38STD8Q=",

View file

@ -1,6 +1,6 @@
# Allow creating tokens under the role
path "auth/token/create/nomad-server" {
capabilities = ["create", "update"]
capabilities = ["update"]
}
# Allow looking up the role

View file

@ -47,6 +47,12 @@ vault {
- `enabled` `(bool: false)` - Specifies if the Vault integration should be
activated.
- `create_from_role` `(string: "")` - Specifies the role to create tokens from.
The token given to Nomad does not have to be created from this role but must
have "update" capability on "auth/token/create/<create_from_role>" path in
Vault. If this value is unset and the token is created from a role, the value
is defaulted to the role the token is from.
- `task_token_ttl` `(string: "")` - Specifies the TTL of created tokens when
using a root token. This is specified using a label suffix like "30s" or "1h".

View file

@ -69,7 +69,7 @@ when creating the role with the Vault endpoint `/auth/token/roles/<role_name>`:
```hcl
# Allow creating tokens under the role
path "auth/token/create/nomad-server" {
capabilities = ["create", "update"]
capabilities = ["update"]
}
# Allow looking up the role