Skip to content

Commit

Permalink
feat: Allow passing <path:..> as a placeholder (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
werne2j committed Apr 12, 2021
1 parent f6ce781 commit b5b2599
Show file tree
Hide file tree
Showing 30 changed files with 399 additions and 405 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ This plugin is aimed at helping to solve the issue of secret management with Git
### How it works
The argocd-vault-plugin works by taking a directory of yaml files that have been templated out using the pattern of `<placeholder>` where you would want a value from Vault to go. The inside of the `<>` would be the actual key in Vault.

An annotation or path prefix can be used to specify exactly where the plugin should look for the vault values. The annotation needs to be in the format `avp_path: "path/to/secret"`. The path prefix is defined as an Environment Variable `PATH_PREFIX` and when set will concatenate the prefix with the resource type to create a path that is something like `PATH_PREFIX/configmap`. (See [Configuration](#configuration))
An annotation can be used to specify exactly where the plugin should look for the vault values. The annotation needs to be in the format `avp_path: "path/to/secret"`.

For example, if you have a secret with the key `password-vault-key` that you would want to pull from vault, you might have a yaml that looks something like the below code. In this yaml, the plugin will pull the value of `path/to/secret/password-vault-key` and inject it into the secret yaml.

Expand Down Expand Up @@ -66,10 +66,36 @@ data:
password: cGFzc3dvcmQK # The Value from the key password-vault-key in vault
```

<b>*Note*</b>: The plugin does not perform any transformation of the secrets in transit. So if you have plain text secrets in Vault, you will need to use the `stringData` field and if you have a base64 encoded secret in Vault, you will need to use the `data` field according to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/).
The plugin also supports putting the path directly within the placeholder. The format must be `<path:path/to/secret#key>`, where `path/to/secret` is the vault path and the Vault key goes after the `#` symbol. Doing this does not require an `avp_path` annotation and will override any `avp_path` annotation that is set. For example:

```
kind: Secret
apiVersion: v1
metadata:
name: example-secret
type: Opaque
data:
password: <path:path/to/secret#password-vault-key>
```

<b>*Note*</b>: The plugin will attempt to read any strings that match the following PCRE regex: `<.*>` (any characters between matching angle brackets), in all YAML files at the path given as the `<path>` argument. If there are YAML files that use `<string>`'s for other purposes and should _not_ be replaced, you can tell AVP to skip that file by adding the annotation `avp_ignore: "true"`.

##### Modifiers
By Default the plugin does not perform any transformation of the secrets in transit. So if you have plain text secrets in Vault, you will need to use the `stringData` field and if you have a base64 encoded secret in Vault, you will need to use the `data` field according to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/).

However, as of now, we support one modifier. And that is `base64encode`. So if you have a plain text value in Vault and would like to Base64 encode it on the fly to inject into a Kubernetes secret you can do:

```
kind: Secret
apiVersion: v1
metadata:
name: example-secret
type: Opaque
data:
password: <path:path/to/secret#password-vault-key | base64encode>
```
And the plugin will pull the value from Vault, Base64 encode the value and then inject it into the placeholder.

## Installation
There are multiple ways to download and install argocd-vault-plugin depending on your use case.

Expand Down
24 changes: 14 additions & 10 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package cmd

import (
"fmt"
"strconv"

"github.com/IBM/argocd-vault-plugin/pkg/config"
"github.com/IBM/argocd-vault-plugin/pkg/kube"
"github.com/IBM/argocd-vault-plugin/pkg/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// NewGenerateCommand initializes the generate command
Expand Down Expand Up @@ -38,39 +40,41 @@ func NewGenerateCommand() *cobra.Command {
return fmt.Errorf("could not read YAML files: %s", errs)
}

config, err := config.New(&config.Options{
v := viper.New()
config, err := config.New(v, &config.Options{
SecretName: secretName,
ConfigPath: configPath,
})
if err != nil {
return err
}

backend := config.Backend

err = utils.CheckExistingToken(config.VaultClient)
if err != nil {
err = backend.Login()
err = config.Backend.Login()
if err != nil {
return err
}
}

for _, manifest := range manifests {

// skip empty manifests
if len(manifest) == 0 {
if len(manifest.Object) == 0 {
continue
}

template, err := kube.NewTemplate(manifest, backend, config.PathPrefix)
template, err := kube.NewTemplate(manifest, config.Backend)
if err != nil {
return err
}

err = template.Replace()
if err != nil {
return err
annotations := manifest.GetAnnotations()
avpIgnore, _ := strconv.ParseBool(annotations["avp_ignore"])
if !avpIgnore {
err = template.Replace()
if err != nil {
return err
}
}

output, err := template.ToYAML()
Expand Down
19 changes: 6 additions & 13 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8yaml "k8s.io/apimachinery/pkg/util/yaml"
)

Expand All @@ -27,7 +28,7 @@ func listYamlFiles(root string) ([]string, error) {
return files, nil
}

func readFilesAsManifests(paths []string) (result []map[string]interface{}, errs []error) {
func readFilesAsManifests(paths []string) (result []unstructured.Unstructured, errs []error) {

for _, path := range paths {
manifest, err := manifestFromYAML(path)
Expand All @@ -40,16 +41,17 @@ func readFilesAsManifests(paths []string) (result []map[string]interface{}, errs
return result, errs
}

func manifestFromYAML(path string) ([]map[string]interface{}, error) {
func manifestFromYAML(path string) ([]unstructured.Unstructured, error) {
rawdata, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read YAML: %s from disk: %s", path, err)
}

decoder := k8yaml.NewYAMLToJSONDecoder(bytes.NewReader(rawdata))
var manifests []map[string]interface{}

var manifests []unstructured.Unstructured
for {
nxtManifest := make(map[string]interface{})
nxtManifest := unstructured.Unstructured{}
err := decoder.Decode(&nxtManifest)
if err != nil {
if err == io.EOF {
Expand All @@ -62,12 +64,3 @@ func manifestFromYAML(path string) ([]map[string]interface{}, error) {

return manifests, nil
}

func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
12 changes: 6 additions & 6 deletions fixtures/input/nonempty/excluded.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
# Source: conditionally/excluded/secret.yaml
#
#
#
---
---
# Source: conditionally/excluded/secret.yaml
#
#
#
---
10 changes: 10 additions & 0 deletions fixtures/input/nonempty/secret_path.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
annotations:
kv_version: "1"
name: example-secret
namespace: default
type: Opaque
data:
SECRET_VAR: <path:secret/testing#secret-var-value>
11 changes: 11 additions & 0 deletions fixtures/output/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,14 @@ metadata:
namespace: test-namespace
type: Opaque
---
apiVersion: v1
data:
SECRET_VAR: dGVzdC1wYXNzd29yZA==
kind: Secret
metadata:
annotations:
kv_version: "1"
name: example-secret
namespace: default
type: Opaque
---
2 changes: 2 additions & 0 deletions fixtures/output/small-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ data:
MY_NONSECRET_STRING: foo
kind: ConfigMap
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
2 changes: 2 additions & 0 deletions fixtures/output/small-cronjob.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
annotations:
avp_path: path
name: my-app
spec:
jobTemplate:
Expand Down
2 changes: 2 additions & 0 deletions fixtures/output/small-deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
spec:
Expand Down
2 changes: 2 additions & 0 deletions fixtures/output/small-ingress.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
spec:
Expand Down
2 changes: 2 additions & 0 deletions fixtures/output/small-job.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: batch/v1
kind: Job
metadata:
annotations:
avp_path: path
name: my-app
spec:
template:
Expand Down
3 changes: 3 additions & 0 deletions fixtures/output/small-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ data:
MY_SECRET_STRING: Zm9v
kind: Secret
metadata:
annotations:
avp_path: path
kv_version: "1"
name: my-app
namespace: default
2 changes: 2 additions & 0 deletions fixtures/output/small-secret2.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
stringData:
Expand Down
2 changes: 2 additions & 0 deletions fixtures/output/small-secret3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ data:
MY_SECRET_STRING: Zm9v
kind: Secret
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
2 changes: 2 additions & 0 deletions fixtures/output/small-secret4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ data:
MY_LEAKED_SECRET: cGFzc3dvcmQ=
kind: Secret
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
2 changes: 2 additions & 0 deletions fixtures/output/small-service.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: v1
kind: Service
metadata:
annotations:
avp_path: path
name: my-app
namespace: default
spec:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/hashicorp/vault/api v1.0.5-0.20201001211907-38d91b749c77
github.com/hashicorp/vault/sdk v0.1.14-0.20201109203410-5e6e24692b32
github.com/pierrec/lz4 v2.6.0+incompatible // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/viper v1.7.0
golang.org/x/crypto v0.0.0-20201116153603-4be66e5b6582 // indirect
Expand Down
2 changes: 1 addition & 1 deletion pkg/backends/ibmsecretmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (i *IBMSecretManager) Login() error {
}

// GetSecrets gets secrets from IBM Secret Manager and returns the formatted data
func (i *IBMSecretManager) GetSecrets(path, _ string) (map[string]interface{}, error) {
func (i *IBMSecretManager) GetSecrets(path string, _ map[string]string) (map[string]interface{}, error) {
secret, err := i.VaultClient.Logical().Read(path)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions pkg/backends/ibmsecretmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestSecretManagerGetSecrets(t *testing.T) {
"secret2": "value2",
}

data, err := sm.GetSecrets("secret/ibm/arbitrary/groups/1", "")
data, err := sm.GetSecrets("secret/ibm/arbitrary/groups/1", map[string]string{})
if err != nil {
t.Fatalf("expected 0 errors but got: %s", err)
}
Expand All @@ -42,7 +42,7 @@ func TestSecretManagerGetSecretsFail(t *testing.T) {
VaultClient: client,
}

_, err := sm.GetSecrets("secret/ibm/arbitrary/groups/3", "")
_, err := sm.GetSecrets("secret/ibm/arbitrary/groups/3", map[string]string{})

expected := fmt.Sprintf("Could not find secrets at path %s", "secret/ibm/arbitrary/groups/3")

Expand Down
11 changes: 6 additions & 5 deletions pkg/backends/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (v *Vault) Login() error {
}

// GetSecrets gets secrets from vault and returns the formatted data
func (v *Vault) GetSecrets(path, kvVersion string) (map[string]interface{}, error) {
func (v *Vault) GetSecrets(path string, annotations map[string]string) (map[string]interface{}, error) {
secret, err := v.VaultClient.Logical().Read(path)
if err != nil {
return nil, err
Expand All @@ -46,11 +46,12 @@ func (v *Vault) GetSecrets(path, kvVersion string) (map[string]interface{}, erro
return nil, fmt.Errorf("Could not find secrets at path %s", path)
}

if kvVersion != "" {
v.KvVersion = kvVersion
var kvVersion = v.KvVersion
if kv, ok := annotations["kv_version"]; ok {
kvVersion = kv
}

if v.KvVersion == "2" {
if kvVersion == "2" {
if _, ok := secret.Data["data"]; ok {
return secret.Data["data"].(map[string]interface{}), nil
}
Expand All @@ -60,7 +61,7 @@ func (v *Vault) GetSecrets(path, kvVersion string) (map[string]interface{}, erro
return nil, errors.New("Could not get data from Vault, check that kv-v2 is the correct engine")
}

if v.KvVersion == "1" {
if kvVersion == "1" {
return secret.Data, nil
}

Expand Down
Loading

0 comments on commit b5b2599

Please sign in to comment.