Skip to content

Commit

Permalink
feat: Add Kubernetes Secret backend
Browse files Browse the repository at this point in the history
Signed-off-by: Dennis Lapchenko <[email protected]>
  • Loading branch information
dennislapchenko committed Jun 21, 2023
1 parent 2f3a187 commit aeeedf1
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 5 deletions.
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 @@ -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
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

0 comments on commit aeeedf1

Please sign in to comment.