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

feat: Add new Kubernetes Secret backend #521

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,73 @@ type: Opaque
data:
password: <path:secret-id#key | base64encode>
```

### 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: <key>
```

###### 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: <key>
```

###### Inline Path

```yaml
kind: Secret
apiVersion: v1
metadata:
name: test-secret
type: Opaque
data:
password: <path:my-secret#key>
```

###### Inline Path With Namespace

```yaml
kind: Secret
apiVersion: v1
metadata:
name: test-secret
type: Opaque
data:
password: <path:prod:my-secret#key>
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,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
Expand Down
60 changes: 60 additions & 0 deletions pkg/backends/kubernetessecret.go
Original file line number Diff line number Diff line change
@@ -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
}
110 changes: 110 additions & 0 deletions pkg/backends/kubernetessecret_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
5 changes: 5 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var backendPrefixes []string = []string{
"google",
"sops",
"op_connect",
"k8s_secret",
}

// New returns a new Config struct
Expand Down Expand Up @@ -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))
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 16 additions & 4 deletions pkg/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
DelineaSecretServerbackend = "delineasecretserver"
OnePasswordConnect = "1passwordconnect"
KeeperSecretsManagerBackend = "keepersecretsmanager"
KubernetesSecretBackend = "kubernetessecret"
K8sAuth = "k8s"
ApproleAuth = "approle"
GithubAuth = "github"
Expand Down