From 87f2d5c17b11be4e6542d932a113f4f00ecdb4c8 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 18 Aug 2016 10:50:47 -0700 Subject: [PATCH] Start --- nomad/node_endpoint.go | 82 +++++++++++++++++++ nomad/structs/funcs.go | 4 +- nomad/structs/structs.go | 24 +++++- nomad/structs/structs_test.go | 59 ++++++------- nomad/vault.go | 60 +++++++++++++- .../hashicorp/vault/api/auth_token.go | 1 + .../hashicorp/vault/api/sys_audit.go | 39 ++++----- .../hashicorp/vault/api/sys_auth.go | 24 ++++-- .../hashicorp/vault/api/sys_capabilities.go | 9 +- .../hashicorp/vault/api/sys_init.go | 8 +- .../hashicorp/vault/api/sys_mounts.go | 35 ++++---- .../hashicorp/vault/api/sys_policy.go | 41 ++++------ .../hashicorp/vault/api/sys_rekey.go | 6 +- .../hashicorp/vault/api/sys_rotate.go | 27 ++---- vendor/vendor.json | 12 ++- 15 files changed, 288 insertions(+), 143 deletions(-) diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index 17e259808..4170d4fbe 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -874,3 +874,85 @@ func (b *batchFuture) Respond(index uint64, err error) { b.err = err close(b.doneCh) } + +// DeriveVaultToken is used by the clients to request wrapped Vault tokens for +// tasks +func (n *Node) DeriveVaultToken(args *structs.DeriveVaultTokenRequest, + reply *structs.DeriveVaultTokenResponse) error { + if done, err := n.srv.forward("Node.DeriveVaultToken", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"nomad", "client", "derive_vault_token"}, time.Now()) + + // Verify the arguments + if args.NodeID == "" { + return fmt.Errorf("missing node ID") + } + if args.SecretID == "" { + return fmt.Errorf("missing node SecretID") + } + if args.AllocID == "" { + return fmt.Errorf("missing allocation ID") + } + if len(args.Tasks) == 0 { + return fmt.Errorf("no tasks specified") + } + + // Verify the following: + // * The Node exists and has the correct SecretID + // * The Allocation exists on the specified node + // * The allocation contains the given tasks and they each require Vault + // tokens + snap, err := n.srv.fsm.State().Snapshot() + if err != nil { + return err + } + node, err := snap.NodeByID(args.NodeID) + if err != nil { + return err + } + if node == nil { + return fmt.Errorf("Node %q does not exist", args.NodeID) + } + //if node.SecretID != args.SecretID { + //return fmt.Errorf("SecretID mismatch") + //} + + alloc, err := snap.AllocByID(args.AllocID) + if err != nil { + return err + } + if alloc == nil { + return fmt.Errorf("Allocation %q does not exist", args.AllocID) + } + if alloc.NodeID != args.NodeID { + return fmt.Errorf("Allocation %q not running on Node %q", args.AllocID, args.NodeID) + } + + // Check the policies + policies := alloc.Job.VaultPolicies() + if policies == nil { + return fmt.Errorf("Job doesn't require Vault policies") + } + tg, ok := policies[alloc.TaskGroup] + if !ok { + return fmt.Errorf("Task group does not require Vault policies") + } + + var unneeded []string + for _, task := range args.Tasks { + taskVault := tg[task] + if len(taskVault.Policies) == 0 { + unneeded = append(unneeded, task) + } + } + + if len(unneeded) != 0 { + return fmt.Errorf("Requested Vault tokens for tasks without defined Vault policies: %s", + strings.Join(unneeded, ", ")) + } + + // At this point the request is valid and we should contact Vault for tokens + + return nil +} diff --git a/nomad/structs/funcs.go b/nomad/structs/funcs.go index 68f0af18c..37df72c33 100644 --- a/nomad/structs/funcs.go +++ b/nomad/structs/funcs.go @@ -253,12 +253,12 @@ func SliceStringIsSubset(larger, smaller []string) (bool, []string) { // VaultPoliciesSet takes the structure returned by VaultPolicies and returns // the set of required policies -func VaultPoliciesSet(policies map[string]map[string][]string) []string { +func VaultPoliciesSet(policies map[string]map[string]*Vault) []string { set := make(map[string]struct{}) for _, tgp := range policies { for _, tp := range tgp { - for _, p := range tp { + for _, p := range tp.Policies { set[p] = struct{}{} } } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 5a25d99c6..962e59b9b 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -354,6 +354,22 @@ type PeriodicForceRequest struct { WriteRequest } +type DeriveVaultTokenRequest struct { + NodeID string + SecretID string + AllocID string + Tasks []string + QueryOptions +} + +type DeriveVaultTokenResponse struct { + NodeID string + SecretID string + AllocID string + Tasks []string + QueryMeta +} + // GenericRequest is used to request where no // specific information is needed. type GenericRequest struct { @@ -1230,11 +1246,11 @@ func (j *Job) IsPeriodic() bool { } // VaultPolicies returns the set of Vault policies per task group, per task -func (j *Job) VaultPolicies() map[string]map[string][]string { - policies := make(map[string]map[string][]string, len(j.TaskGroups)) +func (j *Job) VaultPolicies() map[string]map[string]*Vault { + policies := make(map[string]map[string]*Vault, len(j.TaskGroups)) for _, tg := range j.TaskGroups { - tgPolicies := make(map[string][]string, len(tg.Tasks)) + tgPolicies := make(map[string]*Vault, len(tg.Tasks)) policies[tg.Name] = tgPolicies for _, task := range tg.Tasks { @@ -1242,7 +1258,7 @@ func (j *Job) VaultPolicies() map[string]map[string][]string { continue } - tgPolicies[task.Name] = task.Vault.Policies + tgPolicies[task.Name] = task.Vault } } diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index ec0ee0aa3..7b54a2b5c 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -224,8 +224,25 @@ func TestJob_SystemJob_Validate(t *testing.T) { func TestJob_VaultPolicies(t *testing.T) { j0 := &Job{} - e0 := make(map[string]map[string][]string, 0) + e0 := make(map[string]map[string]*Vault, 0) + vj1 := &Vault{ + Policies: []string{ + "p1", + "p2", + }, + } + vj2 := &Vault{ + Policies: []string{ + "p3", + "p4", + }, + } + vj3 := &Vault{ + Policies: []string{ + "p5", + }, + } j1 := &Job{ TaskGroups: []*TaskGroup{ &TaskGroup{ @@ -235,13 +252,8 @@ func TestJob_VaultPolicies(t *testing.T) { Name: "t1", }, &Task{ - Name: "t2", - Vault: &Vault{ - Policies: []string{ - "p1", - "p2", - }, - }, + Name: "t2", + Vault: vj1, }, }, }, @@ -249,40 +261,31 @@ func TestJob_VaultPolicies(t *testing.T) { Name: "bar", Tasks: []*Task{ &Task{ - Name: "t3", - Vault: &Vault{ - Policies: []string{ - "p3", - "p4", - }, - }, + Name: "t3", + Vault: vj2, }, &Task{ - Name: "t4", - Vault: &Vault{ - Policies: []string{ - "p5", - }, - }, + Name: "t4", + Vault: vj3, }, }, }, }, } - e1 := map[string]map[string][]string{ - "foo": map[string][]string{ - "t2": []string{"p1", "p2"}, + e1 := map[string]map[string]*Vault{ + "foo": map[string]*Vault{ + "t2": vj1, }, - "bar": map[string][]string{ - "t3": []string{"p3", "p4"}, - "t4": []string{"p5"}, + "bar": map[string]*Vault{ + "t3": vj2, + "t4": vj3, }, } cases := []struct { Job *Job - Expected map[string]map[string][]string + Expected map[string]map[string]*Vault }{ { Job: j0, diff --git a/nomad/vault.go b/nomad/vault.go index a7c734562..3c19006a2 100644 --- a/nomad/vault.go +++ b/nomad/vault.go @@ -21,6 +21,11 @@ const ( // minimumTokenTTL is the minimum Token TTL allowed for child tokens. minimumTokenTTL = 5 * time.Minute + + // defaultTokenTTL is the default Token TTL used when the passed token is a + // root token such that child tokens aren't being created against a role + // that has defined a TTL + defaultTokenTTL = "72h" ) // VaultClient is the Servers interface for interfacing with Vault @@ -131,6 +136,9 @@ func NewVaultClient(c *config.VaultConfig, logger *log.Logger) (*vaultClient, er } v.childTTL = c.TaskTokenTTL + } else { + // Default the TaskTokenTTL + v.childTTL = defaultTokenTTL } // Get the Vault API configuration @@ -414,8 +422,58 @@ func (v *vaultClient) ConnectionEstablished() bool { return v.connEstablished } +// CreateToken takes the allocation and task and returns an appropriate Vault +// token func (v *vaultClient) CreateToken(a *structs.Allocation, task string) (*vapi.Secret, error) { - return nil, nil + // Nothing to do + if !v.enabled { + return nil, fmt.Errorf("Vault integration disabled") + } + + // Check if we have established a connection with Vault + if !v.ConnectionEstablished() { + return nil, fmt.Errorf("Connection to Vault has not been established. Retry") + } + + // Retrieve the Vault block for the task + policies := a.Job.VaultPolicies() + if policies == nil { + return nil, fmt.Errorf("Job doesn't require Vault policies") + } + tg, ok := policies[a.TaskGroup] + if !ok { + return nil, fmt.Errorf("Task group does not require Vault policies") + } + taskVault, ok := tg[task] + if !ok { + return nil, fmt.Errorf("Task does not require Vault policies") + } + + // Build the creation request + req := &vapi.TokenCreateRequest{ + Policies: taskVault.Policies, + Metadata: map[string]string{ + "AllocationID": a.ID, + "Task": task, + "NodeID": a.NodeID, + }, + TTL: v.childTTL, + DisplayName: fmt.Sprintf("%s: %s", a.ID, task), + } + + // Make the request and switch depending on whether we are using a root + // token or a role based token + var secret *vapi.Secret + var err error + if v.token.Root { + req.Period = v.childTTL + secret, err = v.auth.Create(req) + } else { + // Make the token using the role + secret, err = v.auth.CreateWithRole(req, v.token.Role) + } + + return secret, err } // LookupToken takes a Vault token and does a lookup against Vault diff --git a/vendor/github.com/hashicorp/vault/api/auth_token.go b/vendor/github.com/hashicorp/vault/api/auth_token.go index 2dae4df62..1901ea110 100644 --- a/vendor/github.com/hashicorp/vault/api/auth_token.go +++ b/vendor/github.com/hashicorp/vault/api/auth_token.go @@ -170,6 +170,7 @@ type TokenCreateRequest struct { Lease string `json:"lease,omitempty"` TTL string `json:"ttl,omitempty"` ExplicitMaxTTL string `json:"explicit_max_ttl,omitempty"` + Period string `json:"period,omitempty"` NoParent bool `json:"no_parent,omitempty"` NoDefaultPolicy bool `json:"no_default_policy,omitempty"` DisplayName string `json:"display_name"` diff --git a/vendor/github.com/hashicorp/vault/api/sys_audit.go b/vendor/github.com/hashicorp/vault/api/sys_audit.go index b6fed6af9..1ffdef880 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_audit.go +++ b/vendor/github.com/hashicorp/vault/api/sys_audit.go @@ -22,21 +22,12 @@ func (c *Sys) AuditHash(path string, input string) (string, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) - if err != nil { - return "", err - } - - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return "", nil - } - type d struct { - Hash string + Hash string `json:"hash"` } var result d - err = mapstructure.Decode(secret.Data, &result) + err = resp.DecodeJSON(&result) if err != nil { return "", err } @@ -52,26 +43,32 @@ func (c *Sys) ListAudit() (map[string]*Audit, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return nil, err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - - result := map[string]*Audit{} - for k, v := range secret.Data { + mounts := map[string]*Audit{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } var res Audit err = mapstructure.Decode(v, &res) if err != nil { return nil, err } - result[k] = &res + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res } - return result, err + return mounts, nil } func (c *Sys) EnableAudit( @@ -106,7 +103,7 @@ func (c *Sys) DisableAudit(path string) error { } // Structures for the requests/resposne are all down here. They aren't -// individually documentd because the map almost directly to the raw HTTP API +// individually documented because the map almost directly to the raw HTTP API // documentation. Please refer to that documentation for more details. type Audit struct { diff --git a/vendor/github.com/hashicorp/vault/api/sys_auth.go b/vendor/github.com/hashicorp/vault/api/sys_auth.go index 743b8e6d8..1940e8417 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_auth.go +++ b/vendor/github.com/hashicorp/vault/api/sys_auth.go @@ -14,26 +14,32 @@ func (c *Sys) ListAuth() (map[string]*AuthMount, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return nil, err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - - result := map[string]*AuthMount{} - for k, v := range secret.Data { + mounts := map[string]*AuthMount{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } var res AuthMount err = mapstructure.Decode(v, &res) if err != nil { return nil, err } - result[k] = &res + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res } - return result, err + return mounts, nil } func (c *Sys) EnableAuth(path, authType, desc string) error { diff --git a/vendor/github.com/hashicorp/vault/api/sys_capabilities.go b/vendor/github.com/hashicorp/vault/api/sys_capabilities.go index 6d501a495..80f621884 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_capabilities.go +++ b/vendor/github.com/hashicorp/vault/api/sys_capabilities.go @@ -28,17 +28,14 @@ func (c *Sys) Capabilities(token, path string) ([]string, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return nil, err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - var capabilities []string - capabilitiesRaw := secret.Data["capabilities"].([]interface{}) + capabilitiesRaw := result["capabilities"].([]interface{}) for _, capability := range capabilitiesRaw { capabilities = append(capabilities, capability.(string)) } diff --git a/vendor/github.com/hashicorp/vault/api/sys_init.go b/vendor/github.com/hashicorp/vault/api/sys_init.go index 37c2bcc8c..d307f732b 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_init.go +++ b/vendor/github.com/hashicorp/vault/api/sys_init.go @@ -45,7 +45,9 @@ type InitStatusResponse struct { } type InitResponse struct { - Keys []string `json:"keys"` - RecoveryKeys []string `json:"recovery_keys"` - RootToken string `json:"root_token"` + Keys []string `json:"keys"` + KeysB64 []string `json:"keys_base64"` + RecoveryKeys []string `json:"recovery_keys"` + RecoveryKeysB64 []string `json:"recovery_keys_base64"` + RootToken string `json:"root_token"` } diff --git a/vendor/github.com/hashicorp/vault/api/sys_mounts.go b/vendor/github.com/hashicorp/vault/api/sys_mounts.go index 504e5711b..ca5e42707 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_mounts.go +++ b/vendor/github.com/hashicorp/vault/api/sys_mounts.go @@ -15,26 +15,32 @@ func (c *Sys) ListMounts() (map[string]*MountOutput, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return nil, err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - - result := map[string]*MountOutput{} - for k, v := range secret.Data { + mounts := map[string]*MountOutput{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } var res MountOutput err = mapstructure.Decode(v, &res) if err != nil { return nil, err } - result[k] = &res + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res } - return result, nil + return mounts, nil } func (c *Sys) Mount(path string, mountInfo *MountInput) error { @@ -104,17 +110,8 @@ func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) - if err != nil { - return nil, err - } - - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - var result MountConfigOutput - err = mapstructure.Decode(secret.Data, &result) + err = resp.DecodeJSON(&result) if err != nil { return nil, err } diff --git a/vendor/github.com/hashicorp/vault/api/sys_policy.go b/vendor/github.com/hashicorp/vault/api/sys_policy.go index 35e18b388..ba0e17fab 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_policy.go +++ b/vendor/github.com/hashicorp/vault/api/sys_policy.go @@ -1,10 +1,6 @@ package api -import ( - "fmt" - - "github.com/mitchellh/mapstructure" -) +import "fmt" func (c *Sys) ListPolicies() ([]string, error) { r := c.c.NewRequest("GET", "/v1/sys/policy") @@ -14,22 +10,25 @@ func (c *Sys) ListPolicies() ([]string, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return nil, err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil + var ok bool + if _, ok = result["policies"]; !ok { + return nil, fmt.Errorf("policies not found in response") } - var result listPoliciesResp - err = mapstructure.Decode(secret.Data, &result) - if err != nil { - return nil, err + listRaw := result["policies"].([]interface{}) + var policies []string + + for _, val := range listRaw { + policies = append(policies, val.(string)) } - return result.Policies, err + return policies, err } func (c *Sys) GetPolicy(name string) (string, error) { @@ -45,22 +44,18 @@ func (c *Sys) GetPolicy(name string) (string, error) { return "", err } - secret, err := ParseSecret(resp.Body) + var result map[string]interface{} + err = resp.DecodeJSON(&result) if err != nil { return "", err } - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return "", nil + var ok bool + if _, ok = result["rules"]; !ok { + return "", fmt.Errorf("rules not found in response") } - var result getPoliciesResp - err = mapstructure.Decode(secret.Data, &result) - if err != nil { - return "", err - } - - return result.Rules, err + return result["rules"].(string), nil } func (c *Sys) PutPolicy(name, rules string) error { diff --git a/vendor/github.com/hashicorp/vault/api/sys_rekey.go b/vendor/github.com/hashicorp/vault/api/sys_rekey.go index 4fbfbb9fc..e6d039e27 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_rekey.go +++ b/vendor/github.com/hashicorp/vault/api/sys_rekey.go @@ -190,11 +190,13 @@ type RekeyUpdateResponse struct { Nonce string Complete bool Keys []string + KeysB64 []string `json:"keys_base64"` PGPFingerprints []string `json:"pgp_fingerprints"` Backup bool } type RekeyRetrieveResponse struct { - Nonce string - Keys map[string][]string + Nonce string + Keys map[string][]string + KeysB64 map[string][]string `json:"keys_base64"` } diff --git a/vendor/github.com/hashicorp/vault/api/sys_rotate.go b/vendor/github.com/hashicorp/vault/api/sys_rotate.go index 2a78b4691..8108dced8 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_rotate.go +++ b/vendor/github.com/hashicorp/vault/api/sys_rotate.go @@ -1,10 +1,6 @@ package api -import ( - "time" - - "github.com/mitchellh/mapstructure" -) +import "time" func (c *Sys) Rotate() error { r := c.c.NewRequest("POST", "/v1/sys/rotate") @@ -23,25 +19,12 @@ func (c *Sys) KeyStatus() (*KeyStatus, error) { } defer resp.Body.Close() - secret, err := ParseSecret(resp.Body) - if err != nil { - return nil, err - } - - if secret == nil || secret.Data == nil || len(secret.Data) == 0 { - return nil, nil - } - - var result KeyStatus - err = mapstructure.Decode(secret.Data, &result) - if err != nil { - return nil, err - } - - return &result, err + result := new(KeyStatus) + err = resp.DecodeJSON(result) + return result, err } type KeyStatus struct { - Term int + Term int `json:"term"` InstallTime time.Time `json:"install_time"` } diff --git a/vendor/vendor.json b/vendor/vendor.json index cceb23094..1fd4757a5 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -623,10 +623,16 @@ "revisionTime": "2016-06-09T00:18:40Z" }, { - "checksumSHA1": "0rkVtm9F1/pW9EGhHYJpCnY99O8=", + "checksumSHA1": "RAJfRxZ8UmcL6+7VuXAZxBlnM/4=", + "path": "github.com/hashicorp/vault", + "revision": "fece3ca069fc5bafec5280bbcb0c0693ff69fdaf", + "revisionTime": "2016-08-17T21:47:06Z" + }, + { + "checksumSHA1": "JH8wmQ8cWdn7mYu1T7gJ3IMIrec=", "path": "github.com/hashicorp/vault/api", - "revision": "fbecd94926e289d3b81d8dae6136452a6c4c93f6", - "revisionTime": "2016-08-13T15:54:01Z" + "revision": "fece3ca069fc5bafec5280bbcb0c0693ff69fdaf", + "revisionTime": "2016-08-17T21:47:06Z" }, { "checksumSHA1": "5lR6EdY0ARRdKAq3hZcL38STD8Q=",