Skip to content

Commit

Permalink
feat: Add new Kubernetes Secret backend
Browse files Browse the repository at this point in the history
Signed-off-by: Dennis Lapchenko <dennis.lapchenko@gmail.com>
  • Loading branch information
dennislapchenko committed Jun 20, 2023
1 parent 2f3a187 commit 98b7116
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 31 deletions.
56 changes: 56 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,59 @@ type: Opaque
data:
password: <path:secret-id#key | base64encode>
```

### Kubernets Secret

**Note**: The Kubernetes secret backend does not support versioning

##### Kubernetes Secret Authentication

Refer to the "Configuring Plugin" section, as this backend uses same in-cluster service-account as the plugin itself.

These are the parameters for Kubernetes Secret:

```
AVP_TYPE: kubernetessecret
AVP_K8S_SECRET_SECRET: avp-secret-data
```

##### Examples

###### Inline Path Reading from configured secret

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

###### Path Annotation

```yaml
kind: Secret
apiVersion: v1
metadata:
name: test-secret
annotations:
avp.kubernetes.io/path: "custom-secret-data"
type: Opaque
data:
password: <key>
```

###### Inline Path

```yaml
kind: Secret
apiVersion: v1
metadata:
name: test-secret
type: Opaque
data:
password: <path:custom-secret-data#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
70 changes: 70 additions & 0 deletions pkg/backends/kubernetessecret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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"
)

var useDefaultSecret = func(s string) bool { return s == "default" || s == "~" }

type kubeSecretsClient interface {
ReadSecretData(string) (map[string][]byte, error)
}

// KubernetesSecret is a struct for working with a Kubernetes Secret backend
type KubernetesSecret struct {
secretName string
client kubeSecretsClient
}

// NewKubernetesSecret returns a new Kubernetes Secret backend.
func NewKubernetesSecret(secretName string) *KubernetesSecret {
return &KubernetesSecret{
secretName: secretName,
}
}

// 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) {
secretName := path
if useDefaultSecret(path) {
secretName = k.secretName
}
utils.VerboseToStdErr("K8s Secret getting secret: %s", secretName)

data, err := k.client.ReadSecretData(secretName)
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(kvpath, secret, version string, annotations map[string]string) (interface{}, error) {
utils.VerboseToStdErr("K8s Secret getting secret %s and key %s", kvpath, secret)
data, err := k.GetSecrets(kvpath, version, annotations)
if err != nil {
return nil, err
}
return data[secret], nil
}
75 changes: 75 additions & 0 deletions pkg/backends/kubernetessecret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package backends

import (
"reflect"
"testing"
)

func newMockK8sClient(vals map[string]string, err error) *mockK8sClient {
encoded := make(map[string][]byte)
for k, v := range vals {
encoded[k] = []byte(v)
}
return &mockK8sClient{
response: encoded,
err: err,
}
}

type mockK8sClient struct {
response map[string][]byte
err error
}

func (m *mockK8sClient) ReadSecretData(path string) (map[string][]byte, error) {
return m.response, m.err
}

