Browse Source

:bug: Fixes vault PushSecret logic (#1866)

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Gustavo Fernandes de Carvalho 3 years ago
parent
commit
a051da82cf

+ 10 - 0
docs/provider/hashicorp-vault.md

@@ -315,6 +315,16 @@ or `Kind=ClusterSecretStore` resource.
 ```
 **NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in `secretRef` with the namespace where the secret resides.
 
+### PushSecret
+Vault supports PushSecret features which allow you to sync a given kubernetes secret key into a hashicorp vault secret. In order to do so, it is expected that the secret key is a valid JSON object.
+
+In order to use PushSecret, you need to give `create`, `read` and `update` permissions to the path where you want to push secrets to. Use it with care!
+
+Here is an example on how to set it up:
+```yaml
+{% include 'vault-pushsecret.yaml' %}
+```
+
 ### Vault Enterprise
 
 #### Eventual Consistency and Performance Standby Nodes

+ 26 - 0
docs/snippets/vault-pushsecret.yaml

@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: source-secret
+  namespace: default
+stringData:
+  source-key: "{\"foo\":\"bar\"}" # Needs to be a JSON
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example
+  namespace: default
+spec:
+  refreshInterval: 10s # Refresh interval for which push secret will reconcile
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: vault-secretstore
+      kind: SecretStore
+  selector:
+    secret:
+      name: source-secret # Source Kubernetes secret to be pushed
+  data:
+    - match:
+        secretKey: source-key # Source Kubernetes secret key containing the vault secret (in JSON format)
+        remoteRef:
+          remoteKey: vault/secret # path to vault secret. This path is appended with the vault-store path.

+ 15 - 13
pkg/provider/vault/vault.go

@@ -15,6 +15,7 @@ limitations under the License.
 package vault
 
 import (
+	"bytes"
 	"context"
 	"crypto/tls"
 	"crypto/x509"
@@ -450,10 +451,13 @@ func (v *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1bet
 			"managed-by": "external-secrets",
 		},
 	}
+	secretVal := make(map[string]interface{})
+	err := json.Unmarshal(value, &secretVal)
+	if err != nil {
+		return fmt.Errorf("failed to convert value to a valid JSON: %w", err)
+	}
 	secretToPush := map[string]interface{}{
-		"data": map[string]string{
-			remoteRef.GetRemoteKey(): string(value),
-		},
+		"data": secretVal,
 	}
 	path := v.buildPath(remoteRef.GetRemoteKey())
 	metaPath, err := v.buildMetadataPath(remoteRef.GetRemoteKey())
@@ -462,20 +466,11 @@ func (v *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1bet
 	}
 
 	// Retrieve the secret map from vault and convert the secret value in string form.
-	vaultSecret, err := v.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: path})
-	vaultSecretValue := string(vaultSecret[remoteRef.GetRemoteKey()])
+	vaultSecret, err := v.readSecret(ctx, path, "")
 	// If error is not of type secret not found, we should error
 	if err != nil && !strings.Contains(err.Error(), "secret not found") {
 		return err
 	}
-
-	// Retrieve the secret value to be pushed and convert it to string form.
-	pushSecretValue := string(value)
-
-	if vaultSecretValue == pushSecretValue {
-		return nil
-	}
-
 	// If the secret exists (err == nil), we should check if it is managed by external-secrets
 	if err == nil {
 		metadata, err := v.readSecretMetadata(ctx, remoteRef.GetRemoteKey())
@@ -487,6 +482,13 @@ func (v *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1bet
 			return fmt.Errorf("secret not managed by external-secrets")
 		}
 	}
+	vaultSecretValue, err := json.Marshal(vaultSecret)
+	if err != nil {
+		return fmt.Errorf("error marshaling vault secret: %w", err)
+	}
+	if bytes.Equal(vaultSecretValue, value) {
+		return nil
+	}
 	_, err = v.logical.WriteWithContext(ctx, metaPath, label)
 	if err != nil {
 		return err

+ 5 - 2
pkg/provider/vault/vault_test.go

@@ -1438,6 +1438,9 @@ func TestSetSecret(t *testing.T) {
 						"data": map[string]interface{}{
 							"fake-key": "fake-value",
 						},
+						"custom_metadata": map[string]interface{}{
+							"managed-by": "external-secrets",
+						},
 					}, nil),
 				},
 			},
@@ -1482,12 +1485,12 @@ func TestSetSecret(t *testing.T) {
 
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
-			ref := fakeRef{key: "fake-key"}
+			ref := fakeRef{key: "secret"}
 			client := &client{
 				logical: tc.args.vLogical,
 				store:   tc.args.store,
 			}
-			err := client.PushSecret(context.Background(), []byte("fake-value"), ref)
+			err := client.PushSecret(context.Background(), []byte(`{"fake-key":"fake-value"}`), ref)
 
 			// Error nil XOR tc.want.err nil
 			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {