keyvault.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package keyvault
  13. import (
  14. "context"
  15. "encoding/json"
  16. "fmt"
  17. "path"
  18. "regexp"
  19. "strings"
  20. "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
  21. kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
  22. "github.com/tidwall/gjson"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. "sigs.k8s.io/controller-runtime/pkg/client"
  26. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  27. smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  28. "github.com/external-secrets/external-secrets/pkg/provider"
  29. "github.com/external-secrets/external-secrets/pkg/provider/schema"
  30. )
  31. const (
  32. defaultObjType = "secret"
  33. vaultResource = "https://vault.azure.net"
  34. )
  35. // interface to keyvault.BaseClient.
  36. type SecretClient interface {
  37. GetKey(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (result keyvault.KeyBundle, err error)
  38. GetSecret(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
  39. GetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
  40. GetCertificate(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
  41. }
  42. type Azure struct {
  43. kube client.Client
  44. store esv1alpha1.GenericStore
  45. baseClient SecretClient
  46. vaultURL string
  47. namespace string
  48. }
  49. func init() {
  50. schema.Register(&Azure{}, &esv1alpha1.SecretStoreProvider{
  51. AzureKV: &esv1alpha1.AzureKVProvider{},
  52. })
  53. }
  54. // NewClient constructs a new secrets client based on the provided store.
  55. func (a *Azure) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
  56. return newClient(ctx, store, kube, namespace)
  57. }
  58. func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
  59. anAzure := &Azure{
  60. kube: kube,
  61. store: store,
  62. namespace: namespace,
  63. }
  64. clientSet, err := anAzure.setAzureClientWithManagedIdentity()
  65. if clientSet {
  66. return anAzure, err
  67. }
  68. clientSet, err = anAzure.setAzureClientWithServicePrincipal(ctx)
  69. if clientSet {
  70. return anAzure, err
  71. }
  72. return nil, fmt.Errorf("cannot initialize Azure Client: no valid authType was specified")
  73. }
  74. // Implements store.Client.GetSecret Interface.
  75. // Retrieves a secret/Key/Certificate with the secret name defined in ref.Name
  76. // The Object Type is defined as a prefix in the ref.Name , if no prefix is defined , we assume a secret is required.
  77. func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
  78. version := ""
  79. basicClient := a.baseClient
  80. objectType, secretName := getObjType(ref)
  81. if secretName == "" {
  82. return nil, fmt.Errorf("%s name cannot be empty", objectType)
  83. }
  84. if ref.Version != "" {
  85. version = ref.Version
  86. }
  87. switch objectType {
  88. case defaultObjType:
  89. // returns a SecretBundle with the secret value
  90. // https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#SecretBundle
  91. secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, version)
  92. if err != nil {
  93. return nil, err
  94. }
  95. if ref.Property == "" {
  96. return []byte(*secretResp.Value), nil
  97. }
  98. res := gjson.Get(*secretResp.Value, ref.Property)
  99. if !res.Exists() {
  100. return nil, fmt.Errorf("property %s does not exist in key %s", ref.Property, ref.Key)
  101. }
  102. return []byte(res.String()), err
  103. case "cert":
  104. // returns a CertBundle. We return CER contents of x509 certificate
  105. // see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#CertificateBundle
  106. secretResp, err := basicClient.GetCertificate(context.Background(), a.vaultURL, secretName, version)
  107. if err != nil {
  108. return nil, err
  109. }
  110. return *secretResp.Cer, nil
  111. case "key":
  112. // returns a KeyBundle that contains a jwk
  113. // azure kv returns only public keys
  114. // see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#KeyBundle
  115. keyResp, err := basicClient.GetKey(context.Background(), a.vaultURL, secretName, version)
  116. if err != nil {
  117. return nil, err
  118. }
  119. return json.Marshal(keyResp.Key)
  120. }
  121. return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
  122. }
  123. // Implements store.Client.GetSecretMap Interface.
  124. // New version of GetSecretMap.
  125. func (a *Azure) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  126. objectType, secretName := getObjType(ref)
  127. switch objectType {
  128. case defaultObjType:
  129. data, err := a.GetSecret(ctx, ref)
  130. if err != nil {
  131. return nil, err
  132. }
  133. kv := make(map[string]string)
  134. err = json.Unmarshal(data, &kv)
  135. if err != nil {
  136. return nil, fmt.Errorf("error unmarshalling json data: %w", err)
  137. }
  138. secretData := make(map[string][]byte)
  139. for k, v := range kv {
  140. secretData[k] = []byte(v)
  141. }
  142. return secretData, nil
  143. case "cert":
  144. return nil, fmt.Errorf("cannot get use dataFrom to get certificate secret")
  145. case "key":
  146. return nil, fmt.Errorf("cannot get use dataFrom to get key secret")
  147. }
  148. return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
  149. }
  150. // Implements store.Client.GetAllSecrets Interface.
  151. // New version of GetAllSecrets.
  152. func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  153. basicClient := a.baseClient
  154. secretsMap := make(map[string][]byte)
  155. checkTags := len(ref.Tags) > 0
  156. checkName := len(ref.RegExp) > 0
  157. secretListIter, err := basicClient.GetSecretsComplete(context.Background(), a.vaultURL, nil)
  158. if err != nil {
  159. return nil, err
  160. }
  161. for secretListIter.NotDone() {
  162. secretList := secretListIter.Response().Value
  163. for _, secret := range *secretList {
  164. if secret.ID == nil || !*secret.Attributes.Enabled {
  165. continue
  166. }
  167. if checkTags && !okByTags(ref, secret) {
  168. continue
  169. }
  170. secretName := path.Base(*secret.ID)
  171. if checkName && !okByName(ref, secretName) {
  172. continue
  173. }
  174. secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, "")
  175. secretValue := *secretResp.Value
  176. if err != nil {
  177. return nil, err
  178. }
  179. secretsMap[secretName] = []byte(secretValue)
  180. }
  181. err = secretListIter.Next()
  182. if err != nil {
  183. return nil, err
  184. }
  185. }
  186. return secretsMap, nil
  187. }
  188. func okByName(ref esv1alpha1.ExternalSecretDataRemoteRef, secretName string) bool {
  189. matches, _ := regexp.MatchString(ref.RegExp, secretName)
  190. return matches
  191. }
  192. func okByTags(ref esv1alpha1.ExternalSecretDataRemoteRef, secret keyvault.SecretItem) bool {
  193. tagsFound := true
  194. for k, v := range ref.Tags {
  195. if val, ok := secret.Tags[k]; !ok || *val != v {
  196. tagsFound = false
  197. break
  198. }
  199. }
  200. return tagsFound
  201. }
  202. func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
  203. spec := *a.store.GetSpec().Provider.AzureKV
  204. if *spec.AuthType != esv1alpha1.ManagedIdentity {
  205. return false, nil
  206. }
  207. msiConfig := kvauth.NewMSIConfig()
  208. msiConfig.Resource = vaultResource
  209. if spec.IdentityID != nil {
  210. msiConfig.ClientID = *spec.IdentityID
  211. }
  212. authorizer, err := msiConfig.Authorizer()
  213. if err != nil {
  214. return true, err
  215. }
  216. basicClient := keyvault.New()
  217. basicClient.Authorizer = authorizer
  218. a.baseClient = basicClient
  219. a.vaultURL = *spec.VaultURL
  220. return true, nil
  221. }
  222. func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
  223. spec := *a.store.GetSpec().Provider.AzureKV
  224. if *spec.AuthType != esv1alpha1.ServicePrincipal {
  225. return false, nil
  226. }
  227. if spec.TenantID == nil {
  228. return true, fmt.Errorf("missing tenantID in store config")
  229. }
  230. if spec.AuthSecretRef == nil {
  231. return true, fmt.Errorf("missing clientID/clientSecret in store config")
  232. }
  233. if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
  234. return true, fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
  235. }
  236. clusterScoped := false
  237. if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
  238. clusterScoped = true
  239. }
  240. cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
  241. if err != nil {
  242. return true, err
  243. }
  244. csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
  245. if err != nil {
  246. return true, err
  247. }
  248. clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID)
  249. clientCredentialsConfig.Resource = vaultResource
  250. authorizer, err := clientCredentialsConfig.Authorizer()
  251. if err != nil {
  252. return true, err
  253. }
  254. basicClient := keyvault.New()
  255. basicClient.Authorizer = authorizer
  256. a.baseClient = &basicClient
  257. a.vaultURL = *spec.VaultURL
  258. return true, nil
  259. }
  260. func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
  261. var secret corev1.Secret
  262. ref := types.NamespacedName{
  263. Namespace: namespace,
  264. Name: secretRef.Name,
  265. }
  266. if clusterScoped && secretRef.Namespace != nil {
  267. ref.Namespace = *secretRef.Namespace
  268. }
  269. err := a.kube.Get(ctx, ref, &secret)
  270. if err != nil {
  271. return "", fmt.Errorf("could not find secret %s/%s: %w", ref.Namespace, ref.Name, err)
  272. }
  273. keyBytes, ok := secret.Data[secretRef.Key]
  274. if !ok {
  275. return "", fmt.Errorf("no data for %q in secret '%s/%s'", secretRef.Key, secretRef.Name, namespace)
  276. }
  277. value := strings.TrimSpace(string(keyBytes))
  278. return value, nil
  279. }
  280. func (a *Azure) Close(ctx context.Context) error {
  281. return nil
  282. }
  283. func getObjType(ref esv1alpha1.ExternalSecretDataRemoteRef) (string, string) {
  284. objectType := defaultObjType
  285. secretName := ref.Key
  286. nameSplitted := strings.Split(secretName, "/")
  287. if len(nameSplitted) > 1 {
  288. objectType = nameSplitted[0]
  289. secretName = nameSplitted[1]
  290. // TODO: later tokens can be used to read the secret tags
  291. }
  292. return objectType, secretName
  293. }