Browse Source

fix: support more azure key types

Moritz Johner 4 years ago
parent
commit
0a56d2d388

+ 2 - 0
docs/guides-templating.md

@@ -23,6 +23,8 @@ We provide a bunch of convenience functions that help you transform your secrets
 | pkcs12certPass | extracts the certificate from a pkcs12 archive using the provided password | password `string`, data `[]byte` | `[]byte`      |
 | pemPrivateKey  | PEM encodes the provided bytes as private key                              | `[]byte`                         | `string`      |
 | pemCertificate | PEM encodes the provided bytes as certificate                              | `[]byte`                         | `string`      |
+| jwkPublicKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PUBLIC KEY` that contains the public key ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey)) for details | `[]byte`                         | `string`      |
+| jwkPrivateKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey)) for details | `[]byte`                         | `string`      |
 | base64decode   | decodes the provided bytes as base64                                       | `[]byte`                         | `[]byte`      |
 | base64encode   | encodes the provided bytes as base64                                       | `[]byte`                         | `[]byte`      |
 | fromJSON       | parses the bytes as JSON so you can access individual properties           | `[]byte`                         | `interface{}` |

File diff suppressed because it is too large
+ 1 - 1
docs/pictures/diagrams.drawio


BIN
docs/pictures/eso-az-kv-azure-kv.png


+ 12 - 2
docs/provider-azure-key-vault.md

@@ -3,7 +3,7 @@
 
 ## Azure Key vault
 
-External Secrets Operator integrates with [Azure Key vault](https://azure.microsoft.com/en-us/services/key-vault/) for secrets , certificates and Keys management.
+External Secrets Operator integrates with [Azure Key vault](https://azure.microsoft.com/en-us/services/key-vault/) for secrets, certificates and Keys management.
 
 ### Authentication
 
@@ -24,6 +24,17 @@ Be sure the `azkv` provider is listed in the `Kind=SecretStore`
 {% include 'azkv-secret-store.yaml' %}
 ```
 
+### Object Types
+
+Azure KeyVault manages different [object types](https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-types), we support `keys`, `secrets` and `certificates`. Simply prefix the key with `key`, `secret` or `cert` to retrieve the desired type (defaults to secret).
+
+| Object Type   | Return Value                                                                                                                                                                                                                      |
+| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `secret`      | the raw secret value.                                                                                                                                                                                                             |
+| `key`         | A JWK which contains the public key. Azure KeyVault does **not** export the private key. You may want to use [template functions](guides-templating.md) to transform this JWK into PEM encoded PKIX ASN.1 DER format. |
+| `certificate` | The raw CER contents of the x509 certificate. You may want to use [template functions](guides-templating.md) to transform this into your desired encoding                                                             |
+
+
 ### Creating external secret
 
 To create a kubernetes secret from the Azure Key vault secret a `Kind=ExternalSecret` is needed.
