diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl index 0059210f2..587d68423 100644 --- a/command/agent/config-test-fixtures/basic.hcl +++ b/command/agent/config-test-fixtures/basic.hcl @@ -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 diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index e7a78edf5..9dfbf6aaf 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -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", diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go index 2195c247d..17268d6b3 100644 --- a/command/agent/config_parse_test.go +++ b/command/agent/config_parse_test.go @@ -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", diff --git a/nomad/structs/config/vault.go b/nomad/structs/config/vault.go index a958661c6..42115c1a9 100644 --- a/nomad/structs/config/vault.go +++ b/nomad/structs/config/vault.go @@ -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/". 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 } diff --git a/nomad/vault.go b/nomad/vault.go index 1c71afc74..a78c82210 100644 --- a/nomad/vault.go +++ b/nomad/vault.go @@ -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/" + // 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 diff --git a/nomad/vault_test.go b/nomad/vault_test.go index d1a9188c4..e17c3bce0 100644 --- a/nomad/vault_test.go +++ b/nomad/vault_test.go @@ -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) { diff --git a/vendor/github.com/hashicorp/vault/api/auth_token.go b/vendor/github.com/hashicorp/vault/api/auth_token.go index 1901ea110..aff10f410 100644 --- a/vendor/github.com/hashicorp/vault/api/auth_token.go +++ b/vendor/github.com/hashicorp/vault/api/auth_token.go @@ -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 diff --git a/vendor/github.com/hashicorp/vault/api/client.go b/vendor/github.com/hashicorp/vault/api/client.go index 179d2bdc9..88a8ea4f9 100644 --- a/vendor/github.com/hashicorp/vault/api/client.go +++ b/vendor/github.com/hashicorp/vault/api/client.go @@ -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 diff --git a/vendor/github.com/hashicorp/vault/api/logical.go b/vendor/github.com/hashicorp/vault/api/logical.go index fb8288e73..0d5e7d495 100644 --- a/vendor/github.com/hashicorp/vault/api/logical.go +++ b/vendor/github.com/hashicorp/vault/api/logical.go @@ -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 { diff --git a/vendor/github.com/hashicorp/vault/api/ssh_agent.go b/vendor/github.com/hashicorp/vault/api/ssh_agent.go index 5a8192ae9..729fd99c4 100644 --- a/vendor/github.com/hashicorp/vault/api/ssh_agent.go +++ b/vendor/github.com/hashicorp/vault/api/ssh_agent.go @@ -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:") diff --git a/vendor/github.com/hashicorp/vault/api/sys_init.go b/vendor/github.com/hashicorp/vault/api/sys_init.go index d307f732b..f824ab7dd 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_init.go +++ b/vendor/github.com/hashicorp/vault/api/sys_init.go @@ -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 { diff --git a/vendor/vendor.json b/vendor/vendor.json index 34e53b1ec..008156732 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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=", diff --git a/website/source/data/vault/nomad-server-policy.hcl b/website/source/data/vault/nomad-server-policy.hcl index 95a5fbe22..d1190af7e 100644 --- a/website/source/data/vault/nomad-server-policy.hcl +++ b/website/source/data/vault/nomad-server-policy.hcl @@ -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 diff --git a/website/source/docs/agent/configuration/vault.html.md b/website/source/docs/agent/configuration/vault.html.md index e1e86abf0..a491d6348 100644 --- a/website/source/docs/agent/configuration/vault.html.md +++ b/website/source/docs/agent/configuration/vault.html.md @@ -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/" 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". diff --git a/website/source/docs/vault-integration/index.html.md b/website/source/docs/vault-integration/index.html.md index 7221e4653..7baff34b0 100644 --- a/website/source/docs/vault-integration/index.html.md +++ b/website/source/docs/vault-integration/index.html.md @@ -69,7 +69,7 @@ when creating the role with the Vault endpoint `/auth/token/roles/`: ```hcl # Allow creating tokens under the role path "auth/token/create/nomad-server" { - capabilities = ["create", "update"] + capabilities = ["update"] } # Allow looking up the role