func TestKubernetesSecretGetSecrets(t *testing.T) {
sm := NewKubernetesSecret("test")
sm.client = newMockK8sClient(map[string]string{
"test-secret": "current-value",
"test2": "bar",
}, nil)

t.Run("Get secrets", func(t *testing.T) {

data, err := sm.GetSecrets("test", "", 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", func(t *testing.T) {
secret, err := sm.GetIndividualSecret("test", "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("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)
}
})
}
9 changes: 9 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,14 @@ func New(v *viper.Viper, co *Options) (*Config, error) {
}
backend = backends.NewDelineaSecretServerBackend(tss)
}
case types.KubernetesSecretBackend:
{
if !v.IsSet(types.EnvKubernetesSecretSecret) {
return nil, fmt.Errorf("%s is required for Kubernetes Secret backend", types.EnvKubernetesSecretSecret)
}
secretName := v.GetString(types.EnvKubernetesSecretSecret)
backend = backends.NewKubernetesSecret(secretName)
}
default:
return nil, fmt.Errorf("Must provide a supported Vault Type, received %s", v.GetString(types.EnvAvpType))
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ fDGt+yaf3RaZbVwHSVLzxiXGsu1WQJde3uJeNh5c6z+5
},
"*backends.DelineaSecretServer",
},
{
map[string]interface{}{
"AVP_TYPE": "kubernetessecret",
"AVP_K8S_SECRET_SECRET": "argocd-vault-plugin-secret-data",
},
"*backends.KubernetesSecret",
},
}
for _, tc := range testCases {
for k, v := range tc.environment {
Expand Down Expand Up @@ -485,6 +492,12 @@ func TestNewConfigMissingParameter(t *testing.T) {
},
"*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
54 changes: 28 additions & 26 deletions pkg/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,33 @@ const (
EnvArgoCDPrefix = "ARGOCD_ENV"

// Environment Variable Constants
EnvAvpType = "AVP_TYPE"
EnvAvpRoleID = "AVP_ROLE_ID"
EnvAvpSecretID = "AVP_SECRET_ID"
EnvAvpAuthType = "AVP_AUTH_TYPE"
EnvAvpGithubToken = "AVP_GITHUB_TOKEN"
EnvAvpK8sRole = "AVP_K8S_ROLE"
EnvAvpK8sMountPath = "AVP_K8S_MOUNT_PATH"
EnvAvpMountPath = "AVP_MOUNT_PATH"
EnvAvpK8sTokenPath = "AVP_K8S_TOKEN_PATH"
EnvAvpIBMAPIKey = "AVP_IBM_API_KEY"
EnvAvpIBMInstanceURL = "AVP_IBM_INSTANCE_URL"
EnvAvpKvVersion = "AVP_KV_VERSION"
EnvAvpPathPrefix = "AVP_PATH_PREFIX"
EnvAWSRegion = "AWS_REGION"
EnvVaultAddress = "VAULT_ADDR"
EnvYCLKeyID = "AVP_YCL_KEY_ID"
EnvYCLServiceAccountID = "AVP_YCL_SERVICE_ACCOUNT_ID"
EnvYCLPrivateKey = "AVP_YCL_PRIVATE_KEY"
EnvAvpUsername = "AVP_USERNAME"
EnvAvpPassword = "AVP_PASSWORD"
EnvPathValidation = "AVP_PATH_VALIDATION"
EnvAvpKSMConfigPath = "AVP_KEEPER_CONFIG_PATH"
EnvAvpDelineaURL = "AVP_DELINEA_URL"
EnvAvpDelineaUser = "AVP_DELINEA_USER"
EnvAvpDelineaPassword = "AVP_DELINEA_PASSWORD"
EnvAvpDelineaDomain = "AVP_DELINEA_DOMAIN"
EnvAvpType = "AVP_TYPE"
EnvAvpRoleID = "AVP_ROLE_ID"
EnvAvpSecretID = "AVP_SECRET_ID"
EnvAvpAuthType = "AVP_AUTH_TYPE"
EnvAvpGithubToken = "AVP_GITHUB_TOKEN"
EnvAvpK8sRole = "AVP_K8S_ROLE"
EnvAvpK8sMountPath = "AVP_K8S_MOUNT_PATH"
EnvAvpMountPath = "AVP_MOUNT_PATH"
EnvAvpK8sTokenPath = "AVP_K8S_TOKEN_PATH"
EnvAvpIBMAPIKey = "AVP_IBM_API_KEY"
EnvAvpIBMInstanceURL = "AVP_IBM_INSTANCE_URL"
EnvAvpKvVersion = "AVP_KV_VERSION"
EnvAvpPathPrefix = "AVP_PATH_PREFIX"
EnvAWSRegion = "AWS_REGION"
EnvVaultAddress = "VAULT_ADDR"
EnvYCLKeyID = "AVP_YCL_KEY_ID"
EnvYCLServiceAccountID = "AVP_YCL_SERVICE_ACCOUNT_ID"
EnvYCLPrivateKey = "AVP_YCL_PRIVATE_KEY"
EnvAvpUsername = "AVP_USERNAME"
EnvAvpPassword = "AVP_PASSWORD"
EnvPathValidation = "AVP_PATH_VALIDATION"
EnvAvpKSMConfigPath = "AVP_KEEPER_CONFIG_PATH"
EnvAvpDelineaURL = "AVP_DELINEA_URL"
EnvAvpDelineaUser = "AVP_DELINEA_USER"
EnvAvpDelineaPassword = "AVP_DELINEA_PASSWORD"
EnvAvpDelineaDomain = "AVP_DELINEA_DOMAIN"
EnvKubernetesSecretSecret = "AVP_K8S_SECRET_SECRET"

// Backend and Auth Constants
VaultBackend = "vault"
Expand All @@ -43,6 +44,7 @@ const (
DelineaSecretServerbackend = "delineasecretserver"
OnePasswordConnect = "1passwordconnect"
KeeperSecretsManagerBackend = "keepersecretsmanager"
KubernetesSecretBackend = "kubernetessecret"
K8sAuth = "k8s"
ApproleAuth = "approle"
GithubAuth = "github"
Expand Down

0 comments on commit 98b7116

Please sign in to comment.