@@ -40,4 +51,3 @@ The operator will fetch the Azure Key vault secret and inject it as a `Kind=Secr
 ```
 kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
 ```
-

+ 23 - 18
docs/snippets/azkv-external-secret.yaml

@@ -3,34 +3,39 @@ kind: ExternalSecret
 metadata:
   name: example-external-secret
 spec:
-  refreshInterval: 1h           # rate SecretManager pulls Azure
+  refreshInterval: 1h
   secretStoreRef:
     kind: SecretStore
-    name: example-secret-store               # name of the SecretStore (or kind specified)
- 
+    name: example-secret-store
+
   target:
-    name: secret-to-be-created  # name of the k8s Secret to be created
+    name: secret-to-be-created
     creationPolicy: Owner
-  
+
   data:
-  - secretKey: dev-secret-test  # name of the  key to be created in the secret object
+  # name of the SECRET in the Azure KV (no prefix is by default a SECRET)
+  - secretKey: dev-secret-test
     remoteRef:
-      key: dev-secret-test #name of the SECRET in the Azure KV (no prefix => SECRET)
+      key: dev-secret-test
 
-  - secretKey: dev-another-secret-test  # name of the  key to be created in the secret object
+  # explicit type and name of secret in the Azure KV
+  - secretKey: dev-another-secret-test
     remoteRef:
-      key: secret/dev-secret-test #type and name of secret in the Azure KV
+      key: secret/dev-secret-test
 
-  - secretKey: dev-cert-test  # name of the  key to be created in the secret object
+  # type/name of certificate in the Azure KV
+  # raw value will be returned, use templating features for data processing
+  - secretKey: dev-cert-test
     remoteRef:
-      key: cert/dev-cert-test #type/name of certificate in the Azure KV 
-                              #raw value will be returned , use templating features for data processing
+      key: cert/dev-cert-test
 
-  - secretKey: dev-key-test  # name of the  key to be created in the secret object
+  # type/name of the public key in the Azure KV
+  # the key is returned PEM encoded
+  - secretKey: dev-key-test
     remoteRef:
-      key: key/dev-key-test #type/name of the public key in the Azure KV 
-  
-  # dataFrom , return ALL secrets saved in the referenced secretStore 
+      key: key/dev-key-test
+
+  # dataFrom , return ALL secrets saved in the referenced secretStore
   # each secret name in the KV will be used as the secret key in the SECRET k8s target object
-  dataFrom: 
-  - name: "*"
+  dataFrom:
+  - name: "*"

+ 10 - 6
docs/snippets/azkv-secret-store.yaml

@@ -3,13 +3,17 @@ kind: SecretStore
 metadata:
   name: example-secret-store
 spec:
-  azurekv:      #Provider type , azure keyvault
-    tenantid: "d3bc2180-xxxx-xxxx-xxxx-154105743342" #azure tenant ID
-    vaultUrl: "https://my-keyvault-name.vault.azure.net" #Keyvault URL
+  # provider type: azure keyvault
+  azurekv:
+    # azure tenant ID, see: https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant
+    tenantId: "d3bc2180-xxxx-xxxx-xxxx-154105743342"
+    # URL of your vault instance, see: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates
+    vaultUrl: "https://my-keyvault-name.vault.azure.net"
     authSecretRef:
-      #Secret created in the cluster holding the azure service principal with proper access rights
-      clientID:
-        name: azure-secret-sp  
+      # points to the secret that contains
+      # the azure service principal credentials
+      clientId:
+        name: azure-secret-sp
         key: ClientID
       clientSecret:
         name: azure-secret-sp

+ 109 - 0
docs/spec.md

@@ -180,6 +180,101 @@ see: <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.
 </td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.AzureKVAuth">AzureKVAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AzureKVProvider">AzureKVProvider</a>)
+</p>
+<p>
+<p>Configuration used to authenticate with Azure.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>clientId</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The Azure clientId of the service principle used for authentication.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>clientSecret</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The Azure ClientSecret of the service principle used for authentication.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AzureKVProvider">AzureKVProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>Configures an store to sync secrets using Azure KV.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>vaultUrl</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Vault Url from which the secrets to be fetched from.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>tenantId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>TenantID configures the Azure Tenant to send requests to.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>authSecretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AzureKVAuth">
+AzureKVAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how the operator authenticates with Azure.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.ClusterSecretStore">ClusterSecretStore
 </h3>
 <p>
@@ -1132,6 +1227,20 @@ AWSProvider
 </tr>
 <tr>
 <td>
+<code>azurekv</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AzureKVProvider">
+AzureKVProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>AzureKV configures this store to sync secrets using Azure Key Vault provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>vault</code></br>
 <em>
 <a href="#external-secrets.io/v1alpha1.VaultProvider">

+ 1 - 0
go.mod

@@ -56,6 +56,7 @@ require (
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/kr/text v0.2.0 // indirect
+	github.com/lestrrat-go/jwx v1.2.1
 	github.com/mitchellh/mapstructure v1.3.3 // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/onsi/ginkgo v1.16.1

+ 26 - 0
go.sum

@@ -140,6 +140,10 @@ github.com/crossplane/crossplane-runtime v0.13.0/go.mod h1:Bc54/KBvV9ld/tvervcnh
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@@ -211,6 +215,8 @@ github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
 github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/gobuffalo/flect v0.2.2 h1:PAVD7sp0KOdfswjAw9BpLCU9hXo7wFSzgpQ+zNeks/A=
 github.com/gobuffalo/flect v0.2.2/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=
+github.com/goccy/go-json v0.4.8 h1:TfwOxfSp8hXH+ivoOk36RyDNmXATUETRdaNWDaZglf8=
+github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -404,6 +410,22 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lestrrat-go/backoff/v2 v2.0.7 h1:i2SeK33aOFJlUNJZzf2IpXRBvqBBnaGXfY5Xaop/GsE=
+github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
+github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
+github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
+github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
+github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
+github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
+github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
+github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.2.1 h1:WJ/3tiPUz1wV24KiwMEanbENwHnYub9UqzCbQ82mv9c=
+github.com/lestrrat-go/jwx v1.2.1/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc=
+github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
+github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8=
+github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -674,6 +696,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@@ -711,6 +734,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -905,8 +929,10 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 h1:cYSqdOzmV9wJ7lWurRAws06Dmif0Wv6UL4gQLlz+im0=
 golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=

+ 82 - 1
pkg/provider/azure/keyvault/fake/fake.go

@@ -21,15 +21,22 @@ import (
 	mock "github.com/stretchr/testify/mock"
 )
 
-type secretData = struct {
+type secretData struct {
 	item           keyvault.SecretItem
 	secretVersions map[string]keyvault.SecretBundle
 	lastVersion    string
 }
 
+type keyData struct {
+	item        keyvault.KeyItem
+	keyVersions map[string]keyvault.KeyBundle
+	lastVersion string
+}
+
 type AzureMock struct {
 	mock.Mock
 	knownSecrets map[string]map[string]*secretData
+	knownKeys    map[string]map[string]*keyData
 }
 
 func (m *AzureMock) AddSecret(vaultBaseURL, secretName, secretContent string, enabled bool) string {
@@ -103,6 +110,75 @@ func (m *AzureMock) ExpectsGetSecretsComplete(ctx context.Context, vaultBaseURL
 	m.On("GetSecretsComplete", ctx, vaultBaseURL, maxresults).Return(returnValue, nil)
 }
 
+func (m *AzureMock) AddKey(vaultBaseURL, keyName string, key *keyvault.JSONWebKey, enabled bool) string {
+	uid := uuid.NewString()
+	m.AddKeyWithVersion(vaultBaseURL, keyName, uid, key, enabled)
+	return uid
+}
+
+func (m *AzureMock) AddKeyWithVersion(vaultBaseURL, keyName, keyVersion string, key *keyvault.JSONWebKey, enabled bool) {
+	if m.knownKeys == nil {
+		m.knownKeys = make(map[string]map[string]*keyData)
+	}
+	if m.knownKeys[vaultBaseURL] == nil {
+		m.knownKeys[vaultBaseURL] = make(map[string]*keyData)
+	}
+
+	keyItemID := vaultBaseURL + keyName
+
+	if m.knownKeys[vaultBaseURL][keyName] == nil {
+		m.knownKeys[vaultBaseURL][keyName] = &keyData{
+			item:        newValidKeyItem(keyItemID, enabled),
+			keyVersions: make(map[string]keyvault.KeyBundle),
+		}
+	} else {
+		m.knownKeys[vaultBaseURL][keyName].item.Attributes.Enabled = &enabled
+	}
+	m.knownKeys[vaultBaseURL][keyName].keyVersions[keyVersion] = newValidKeyBundle(key)
+	m.knownKeys[vaultBaseURL][keyName].lastVersion = keyVersion
+}
+
+func newValidKeyBundle(key *keyvault.JSONWebKey) keyvault.KeyBundle {
+	return keyvault.KeyBundle{
+		Key: key,
+	}
+}
+
+func newValidKeyItem(keyItemID string, enabled bool) keyvault.KeyItem {
+	return keyvault.KeyItem{
+		Kid:        &keyItemID,
+		Attributes: &keyvault.KeyAttributes{Enabled: &enabled},
+	}
+}
+
+func (m *AzureMock) ExpectsGetKey(ctx context.Context, vaultBaseURL, keyName, keyVersion string) {
+	data := m.knownKeys[vaultBaseURL][keyName]
+	version := keyVersion
+	if version == "" {
+		version = data.lastVersion
+	}
+	returnValue := data.keyVersions[version]
+	m.On("GetKey", ctx, vaultBaseURL, keyName, keyVersion).Return(returnValue, nil)
+}
+
+func (m *AzureMock) ExpectsGetKeysComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) {
+	keyMap := m.knownKeys[vaultBaseURL]
+	keyItems := make([]keyvault.KeyItem, len(keyMap))
+	i := 0
+	for _, value := range keyMap {
+		keyItems[i] = value.item
+		i++
+	}
+	firstPage := keyvault.KeyListResult{
+		Value:    &keyItems,
+		NextLink: nil,
+	}
+	returnValue := keyvault.NewKeyListResultIterator(keyvault.NewKeyListResultPage(firstPage, func(context.Context, keyvault.KeyListResult) (keyvault.KeyListResult, error) {
+		return keyvault.KeyListResult{}, nil
+	}))
+	m.On("GetKeysComplete", ctx, vaultBaseURL, maxresults).Return(returnValue, nil)
+}
+
 func (m *AzureMock) GetKey(ctx context.Context, vaultBaseURL, keyName, keyVersion string) (result keyvault.KeyBundle, err error) {
 	args := m.Called(ctx, vaultBaseURL, keyName, keyVersion)
 	return args.Get(0).(keyvault.KeyBundle), args.Error(1)
@@ -121,3 +197,8 @@ func (m *AzureMock) GetSecretsComplete(ctx context.Context, vaultBaseURL string,
 	args := m.Called(ctx, vaultBaseURL, maxresults)
 	return args.Get(0).(keyvault.SecretListResultIterator), args.Error(1)
 }
+
+func (m *AzureMock) GetKeysComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.KeyListResultIterator, err error) {
+	args := m.Called(ctx, vaultBaseURL, maxresults)
+	return args.Get(0).(keyvault.KeyListResultIterator), args.Error(1)
+}

+ 20 - 98
pkg/provider/azure/keyvault/keyvault.go

@@ -15,20 +15,14 @@ limitations under the License.
 package keyvault
 
 import (
-	"bytes"
 	"context"
-	"crypto/rsa"
-	"crypto/x509"
-	"encoding/base64"
-	"encoding/pem"
+	"encoding/json"
 	"fmt"
-	"math/big"
 	"path"
 	"strings"
 
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
 	kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
-	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -94,7 +88,6 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretData
 	version := ""
 	objectType := "secret"
 	basicClient := a.baseClient
-	var secretValue []byte // The value of the secret that will be set to the k8s secret object
 
 	if ref.Version != "" {
 		version = ref.Version
@@ -111,36 +104,33 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretData
 
 	switch objectType {
 	case "secret":
+		// returns a SecretBundle with the secret value
+		// https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#SecretBundle
 		secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, version)
 		if err != nil {
 			return nil, err
 		}
-		secretValue = []byte(*secretResp.Value)
-
+		return []byte(*secretResp.Value), nil
 	case "cert":
+		// returns a CertBundle. We return CER contents of x509 certificate
+		// see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#CertificateBundle
 		secretResp, err := basicClient.GetCertificate(context.Background(), a.vaultURL, secretName, version)
 		if err != nil {
 			return nil, err
 		}
-		secretValue = *secretResp.Cer
-
+		return *secretResp.Cer, nil
 	case "key":
+		// returns a KeyBundla that contains a jwk
+		// azure kv returns only public keys
+		// see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#KeyBundle
 		keyResp, err := basicClient.GetKey(context.Background(), a.vaultURL, secretName, version)
 		if err != nil {
 			return nil, err
 		}
-		jwk := *keyResp.Key
-
-		secretValue, err = getPublicKeyFromJwk(jwk)
-		if err != nil {
-			return nil, err
-		}
-
-	default:
-		return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
+		return json.Marshal(keyResp.Key)
 	}
 
-	return secretValue, nil
+	return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
 }
 
 // Implements store.Client.GetSecretMap Interface.
@@ -177,74 +167,6 @@ func (a *Azure) GetSecretMap(ctx context.Context, _ esv1alpha1.ExternalSecretDat
 	return secretsMap, nil
 }
 
-// getCertBundle returns the certificate bundle.
-func getCertBundleForPKCS(certificateRawVal string) (bundle string, err error) {
-	pfx, err := base64.StdEncoding.DecodeString(certificateRawVal)
-
-	if err != nil {
-		return bundle, err
-	}
-	blocks, _ := pkcs12.ToPEM(pfx, "")
-
-	for _, block := range blocks {
-		// no headers
-		if block.Type == "PRIVATE KEY" {
-			pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
-			if err != nil {
-				panic(err)
-			}
-			derStream := x509.MarshalPKCS1PrivateKey(pkey)
-			block = &pem.Block{
-				Type:  "RSA PRIVATE KEY",
-				Bytes: derStream,
-			}
-		}
-		block.Headers = nil
-		bundle += string(pem.EncodeToMemory(block))
-	}
-	return bundle, nil
-}
-
-func getPublicKeyFromJwk(jwk keyvault.JSONWebKey) (bundle []byte, err error) {
-	if jwk.Kty != "RSA" {
-		return nil, fmt.Errorf("invalid key type: %s", jwk.Kty)
-	}
-	// decode the base64 bytes for n
-	nb, err := base64.RawURLEncoding.DecodeString(*jwk.N)
-	if err != nil {
-		return nil, err
-	}
-	e := 0
-	// The default exponent is usually 65537, so just compare the
-	// base64 for [1,0,1] or [0,1,0,1]
-	if *jwk.E == "AQAB" || *jwk.E == "AAEAAQ" {
-		e = 65537
-	} else {
-		// need to decode "e" as a big-endian int
-		return nil, fmt.Errorf("need to deocde e: %s", *jwk.E)
-	}
-
-	pk := &rsa.PublicKey{
-		N: new(big.Int).SetBytes(nb),
-		E: e,
-	}
-
-	der, err := x509.MarshalPKIXPublicKey(pk)
-	if err != nil {
-		return nil, err
-	}
-	block := &pem.Block{
-		Type:  "RSA PUBLIC KEY",
-		Bytes: der,
-	}
-	var out bytes.Buffer
-	err = pem.Encode(&out, block)
-	if err != nil {
-		return nil, err
-	}
-	return out.Bytes(), nil
-}
-
 func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, string, error) {
 	spec := *a.store.GetSpec().Provider.AzureKV
 	tenantID := *spec.TenantID
@@ -253,18 +175,18 @@ func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, strin
 	if spec.AuthSecretRef == nil {
 		return nil, "", fmt.Errorf("missing clientID/clientSecret in store config")
 	}
-	scoped := true
-	if a.store.GetObjectMeta().String() == "ClusterSecretStore" {
-		scoped = false
+	clusterScoped := false
+	if a.store.GetObjectMeta().String() == esv1alpha1.ClusterSecretStoreKind {
+		clusterScoped = true
 	}
 	if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
 		return nil, "", fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
 	}
-	cid, err := a.secretKeyRef(ctx, a.store.GetNamespacedName(), *spec.AuthSecretRef.ClientID, scoped)
+	cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
 	if err != nil {
 		return nil, "", err
 	}
-	csec, err := a.secretKeyRef(ctx, a.store.GetNamespacedName(), *spec.AuthSecretRef.ClientSecret, scoped)
+	csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
 	if err != nil {
 		return nil, "", err
 	}
@@ -283,18 +205,18 @@ func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, strin
 	return &basicClient, vaultURL, nil
 }
 
-func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, scoped bool) (string, error) {
+func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
 	var secret corev1.Secret
 	ref := types.NamespacedName{
 		Namespace: namespace,
 		Name:      secretRef.Name,
 	}
-	if !scoped && secretRef.Namespace != nil {
+	if clusterScoped && secretRef.Namespace != nil {
 		ref.Namespace = *secretRef.Namespace
 	}
 	err := a.kube.Get(ctx, ref, &secret)
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("could not find secret %s/%s: %w", ref.Namespace, ref.Name, err)
 	}
 	keyBytes, ok := secret.Data[secretRef.Key]
 	if !ok {

+ 59 - 16
pkg/provider/azure/keyvault/keyvault_test.go

@@ -16,9 +16,12 @@ package keyvault
 
 import (
 	context "context"
+	"encoding/json"
 	"testing"
 
+	"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
 	tassert "github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -41,6 +44,9 @@ func TestNewClientNoCreds(t *testing.T) {
 	vaultURL := "https://local.vault.url"
 	tenantID := "1234"
 	store := esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
 		Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{
 			VaultURL: &vaultURL,
 			TenantID: &tenantID,
@@ -65,10 +71,55 @@ func TestNewClientNoCreds(t *testing.T) {
 
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret = &v1.SecretKeySelector{Name: "password"}
 	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
-	tassert.EqualError(t, err, "secrets \"user\" not found")
+	tassert.EqualError(t, err, "could not find secret internal/user: secrets \"user\" not found")
 	tassert.Nil(t, secretClient)
 }
 
+const (
+	jwkPubRSA = `{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}`
+	jwkPubEC  = `{"kid":"https://example.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}`
+)
+
+func TestGetKey(t *testing.T) {
+	testAzure, azureMock := newAzure()
+	ctx := context.Background()
+
+	tbl := []struct {
+		name   string
+		kvName string
+		jwk    *keyvault.JSONWebKey
+		out    string
+	}{
+		{
+			name:   "test public rsa key",
+			kvName: "my-rsa",
+			jwk:    newKVJWK([]byte(jwkPubRSA)),
+			out:    jwkPubRSA,
+		},
+		{
+			name:   "test public ec key",
+			kvName: "my-ec",
+			jwk:    newKVJWK([]byte(jwkPubEC)),
+			out:    jwkPubEC,
+		},
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			azureMock.AddKey(testAzure.vaultURL, row.kvName, row.jwk, true)
+			azureMock.ExpectsGetKey(ctx, testAzure.vaultURL, row.kvName, "")
+
+			rf := esv1alpha1.ExternalSecretDataRemoteRef{
+				Key: "key/" + row.kvName,
+			}
+			secret, err := testAzure.GetSecret(ctx, rf)
+			azureMock.AssertExpectations(t)
+			tassert.Nil(t, err, "the return err should be nil")
+			tassert.Equal(t, []byte(row.out), secret)
+		})
+	}
+}
+
 func TestGetSecretWithVersion(t *testing.T) {
 	testAzure, azureMock := newAzure()
 	ctx := context.Background()
@@ -128,19 +179,11 @@ func TestGetSecretMapNotEnabled(t *testing.T) {
 	tassert.Empty(t, secretMap)
 }
 
-func TestGetCertBundleForPKCS(t *testing.T) {
-	rawCertExample := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURC" +
-		"VENDQWUyZ0F3SUJBZ0lFUnIxWTdEQU5CZ2txaGtpRzl3MEJBUVVGQURBeU1Rc3d" +
-		"DUVlEVlFRR0V3SkUKUlRFUU1BNEdBMVVFQ2hNSFFXMWhaR1YxY3pFUk1BOEdBMV" +
-		"VFQXhNSVUwRlFJRkp2YjNRd0hoY05NVE13TWpFMApNVE15TmpRNVdoY05NelV4T" +
-		"WpNeE1UTXlOalE1V2pBeU1Rc3dDUVlEVlFRR0V3SkVSVEVRTUE0R0ExVUVDaE1I" +
-		"CnFWUlE3NjNGODFwWnorNXgyejJ6NmZyd0JHNUF3YUZKL1RmTE9HQzZQWnl5bW1" +
-		"pSlllL2tjUDdVeUhMQnBUUVkKLzloNTF5dDB5NlRBS1JmRk1wMlhuVUZBaWdyL0" +
-		"0xYVc1NjdORStQYzN5S0RWWlVHdU82UXZ0cExCZkpPS3pZSAowc3F3OElmYjRlN" +
-		"0R6TkJuTmRoVDhzbGdUYkh5K3RzZUtPb0xHNi9rUktmRmRvSmRoeHAzeGNnbm56" +
-		"ZkY0anUvCi9UZTRYaWsxNC9FMAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t"
-	c, ok := getCertBundleForPKCS(rawCertExample)
-	bundle := ""
-	tassert.Nil(t, ok)
-	tassert.Equal(t, c, bundle)
+func newKVJWK(b []byte) *keyvault.JSONWebKey {
+	var key keyvault.JSONWebKey
+	err := json.Unmarshal(b, &key)
+	if err != nil {
+		panic(err)
+	}
+	return &key
 }

+ 52 - 8
pkg/template/template.go

@@ -15,6 +15,7 @@ package template
 
 import (
 	"bytes"
+	"crypto/x509"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
@@ -22,6 +23,7 @@ import (
 	"strings"
 	tpl "text/template"
 
+	"github.com/lestrrat-go/jwx/jwk"
 	"github.com/youmark/pkcs8"
 	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
@@ -42,6 +44,9 @@ var tplFuncs = tpl.FuncMap{
 	"fromJSON":       fromJSON,
 	"toJSON":         toJSON,
 
+	"jwkPublicKeyPem":  jwkPublicKeyPem,
+	"jwkPrivateKeyPem": jwkPrivateKeyPem,
+
 	"toString": toString,
 	"toBytes":  toBytes,
 	"upper":    strings.ToUpper,
@@ -119,22 +124,61 @@ func pkcs12cert(input []byte) ([]byte, error) {
 	return pkcs12certPass("", input)
 }
 
-func pemPrivateKey(key []byte) (string, error) {
-	buf := bytes.NewBuffer(nil)
-	err := pem.Encode(buf, &pem.Block{Type: "PRIVATE KEY", Bytes: key})
+func jwkPublicKeyPem(jwkjson []byte) (string, error) {
+	k, err := jwk.ParseKey(jwkjson)
+	if err != nil {
+		return "", err
+	}
+	var rawkey interface{}
+	err = k.Raw(&rawkey)
+	if err != nil {
+		return "", err
+	}
+	mpk, err := x509.MarshalPKIXPublicKey(rawkey)
+	if err != nil {
+		return "", err
+	}
+	return pemEncode(mpk, "PUBLIC KEY")
+}
+
+func jwkPrivateKeyPem(jwkjson []byte) (string, error) {
+	k, err := jwk.ParseKey(jwkjson)
+	if err != nil {
+		return "", err
+	}
+	var mpk []byte
+	var pk interface{}
+	err = k.Raw(&pk)
+	if err != nil {
+		return "", err
+	}
+	mpk, err = x509.MarshalPKCS8PrivateKey(pk)
 	if err != nil {
-		return "", fmt.Errorf(errEncodePEMKey, err)
+		return "", err
 	}
+	return pemEncode(mpk, "PRIVATE KEY")
+}
+
+func pemEncode(thing []byte, kind string) (string, error) {
+	buf := bytes.NewBuffer(nil)
+	err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: thing})
 	return buf.String(), err
 }
 
+func pemPrivateKey(key []byte) (string, error) {
+	res, err := pemEncode(key, "PRIVATE KEY")
+	if err != nil {
+		return res, fmt.Errorf(errEncodePEMKey, err)
+	}
+	return res, nil
+}
+
 func pemCertificate(cert []byte) (string, error) {
-	buf := bytes.NewBuffer(nil)
-	err := pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
+	res, err := pemEncode(cert, "CERTIFICATE")
 	if err != nil {
-		return "", fmt.Errorf(errEncodePEMCert, err)
+		return res, fmt.Errorf(errEncodePEMCert, err)
 	}
-	return buf.String(), nil
+	return res, nil
 }
 
 func base64decode(in []byte) ([]byte, error) {

File diff suppressed because it is too large
+ 104 - 0
pkg/template/template_test.go