keyvault.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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.ExternalSecretDataFromRemoteRef) (map[string][]byte, error) {
  126. dataRef := ref.GetDataRemoteRef()
  127. objectType, secretName := getObjType(dataRef)
  128. switch objectType {
  129. case defaultObjType:
  130. data, err := a.GetSecret(ctx, dataRef)
  131. if err != nil {
  132. return nil, err
  133. }
  134. kv := make(map[string]string)
  135. err = json.Unmarshal(data, &kv)
  136. if err != nil {
  137. return nil, fmt.Errorf("error unmarshalling json data: %w", err)
  138. }
  139. secretData := make(map[string][]byte)
  140. for k, v := range kv {
  141. secretData[k] = []byte(v)
  142. }
  143. return secretData, nil
  144. case "cert":
  145. return nil, fmt.Errorf("cannot get use dataFrom to get certificate secret")
  146. case "key":
  147. return nil, fmt.Errorf("cannot get use dataFrom to get key secret")
  148. }
  149. return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
  150. }
  151. // Implements store.Client.GetAllSecrets Interface.
  152. // New version of GetAllSecrets.
  153. func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1alpha1.ExternalSecretDataFromRemoteRef) (map[string][]byte, error) {
  154. basicClient := a.baseClient
  155. secretsMap := make(map[string][]byte)
  156. checkTags := len(ref.Find.Tags) > 0
  157. checkName := len(ref.Find.Name.RegExp) > 0
  158. secretListIter, err := basicClient.GetSecretsComplete(context.Background(), a.vaultURL, nil)
  159. if err != nil {
  160. return nil, err
  161. }
  162. for secretListIter.NotDone() {
  163. secretList := secretListIter.Response().Value
  164. for _, secret := range *secretList {
  165. ok, secretName := isValidSecret(checkTags, checkName, ref, secret)
  166. if !ok {
  167. continue
  168. }
  169. secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, "")
  170. secretValue := *secretResp.Value
  171. if err != nil {
  172. return nil, err
  173. }
  174. secretsMap[secretName] = []byte(secretValue)
  175. }
  176. err = secretListIter.Next()
  177. if err != nil {
  178. return nil, err
  179. }
  180. }
  181. return secretsMap, nil
  182. }
  183. func isValidSecret(checkTags, checkName bool, ref esv1alpha1.ExternalSecretDataFromRemoteRef, secret keyvault.SecretItem) (bool, string) {
  184. if secret.ID == nil || !*secret.Attributes.Enabled {
  185. return false, ""
  186. }
  187. if checkTags && !okByTags(ref, secret) {
  188. return false, ""
  189. }
  190. secretName := path.Base(*secret.ID)
  191. if checkName && !okByName(ref, secretName) {
  192. return false, ""
  193. }
  194. return true, secretName
  195. }
  196. func okByName(ref esv1alpha1.ExternalSecretDataFromRemoteRef, secretName string) bool {
  197. matches, _ := regexp.MatchString(ref.Find.Name.RegExp, secretName)
  198. return matches
  199. }
  200. func okByTags(ref esv1alpha1.ExternalSecretDataFromRemoteRef, secret keyvault.SecretItem) bool {
  201. tagsFound := true
  202. for k, v := range ref.Find.Tags {
  203. if val, ok := secret.Tags[k]; !ok || *val != v {
  204. tagsFound = false
  205. break
  206. }
  207. }
  208. return tagsFound
  209. }
  210. func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
  211. spec := *a.store.GetSpec().Provider.AzureKV
  212. if *spec.AuthType != esv1alpha1.ManagedIdentity {
  213. return false, nil
  214. }
  215. msiConfig := kvauth.NewMSIConfig()
  216. msiConfig.Resource = vaultResource
  217. if spec.IdentityID != nil {
  218. msiConfig.ClientID = *spec.IdentityID
  219. }
  220. authorizer, err := msiConfig.Authorizer()
  221. if err != nil {
  222. return true, err
  223. }
  224. basicClient := keyvault.New()
  225. basicClient.Authorizer = authorizer
  226. a.baseClient = basicClient
  227. a.vaultURL = *spec.VaultURL
  228. return true, nil
  229. }
  230. func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
  231. spec := *a.store.GetSpec().Provider.AzureKV
  232. if *spec.AuthType != esv1alpha1.ServicePrincipal {
  233. return false, nil
  234. }
  235. if spec.TenantID == nil {
  236. return true, fmt.Errorf("missing tenantID in store config")
  237. }
  238. if spec.AuthSecretRef == nil {
  239. return true, fmt.Errorf("missing clientID/clientSecret in store config")
  240. }
  241. if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
  242. return true, fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
  243. }
  244. clusterScoped := false
  245. if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
  246. clusterScoped = true
  247. }
  248. cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
  249. if err != nil {
  250. return true, err
  251. }
  252. csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
  253. if err != nil {
  254. return true, err
  255. }
  256. clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID)
  257. clientCredentialsConfig.Resource = vaultResource
  258. authorizer, err := clientCredentialsConfig.Authorizer()
  259. if err != nil {
  260. return true, err
  261. }
  262. basicClient := keyvault.New()
  263. basicClient.Authorizer = authorizer
  264. a.baseClient = &basicClient
  265. a.vaultURL = *spec.VaultURL
  266. return true, nil
  267. }
  268. func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
  269. var secret corev1.Secret
  270. ref := types.NamespacedName{
  271. Namespace: namespace,
  272. Name: secretRef.Name,
  273. }
  274. if clusterScoped && secretRef.Namespace != nil {
  275. ref.Namespace = *secretRef.Namespace
  276. }
  277. err := a.kube.Get(ctx, ref, &secret)
  278. if err != nil {
  279. return "", fmt.Errorf("could not find secret %s/%s: %w", ref.Namespace, ref.Name, err)
  280. }
  281. keyBytes, ok := secret.Data[secretRef.Key]
  282. if !ok {
  283. return "", fmt.Errorf("no data for %q in secret '%s/%s'", secretRef.Key, secretRef.Name, namespace)
  284. }
  285. value := strings.TrimSpace(string(keyBytes))
  286. return value, nil
  287. }
  288. func (a *Azure) Close(ctx context.Context) error {
  289. return nil
  290. }
  291. func getObjType(ref esv1alpha1.ExternalSecretDataRemoteRef) (string, string) {
  292. objectType := defaultObjType
  293. secretName := ref.Key
  294. nameSplitted := strings.Split(secretName, "/")
  295. if len(nameSplitted) > 1 {
  296. objectType = nameSplitted[0]
  297. secretName = nameSplitted[1]
  298. // TODO: later tokens can be used to read the secret tags
  299. }
  300. return objectType, secretName
  301. }