From aeeedf1d40313e9242b55d7401c2b40d2fa659e5 Mon Sep 17 00:00:00 2001 From: Dennis Lapchenko Date: Wed, 21 Jun 2023 10:55:14 +0300 Subject: [PATCH] feat: Add Kubernetes Secret backend Signed-off-by: Dennis Lapchenko --- docs/backends.md | 70 ++++++++++++++++ go.mod | 2 +- pkg/backends/kubernetessecret.go | 60 ++++++++++++++ pkg/backends/kubernetessecret_test.go | 110 ++++++++++++++++++++++++++ pkg/config/config.go | 5 ++ pkg/config/config_test.go | 6 ++ pkg/kube/client.go | 20 ++++- pkg/types/constants.go | 1 + 8 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 pkg/backends/kubernetessecret.go create mode 100644 pkg/backends/kubernetessecret_test.go diff --git a/docs/backends.md b/docs/backends.md index df6b8446..6b2791e8 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -705,3 +705,73 @@ type: Opaque data: password: ``` + +### Kubernetes Secret + +Inject values from any kubernetes secret + +**Note**: The Kubernetes Secret backend does not support versioning + +##### Kubernetes Secret Authentication + +Backend inherits same in-cluster service-account as the plugin itself + +These are the parameters for Kubernetes Secret: + +``` +AVP_TYPE: kubernetessecret +``` + +##### Examples + +###### Path Annotation + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "my-secret" +type: Opaque +data: + password: +``` + +###### Path Annotation With Namespace + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "prod:my-secret" +type: Opaque +data: + password: +``` + +###### Inline Path + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret +type: Opaque +data: + password: +``` + +###### Inline Path With Namespace + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret +type: Opaque +data: + password: +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 3ef12e16..5224eac1 100644 --- a/go.mod +++ b/go.mod @@ -212,7 +212,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pires/go-proxyproto v0.6.1 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/posener/complete v1.2.3 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect diff --git a/pkg/backends/kubernetessecret.go b/pkg/backends/kubernetessecret.go new file mode 100644 index 00000000..fadb1fe7 --- /dev/null +++ b/pkg/backends/kubernetessecret.go @@ -0,0 +1,60 @@ +package backends + +import ( + "github.com/argoproj-labs/argocd-vault-plugin/pkg/kube" + "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" + "github.com/pkg/errors" +) + +type kubeSecretsClient interface { + ReadSecretData(string) (map[string][]byte, error) +} + +// KubernetesSecret is a struct for working with a Kubernetes Secret backend +type KubernetesSecret struct { + client kubeSecretsClient +} + +// NewKubernetesSecret returns a new Kubernetes Secret backend. +func NewKubernetesSecret() *KubernetesSecret { + return &KubernetesSecret{} +} + +// Login initiates kubernetes client +func (k *KubernetesSecret) Login() error { + localClient, err := kube.NewClient() + if err != nil { + return errors.Wrap(err, "Failed to perform login for kubernetes secret backend") + } + k.client = localClient + return nil +} + +// GetSecrets gets secrets from Kubernetes Secret and returns the formatted data +func (k *KubernetesSecret) GetSecrets(path string, version string, annotations map[string]string) (map[string]interface{}, error) { + utils.VerboseToStdErr("K8s Secret getting secret: %s", path) + data, err := k.client.ReadSecretData(path) + if err != nil { + return nil, err + } + + out := make(map[string]interface{}, len(data)) + for k, v := range data { + out[k] = string(v) + } + + utils.VerboseToStdErr("K8s Secret get secret response: %v", out) + return out, nil +} + +// GetIndividualSecret will get the specific secret (placeholder) from the Kubernetes Secret backend +// Kubernetes Secrets can only be wholly read, +// So, we use GetSecrets and extract the specific placeholder we want +func (k *KubernetesSecret) GetIndividualSecret(path, secret, version string, annotations map[string]string) (interface{}, error) { + utils.VerboseToStdErr("K8s Secret getting secret %s and key %s", path, secret) + data, err := k.GetSecrets(path, version, annotations) + if err != nil { + return nil, err + } + return data[secret], nil +} diff --git a/pkg/backends/kubernetessecret_test.go b/pkg/backends/kubernetessecret_test.go new file mode 100644 index 00000000..c74ba732 --- /dev/null +++ b/pkg/backends/kubernetessecret_test.go @@ -0,0 +1,110 @@ +package backends + +import ( + "reflect" + "testing" +) + +func newMockK8sClient(vals map[string]map[string]string, err error) *mockK8sClient { + encoded := make(map[string]map[string][]byte) + for path, secrets := range vals { + encoded[path] = make(map[string][]byte) + for key, value := range secrets { + encoded[path][key] = []byte(value) + } + } + return &mockK8sClient{ + responses_by_path: encoded, + err: err, + } +} + +type mockK8sClient struct { + responses_by_path map[string]map[string][]byte + err error +} + +func (m *mockK8sClient) ReadSecretData(path string) (map[string][]byte, error) { + return m.responses_by_path[path], m.err +} + +func TestKubernetesSecretGetSecrets(t *testing.T) { + sm := NewKubernetesSecret() + sm.client = newMockK8sClient(map[string]map[string]string{ + "secret1": { + "test-secret": "current-value", + "test2": "bar", + }, + "secret2": { + "key": "foz", + }, + }, nil) + + t.Run("Get secrets from first path", func(t *testing.T) { + data, err := sm.GetSecrets("secret1", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "test-secret": "current-value", + "test2": "bar", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("GetIndividualSecret from first path", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("secret1", "test2", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "bar" + + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + + t.Run("Get secrets from secret from second path", func(t *testing.T) { + data, err := sm.GetSecrets("secret2", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "key": "foz", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("GetIndividualSecret from inline path secret", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("secret2", "key", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "foz" + + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + + t.Run("GetIndividualSecretNotFound", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("test", "22test2", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + if secret != nil { + t.Errorf("expected: %s, got: %s.", "nil", secret) + } + }) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 2c53e557..b5c32964 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -49,6 +49,7 @@ var backendPrefixes []string = []string{ "google", "sops", "op_connect", + "k8s_secret", } // New returns a new Config struct @@ -278,6 +279,10 @@ func New(v *viper.Viper, co *Options) (*Config, error) { } backend = backends.NewDelineaSecretServerBackend(tss) } + case types.KubernetesSecretBackend: + { + backend = backends.NewKubernetesSecret() + } default: return nil, fmt.Errorf("Must provide a supported Vault Type, received %s", v.GetString(types.EnvAvpType)) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ceff01c5..2031d127 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -219,6 +219,12 @@ fDGt+yaf3RaZbVwHSVLzxiXGsu1WQJde3uJeNh5c6z+5 }, "*backends.DelineaSecretServer", }, + { + map[string]interface{}{ + "AVP_TYPE": "kubernetessecret", + }, + "*backends.KubernetesSecret", + }, } for _, tc := range testCases { for k, v := range tc.environment { diff --git a/pkg/kube/client.go b/pkg/kube/client.go index f8b755c0..0c28382d 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -32,9 +32,9 @@ func NewClient() (*Client, error) { }, nil } -// ReadSecret reads the specified Secret from the defined namespace, otherwise defaults to `argocd` -// and returns a YAML []byte containing its data, decoded from base64 -func (c *Client) ReadSecret(name string) ([]byte, error) { +// ReadSecretData reads the specified Secret from the defined namespace, otherwise defaults to `argocd` +// and returns map[string][]byte containing its data +func (c *Client) ReadSecretData(name string) (map[string][]byte, error) { secretNamespace, secretName := secretNamespaceName(name) utils.VerboseToStdErr("parsed secret name as %s from namespace %s", secretName, secretNamespace) @@ -43,8 +43,20 @@ func (c *Client) ReadSecret(name string) ([]byte, error) { if err != nil { return nil, err } + + return s.Data, nil +} + +// ReadSecret reads the specified Secret from the defined namespace, otherwise defaults to `argocd` +// and returns a YAML []byte containing its data, decoded from base64 +func (c *Client) ReadSecret(name string) ([]byte, error) { + data, err := c.ReadSecretData(name) + if err != nil { + return nil, err + } + decoded := make(map[string]string) - for key, value := range s.Data { + for key, value := range data { decoded[key] = string(value) } res, err := k8yaml.Marshal(&decoded) diff --git a/pkg/types/constants.go b/pkg/types/constants.go index 9286b6f4..49abbdef 100644 --- a/pkg/types/constants.go +++ b/pkg/types/constants.go @@ -43,6 +43,7 @@ const ( DelineaSecretServerbackend = "delineasecretserver" OnePasswordConnect = "1passwordconnect" KeeperSecretsManagerBackend = "keepersecretsmanager" + KubernetesSecretBackend = "kubernetessecret" K8sAuth = "k8s" ApproleAuth = "approle" GithubAuth = "github"