Skip to content

Commit 36aacf1

Browse files
Merge pull request #14 from vimeo/add-backend
Add Vault EngineType
2 parents 9bb2bfa + bffcaea commit 36aacf1

File tree

10 files changed

+495
-261
lines changed

10 files changed

+495
-261
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ vault:
1616
url: <url to vault>
1717
authType: # "token" or "gcp-default"
1818
token: <token value> # if authType == "token" is provided
19+
defaultEngineType: # "kv" or "kv-v2" (currently supported)
1920
role: "vault role" # if left empty, queries the GCP metadata service
2021
tls: # optional [tls options](https://godoc.org/github.com/hashicorp/vault/api#TLSConfig)
2122
namespace: <kubernetes namespace for created secrets>
@@ -24,13 +25,59 @@ mappings:
2425
# mappings from vault paths to kubernetes secret names
2526
- vaultPath: secret/data/vault-path
2627
secretName: k8s-secretname
28+
vaultEngineType: # optionally "kv" or "kv-v2" to override the defaultEngineType specified above
2729
```
2830
2931
### Labels and Reconciliation
3032
By default, Pentagon will add a [metadata label](https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta) with the key `pentagon` and the value `default`. At the least, this helps identify Pentagon as the creator and maintainer of the secret.
3133

3234
If you set the `label` configuration parameter, you can control the value of the label, allowing multiple Pentagon instances to exist without stepping on each other. Setting a non-default `label` also enables reconciliation which will cleanup any secrets that were created by Pentagon with a matching label, but are no longer present in the `mappings` configuration. This provides a simple way to ensure that old secret data does not remain present in your system after its time has passed.
3335

36+
### About Vault Engine Types
37+
Apparently, different Vault secrets engines have slightly different APIs for returning data. For instance, here is the response for version 1 of the key/value store:
38+
39+
```json
40+
{
41+
"request_id": "12a0c057-f475-4bbd-6305-e4c07e66805c",
42+
"lease_id": "",
43+
"renewable": false,
44+
"lease_duration": 2764800,
45+
"data": {
46+
"foo": "world"
47+
},
48+
"wrap_info": null,
49+
"warnings": null,
50+
"auth": null
51+
}
52+
```
53+
54+
Notice that the `data` object has the `foo` key embedded directly. Alternatively, here is the response for version 2 of the key/value store:
55+
56+
```json
57+
{
58+
"request_id": "78b921ae-79a8-d7e3-da16-336b634fff22",
59+
"lease_id": "",
60+
"renewable": false,
61+
"lease_duration": 0,
62+
"data": {
63+
"data": {
64+
"foo": "world"
65+
},
66+
"metadata": {
67+
"created_time": "2019-10-01T19:36:25.285387Z",
68+
"deletion_time": "",
69+
"destroyed": false,
70+
"version": 1
71+
}
72+
},
73+
"wrap_info": null,
74+
"warnings": null,
75+
"auth": null
76+
}
77+
```
78+
79+
Notice the extra `data` element nested inside the outer `data`. Vault secrets engines can be mounted at arbitrary paths and it does not appear to be possible to reliably detect which engine was used in the API response directly. In order to properly unwrap the secret data,indicate either `kv` or `kv-v2` as the `vaultEngineType` in the configuration. In the common case of using only one secrets engine, simply define the `defaultEngineType` in the `vault` configuration block and the mapping-level `vaultEngineType` will inherit the default. For compatibility, the unset default value defaults to `kv`. Note that this differs from the current default that Vault itself uses for the key/value secrets engine.
80+
3481
## Return Values
3582
The application will return 0 on success (when all keys were copied/updated successfully). A complete list of all possible return values follows:
3683

@@ -70,7 +117,7 @@ spec:
70117
restartPolicy: OnFailure
71118
containers:
72119
- name: pentagon
73-
image: vimeo/pentagon:v1.0.0
120+
image: vimeo/pentagon:v1.1.0
74121
args: ["/etc/pentagon/pentagon.yaml"]
75122
imagePullPolicy: Always
76123
resources:

config.go

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/hashicorp/vault/api"
7+
"github.com/vimeo/pentagon/vault"
78
)
89

910
// DefaultNamespace is the default kubernetes namespace.
@@ -13,19 +14,6 @@ const DefaultNamespace = "default"
1314
// created by pentagon.
1415
const DefaultLabelValue = "default"
1516

16-
// VaultAuthType is a custom type to represent different Vault authentication
17-
// methods.
18-
type VaultAuthType string
19-
20-
// VaultAuthTypeToken expects the Token property to be set on the VaultConfig
21-
// struct with a token to use.
22-
const VaultAuthTypeToken VaultAuthType = "token"
23-
24-
// VaultAuthTypeGCPDefault expects the Role property of the VaultConfig struct
25-
// to be populated with the role that vault expects and will use the machine's
26-
// default service account, running within GCP.
27-
const VaultAuthTypeGCPDefault VaultAuthType = "gcp-default"
28-
2917
// Config describes the configuration for vaultofsecrets
3018
type Config struct {
3119
// VaultURL is the URL used to connect to vault.
@@ -52,6 +40,19 @@ func (c *Config) SetDefaults() {
5240
if c.Label == "" {
5341
c.Label = DefaultLabelValue
5442
}
43+
44+
// default to engine type key/value v1 for backward compatibility
45+
if c.Vault.DefaultEngineType == "" {
46+
c.Vault.DefaultEngineType = vault.EngineTypeKeyValueV1
47+
}
48+
49+
// set all the underlying mapping engine types to their default
50+
// if unspecified
51+
for _, m := range c.Mappings {
52+
if m.VaultEngineType == "" {
53+
m.VaultEngineType = c.Vault.DefaultEngineType
54+
}
55+
}
5556
}
5657

5758
// Validate checks to make sure that the configuration is valid.
@@ -69,7 +70,13 @@ type VaultConfig struct {
6970
URL string `yaml:"url"`
7071

7172
// AuthType can be "token" or "gcp-default".
72-
AuthType VaultAuthType `yaml:"authType"`
73+
AuthType vault.AuthType `yaml:"authType"`
74+
75+
// DefaultEngineType is the type of secrets engine used because the API
76+
// responses may differ based on the engine used. In particular, K/V v2
77+
// has an extra layer of data wrapping that differs from v1.
78+
// Allowed values are "kv" and "kv-v2".
79+
DefaultEngineType vault.EngineType `yaml:"defaultEngineType"`
7380

7481
// Role is the role used when authenticating with vault. If this is unset
7582
// the role will be discovered by querying the GCP metadata service for
@@ -94,4 +101,9 @@ type Mapping struct {
94101
// be written to. Note that this must be a DNS-1123-compatible name and
95102
// match the regex [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
96103
SecretName string `yaml:"secretName"`
104+
105+
// VaultEngineType is the type of secrets engine mounted at the path of this
106+
// Vault secret. This specifically overrides the DefaultEngineType
107+
// specified in VaultConfig.
108+
VaultEngineType vault.EngineType `yaml:"vaultEngineType"`
97109
}

config_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package pentagon
22

3-
import "testing"
3+
import (
4+
"testing"
5+
6+
"github.com/vimeo/pentagon/vault"
7+
)
48

59
func TestSetDefaults(t *testing.T) {
610
c := &Config{
711
Vault: VaultConfig{
8-
AuthType: VaultAuthTypeToken,
12+
AuthType: vault.AuthTypeToken,
913
},
1014
}
1115

@@ -17,6 +21,16 @@ func TestSetDefaults(t *testing.T) {
1721
if c.Namespace != DefaultNamespace {
1822
t.Fatalf("namespace should be %s, is %s", DefaultNamespace, c.Namespace)
1923
}
24+
25+
if c.Vault.DefaultEngineType != vault.EngineTypeKeyValueV1 {
26+
t.Fatalf("unexpected default engine type: %s", c.Vault.DefaultEngineType)
27+
}
28+
29+
for _, m := range c.Mappings {
30+
if m.VaultEngineType == "" {
31+
t.Fatalf("empty vault engine type for mapping: %+v", m)
32+
}
33+
}
2034
}
2135

2236
func TestNoClobber(t *testing.T) {
@@ -56,5 +70,4 @@ func TestValidate(t *testing.T) {
5670
if err != nil {
5771
t.Fatalf("configuration should have been valid: %s", err)
5872
}
59-
6073
}

pentagon/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"k8s.io/client-go/rest"
1616

1717
"github.com/vimeo/pentagon"
18+
"github.com/vimeo/pentagon/vault"
1819
)
1920

2021
func main() {
@@ -104,9 +105,9 @@ func getVaultClient(vaultConfig pentagon.VaultConfig) (*api.Client, error) {
104105
}
105106

106107
switch vaultConfig.AuthType {
107-
case pentagon.VaultAuthTypeToken:
108+
case vault.AuthTypeToken:
108109
client.SetToken(vaultConfig.Token)
109-
case pentagon.VaultAuthTypeGCPDefault:
110+
case vault.AuthTypeGCPDefault:
110111
// default to using configured Role
111112
role := vaultConfig.Role
112113

pentagon/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const k8sNamespace = "default"
2626
const configMapName = "pentagon-config"
2727

2828
type integrationTests struct {
29-
vaultInstance vault
29+
vaultInstance vaultHelper
3030
pj pentagonJob
3131
client *kubernetes.Clientset
3232
}
@@ -85,7 +85,7 @@ func TestIntegration(t *testing.T) {
8585
t.Fatalf("unable to delete existing jobs: %s", err)
8686
}
8787

88-
vaultInstance := vault{
88+
vaultInstance := vaultHelper{
8989
client: clientset,
9090
restConfig: restConfig,
9191
namespace: k8sNamespace,

pentagon/vault_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ import (
1616
)
1717

1818
// helper struct for working with vault instance in k8s
19-
type vault struct {
19+
type vaultHelper struct {
2020
client *kubernetes.Clientset
2121
restConfig *restclient.Config // this was used to create the client above!
2222
namespace string
2323
pod *v1.Pod
2424
}
2525

2626
// sets a secret by exec'ing into the pod and running the command line client.
27-
func (v *vault) setSecret(path string, values map[string]string) error {
27+
func (v *vaultHelper) setSecret(path string, values map[string]string) error {
2828
valuePairs := []string{}
2929
for k, v := range values {
3030
valuePairs = append(valuePairs, fmt.Sprintf("%s=%s", k, v))
@@ -69,12 +69,12 @@ func (v *vault) setSecret(path string, values map[string]string) error {
6969
}
7070

7171
// returns the url to the pod.
72-
func (v *vault) url() string {
72+
func (v *vaultHelper) url() string {
7373
return fmt.Sprintf("http://%s:%d", v.pod.Status.PodIP, vaultPort)
7474
}
7575

7676
// create a vault instance in k8s.
77-
func (v *vault) create(wait time.Duration) error {
77+
func (v *vaultHelper) create(wait time.Duration) error {
7878
// see if it exists first
7979
pods := v.client.CoreV1().Pods(v.namespace)
8080
p, err := pods.Get("vault", metav1.GetOptions{})

reflector.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,28 @@ func (r *Reflector) Reflect(mappings []Mapping) error {
8080
return fmt.Errorf("secret %s not found", mapping.VaultPath)
8181
}
8282

83+
var k8sSecretData map[string][]byte
84+
8385
// convert map[string]interface{} to map[string][]byte
84-
k8sSecretData, err := r.castData(secretData.Data)
85-
if err != nil {
86-
return fmt.Errorf("error casting data: %s", err)
86+
switch mapping.VaultEngineType {
87+
case vault.EngineTypeKeyValueV1:
88+
k8sSecretData, err = r.castData(secretData.Data)
89+
if err != nil {
90+
return fmt.Errorf("error casting data: %s", err)
91+
}
92+
case vault.EngineTypeKeyValueV2:
93+
// there's an extra level of wrapping with the v2 kv secrets engine
94+
if unwrapped, ok := secretData.Data["data"].(map[string]interface{}); ok {
95+
k8sSecretData, err = r.castData(unwrapped)
96+
} else {
97+
return fmt.Errorf("key/value v2 interface did not have " +
98+
"expected extra wrapping")
99+
}
100+
default:
101+
return fmt.Errorf(
102+
"unknown vault engine type: %q",
103+
mapping.VaultEngineType,
104+
)
87105
}
88106

89107
// create the new Secret

0 commit comments

Comments
 (0)