provider.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package vault
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "time"
  19. vault "github.com/hashicorp/vault/api"
  20. "github.com/spf13/pflag"
  21. "k8s.io/client-go/kubernetes"
  22. typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  23. ctrl "sigs.k8s.io/controller-runtime"
  24. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  25. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. "github.com/external-secrets/external-secrets/pkg/cache"
  28. "github.com/external-secrets/external-secrets/pkg/feature"
  29. "github.com/external-secrets/external-secrets/pkg/provider/vault/util"
  30. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  31. )
  32. var (
  33. _ esv1.Provider = &Provider{}
  34. enableCache bool
  35. logger = ctrl.Log.WithName("provider").WithName("vault")
  36. clientCache *cache.Cache[util.Client]
  37. )
  38. const (
  39. errVaultStore = "received invalid Vault SecretStore resource: %w"
  40. errVaultClient = "cannot setup new vault client: %w"
  41. errVaultCert = "cannot set Vault CA certificate: %w"
  42. errClientTLSAuth = "error from Client TLS Auth: %q"
  43. errCANamespace = "missing namespace on caProvider secret"
  44. )
  45. const (
  46. defaultCacheSize = 2 << 17
  47. )
  48. type Provider struct {
  49. // NewVaultClient is a function that returns a new Vault client.
  50. // This is used for testing to inject a fake client.
  51. NewVaultClient func(config *vault.Config) (util.Client, error)
  52. }
  53. // NewVaultClient returns a new Vault client.
  54. func NewVaultClient(config *vault.Config) (util.Client, error) {
  55. vaultClient, err := vault.NewClient(config)
  56. if err != nil {
  57. return nil, err
  58. }
  59. return &util.VaultClient{
  60. SetTokenFunc: vaultClient.SetToken,
  61. TokenFunc: vaultClient.Token,
  62. ClearTokenFunc: vaultClient.ClearToken,
  63. AuthField: vaultClient.Auth(),
  64. AuthTokenField: vaultClient.Auth().Token(),
  65. LogicalField: vaultClient.Logical(),
  66. NamespaceFunc: vaultClient.Namespace,
  67. SetNamespaceFunc: vaultClient.SetNamespace,
  68. AddHeaderFunc: vaultClient.AddHeader,
  69. }, nil
  70. }
  71. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  72. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  73. return esv1.SecretStoreReadWrite
  74. }
  75. // NewClient implements the Client interface.
  76. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  77. // controller-runtime/client does not support TokenRequest or other subresource APIs
  78. // so we need to construct our own client and use it to fetch tokens
  79. // (for Kubernetes service account token auth)
  80. restCfg, err := ctrlcfg.GetConfig()
  81. if err != nil {
  82. return nil, err
  83. }
  84. clientset, err := kubernetes.NewForConfig(restCfg)
  85. if err != nil {
  86. return nil, err
  87. }
  88. return p.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
  89. }
  90. func (p *Provider) NewGeneratorClient(ctx context.Context, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1.VaultProvider, namespace string, retrySettings *esv1.SecretStoreRetrySettings) (util.Client, error) {
  91. vStore, cfg, err := p.prepareConfig(ctx, kube, corev1, vaultSpec, retrySettings, namespace, resolvers.EmptyStoreKind)
  92. if err != nil {
  93. return nil, err
  94. }
  95. client, err := p.NewVaultClient(cfg)
  96. if err != nil {
  97. return nil, err
  98. }
  99. _, err = p.initClient(ctx, vStore, client, cfg, vaultSpec)
  100. if err != nil {
  101. return nil, err
  102. }
  103. return client, nil
  104. }
  105. func (p *Provider) newClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1.SecretsClient, error) {
  106. storeSpec := store.GetSpec()
  107. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
  108. return nil, errors.New(errVaultStore)
  109. }
  110. vaultSpec := storeSpec.Provider.Vault
  111. vStore, cfg, err := p.prepareConfig(
  112. ctx,
  113. kube,
  114. corev1,
  115. vaultSpec,
  116. storeSpec.RetrySettings,
  117. namespace,
  118. store.GetObjectKind().GroupVersionKind().Kind)
  119. if err != nil {
  120. return nil, err
  121. }
  122. client, err := getVaultClient(p, store, cfg, namespace)
  123. if err != nil {
  124. return nil, fmt.Errorf(errVaultClient, err)
  125. }
  126. return p.initClient(ctx, vStore, client, cfg, vaultSpec)
  127. }
  128. func (p *Provider) initClient(ctx context.Context, c *client, client util.Client, cfg *vault.Config, vaultSpec *esv1.VaultProvider) (esv1.SecretsClient, error) {
  129. if vaultSpec.Namespace != nil {
  130. client.SetNamespace(*vaultSpec.Namespace)
  131. }
  132. if vaultSpec.Headers != nil {
  133. for hKey, hValue := range vaultSpec.Headers {
  134. client.AddHeader(hKey, hValue)
  135. }
  136. }
  137. if vaultSpec.ReadYourWrites && vaultSpec.ForwardInconsistent {
  138. client.AddHeader("X-Vault-Inconsistent", "forward-active-node")
  139. }
  140. c.client = client
  141. c.auth = client.Auth()
  142. c.logical = client.Logical()
  143. c.token = client.AuthToken()
  144. // allow SecretStore controller validation to pass
  145. // when using referent namespace.
  146. if c.storeKind == esv1.ClusterSecretStoreKind && c.namespace == "" && isReferentSpec(vaultSpec) {
  147. return c, nil
  148. }
  149. if err := c.setAuth(ctx, cfg); err != nil {
  150. return nil, err
  151. }
  152. return c, nil
  153. }
  154. func (p *Provider) prepareConfig(ctx context.Context, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1.VaultProvider, retrySettings *esv1.SecretStoreRetrySettings, namespace, storeKind string) (*client, *vault.Config, error) {
  155. c := &client{
  156. kube: kube,
  157. corev1: corev1,
  158. store: vaultSpec,
  159. log: logger,
  160. namespace: namespace,
  161. storeKind: storeKind,
  162. }
  163. cfg, err := c.newConfig(ctx)
  164. if err != nil {
  165. return nil, nil, err
  166. }
  167. // Setup retry options if present
  168. if retrySettings != nil {
  169. if retrySettings.MaxRetries != nil {
  170. cfg.MaxRetries = int(*retrySettings.MaxRetries)
  171. } else {
  172. // By default we rely only on the reconciliation process for retrying
  173. cfg.MaxRetries = 0
  174. }
  175. if retrySettings.RetryInterval != nil {
  176. retryWait, err := time.ParseDuration(*retrySettings.RetryInterval)
  177. if err != nil {
  178. return nil, nil, err
  179. }
  180. cfg.MinRetryWait = retryWait
  181. cfg.MaxRetryWait = retryWait
  182. }
  183. }
  184. return c, cfg, nil
  185. }
  186. func getVaultClient(p *Provider, store esv1.GenericStore, cfg *vault.Config, namespace string) (util.Client, error) {
  187. vaultProvider := store.GetSpec().Provider.Vault
  188. auth := vaultProvider.Auth
  189. isStaticToken := auth != nil && auth.TokenSecretRef != nil
  190. useCache := enableCache && !isStaticToken
  191. keyNamespace := store.GetObjectMeta().Namespace
  192. // A single ClusterSecretStore may need to spawn separate vault clients for each namespace.
  193. if store.GetTypeMeta().Kind == esv1.ClusterSecretStoreKind && namespace != "" && isReferentSpec(vaultProvider) {
  194. keyNamespace = namespace
  195. }
  196. key := cache.Key{
  197. Name: store.GetObjectMeta().Name,
  198. Namespace: keyNamespace,
  199. Kind: store.GetTypeMeta().Kind,
  200. }
  201. if useCache {
  202. client, ok := clientCache.Get(store.GetObjectMeta().ResourceVersion, key)
  203. if ok {
  204. return client, nil
  205. }
  206. }
  207. client, err := p.NewVaultClient(cfg)
  208. if err != nil {
  209. return nil, fmt.Errorf(errVaultClient, err)
  210. }
  211. if useCache && !clientCache.Contains(key) {
  212. clientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
  213. }
  214. return client, nil
  215. }
  216. func isReferentSpec(prov *esv1.VaultProvider) bool {
  217. if prov.Auth == nil {
  218. return false
  219. }
  220. if prov.Auth.TokenSecretRef != nil && prov.Auth.TokenSecretRef.Namespace == nil {
  221. return true
  222. }
  223. if prov.Auth.AppRole != nil && prov.Auth.AppRole.SecretRef.Namespace == nil {
  224. return true
  225. }
  226. if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.SecretRef != nil && prov.Auth.Kubernetes.SecretRef.Namespace == nil {
  227. return true
  228. }
  229. if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.ServiceAccountRef != nil && prov.Auth.Kubernetes.ServiceAccountRef.Namespace == nil {
  230. return true
  231. }
  232. if prov.Auth.Ldap != nil && prov.Auth.Ldap.SecretRef.Namespace == nil {
  233. return true
  234. }
  235. if prov.Auth.UserPass != nil && prov.Auth.UserPass.SecretRef.Namespace == nil {
  236. return true
  237. }
  238. if prov.Auth.Jwt != nil && prov.Auth.Jwt.SecretRef != nil && prov.Auth.Jwt.SecretRef.Namespace == nil {
  239. return true
  240. }
  241. if prov.Auth.Jwt != nil && prov.Auth.Jwt.KubernetesServiceAccountToken != nil && prov.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef.Namespace == nil {
  242. return true
  243. }
  244. if prov.Auth.Cert != nil && prov.Auth.Cert.SecretRef.Namespace == nil {
  245. return true
  246. }
  247. if prov.Auth.Iam != nil && prov.Auth.Iam.JWTAuth != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef.Namespace == nil {
  248. return true
  249. }
  250. if prov.Auth.Iam != nil && prov.Auth.Iam.SecretRef != nil &&
  251. (prov.Auth.Iam.SecretRef.AccessKeyID.Namespace == nil ||
  252. prov.Auth.Iam.SecretRef.SecretAccessKey.Namespace == nil ||
  253. (prov.Auth.Iam.SecretRef.SessionToken != nil && prov.Auth.Iam.SecretRef.SessionToken.Namespace == nil)) {
  254. return true
  255. }
  256. return false
  257. }
  258. func initCache(size int) {
  259. logger.Info("initializing vault cache", "size", size)
  260. clientCache = cache.Must(size, func(client util.Client) {
  261. err := revokeTokenIfValid(context.Background(), client)
  262. if err != nil {
  263. logger.Error(err, "unable to revoke cached token on eviction")
  264. }
  265. })
  266. }
  267. func init() {
  268. var vaultTokenCacheSize int
  269. fs := pflag.NewFlagSet("vault", pflag.ExitOnError)
  270. fs.BoolVar(&enableCache, "experimental-enable-vault-token-cache", false, "Enable experimental Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.")
  271. // max. 265k vault leases with 30bytes each ~= 7MB
  272. fs.IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", defaultCacheSize, "Maximum size of Vault token cache. When more tokens than Only used if --experimental-enable-vault-token-cache is set.")
  273. feature.Register(feature.Feature{
  274. Flags: fs,
  275. Initialize: func() { initCache(vaultTokenCacheSize) },
  276. })
  277. esv1.Register(&Provider{
  278. NewVaultClient: NewVaultClient,
  279. }, &esv1.SecretStoreProvider{
  280. Vault: &esv1.VaultProvider{},
  281. }, esv1.MaintenanceStatusMaintained)
  282. }