Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix panic with Vault KV 2 secrets #186

Merged
merged 6 commits into from
Nov 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 40 additions & 2 deletions runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import (
"github.com/pkg/errors"
)

// Regexp for invalid characters in keys
// InvalidRegexp is a regexp for invalid characters in keys
var InvalidRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]`)

// Runner executes a given child process with configuration
type Runner struct {
// ErrCh and DoneCh are channels where errors and finish notifications occur.
ErrCh chan error
Expand Down Expand Up @@ -195,7 +196,7 @@ func (r *Runner) Stop() {
r.stopChild()

if err := r.deletePid(); err != nil {
log.Printf("[WARN] (runner) could not remove pid at %q: %s",
log.Printf("[WARN] (runner) could not remove pid at %#v: %s",
r.config.PidFile, err)
}

Expand Down Expand Up @@ -418,6 +419,43 @@ func (r *Runner) appendSecrets(
cp := r.configPrefixMap[d.String()]

for key, value := range typed.Data {
// check for presence of "metadata", indicating this value came from Vault
// kv version 2, and we should then use the sub map instead
if key == "metadata" {
continue
}

// Vault Secrets KV1 and KV2 return different formats. Here we check the key
// value, and if we've found another key called "data" that is of type
// map[string]interface, we assume it's KV2 and use the key/value pair from
// it, otherwise we assume it's KV1
//
// value here in KV1 format is a simple string.
// value in KV2 format is a map:
// map[string]interface {}{
// "key": "value",
// }
if key == "data" {
switch value.(type) {
case map[string]interface{}:
log.Printf("[DEBUG] Found KV2 secret")
mapV := value.(map[string]interface{})

// assumes this map is only 1 element long, we change the key and value
// to match the sub element
for k, v := range mapV {
key = k
value = v
catsby marked this conversation as resolved.
Show resolved Hide resolved
break
}
default:
// Only handle the sub data map, do nothing otherwise.
// If the secret has been deleted but not destroyed, Vault will return a
// response with a nil sub data map, and will not be handled by the
// above block
}
}

// Ignore any keys that are empty (not sure if this is even possible in
// Vault, but I play defense).
if strings.TrimSpace(key) == "" {
Expand Down
105 changes: 105 additions & 0 deletions runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/consul-template/config"
"github.com/hashicorp/consul-template/dependency"
)

func TestRunner_appendSecrets(t *testing.T) {
t.Parallel()

secretValue := "somevalue"

cases := map[string]struct {
path string
data *dependency.Secret
notFound bool
}{
"kv1_secret": {
"kv/bar",
&dependency.Secret{
Data: map[string]interface{}{
"key_field": secretValue,
},
},
false,
},
"kv2_secret": {
"secret/data/foo",
&dependency.Secret{
Data: map[string]interface{}{
"metadata": map[string]interface{}{
"destroyed": bool(false),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be checking this field during the parsing above? I couldn't find a description in the vault docs of whether the secret would still be populated when this field is true.

Copy link
Member Author

@catsby catsby Nov 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the secret is deleted or destroyed, it wont be returned. If it's delete, you will get a response with metadata but data will be nil:

[vault][master](4)$ curl -s --header "X-Vault-Token: $VAULT_TOKEN" http://127.0.0.1:8200/v1/kv/data/something 
{
  "request_id": "6e926af4-37a9-2e17-679f-e9d7c7553485",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "data": null,
    "metadata": {
      "created_time": "2018-11-14T22:39:13.348641Z",
      "deletion_time": "2018-11-14T22:43:55.199587Z",
      "destroyed": false,
      "version": 2
    }
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

In this case, we enter the default: case and no secret is populated (https://github.com/hashicorp/envconsul/pull/186/files#diff-a0afb4d13dc79c28cad59d370ba902caR451)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kyhavlov I added / edited the tests in 3884fc8 , I believe that covers it but please take another look when you can

"version": "1",
},
"data": map[string]interface{}{
"key_field": secretValue,
},
},
},
false,
},
"kv2_secret_destroyed": {
"secret/data/foo",
&dependency.Secret{
Data: map[string]interface{}{
"metadata": map[string]interface{}{
"destroyed": bool(true),
"version": "2",
},
"data": nil,
},
},
true,
},
}

for name, tc := range cases {
t.Run(fmt.Sprintf("%s", name), func(t *testing.T) {
cfg := Config{
Secrets: &PrefixConfigs{
&PrefixConfig{
Path: config.String(tc.path),
},
},
}
c := DefaultConfig().Merge(&cfg)
r, err := NewRunner(c, true)
if err != nil {
t.Fatal(err)
}
vrq, err := dependency.NewVaultReadQuery(tc.path)
if err != nil {
t.Fatal(err)
}
env := make(map[string]string)
appendError := r.appendSecrets(env, vrq, tc.data)
if appendError != nil {
t.Fatalf("got err: %s", appendError)
}

if len(env) > 1 {
t.Fatalf("Expected only 1 value in this test")
}

keyName := tc.path + "_key_field"
keyName = strings.Replace(keyName, "/", "_", -1)

var value string
value, ok := env[keyName]
if !ok && !tc.notFound {
t.Fatalf("expected (%s) key, but was not found", keyName)
}
if ok && tc.notFound {
t.Fatalf("expected to not find key, but (%s) was found", keyName)
}
if ok && value != secretValue {
t.Fatalf("values didn't match, expected (%s), got (%s)", secretValue, value)
}
})
}
}