provider.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. /*
  2. Copyright © The ESO Authors
  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. vaultutil "github.com/external-secrets/external-secrets/providers/v1/vault/util"
  28. "github.com/external-secrets/external-secrets/runtime/cache"
  29. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  30. "github.com/external-secrets/external-secrets/runtime/feature"
  31. )
  32. var (
  33. _ esv1.Provider = &Provider{}
  34. enableCache bool
  35. logger = ctrl.Log.WithName("provider").WithName("vault")
  36. clientCache *cache.Cache[vaultutil.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. // Provider implements the ESO Provider interface for Hashicorp Vault.
  49. type Provider struct {
  50. // NewVaultClient is a function that returns a new Vault client.
  51. // This is used for testing to inject a fake client.
  52. NewVaultClient func(config *vault.Config) (vaultutil.Client, error)
  53. }
  54. // NewVaultClient returns a new Vault client.
  55. func NewVaultClient(config *vault.Config) (vaultutil.Client, error) {
  56. vaultClient, err := vault.NewClient(config)
  57. if err != nil {
  58. return nil, err
  59. }
  60. return &vaultutil.VaultClient{
  61. SetTokenFunc: vaultClient.SetToken,
  62. TokenFunc: vaultClient.Token,
  63. ClearTokenFunc: vaultClient.ClearToken,
  64. AuthField: vaultClient.Auth(),
  65. AuthTokenField: vaultClient.Auth().Token(),
  66. LogicalField: vaultClient.Logical(),
  67. NamespaceFunc: vaultClient.Namespace,
  68. SetNamespaceFunc: vaultClient.SetNamespace,
  69. AddHeaderFunc: vaultClient.AddHeader,
  70. }, nil
  71. }
  72. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  73. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  74. return esv1.SecretStoreReadWrite
  75. }
  76. // NewClient implements the Client interface.
  77. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  78. // controller-runtime/client does not support TokenRequest or other subresource APIs
  79. // so we need to construct our own client and use it to fetch tokens
  80. // (for Kubernetes service account token auth)
  81. restCfg, err := ctrlcfg.GetConfig()
  82. if err != nil {
  83. return nil, err
  84. }
  85. clientset, err := kubernetes.NewForConfig(restCfg)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return p.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
  90. }
  91. // NewGeneratorClient creates a new Vault client for the generator controller.
  92. func (p *Provider) NewGeneratorClient(
  93. ctx context.Context,
  94. kube kclient.Client,
  95. corev1 typedcorev1.CoreV1Interface,
  96. vaultSpec *esv1.VaultProvider,
  97. namespace string,
  98. retrySettings *esv1.SecretStoreRetrySettings,
  99. ) (vaultutil.Client, error) {
  100. vStore, cfg, err := p.prepareConfig(ctx, kube, corev1, vaultSpec, retrySettings, namespace, resolvers.EmptyStoreKind)
  101. if err != nil {
  102. return nil, err
  103. }
  104. client, err := p.NewVaultClient(cfg)
  105. if err != nil {
  106. return nil, err
  107. }
  108. _, err = p.initClient(ctx, vStore, client, cfg, vaultSpec)
  109. if err != nil {
  110. return nil, err
  111. }
  112. return client, nil
  113. }
  114. func (p *Provider) newClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1.SecretsClient, error) {
  115. storeSpec := store.GetSpec()
  116. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
  117. return nil, errors.New(errVaultStore)
  118. }
  119. vaultSpec := storeSpec.Provider.Vault
  120. vStore, cfg, err := p.prepareConfig(
  121. ctx,
  122. kube,
  123. corev1,
  124. vaultSpec,
  125. storeSpec.RetrySettings,
  126. namespace,
  127. store.GetObjectKind().GroupVersionKind().Kind)
  128. if err != nil {
  129. return nil, err
  130. }
  131. client, err := getVaultClient(p, store, cfg, namespace)
  132. if err != nil {
  133. return nil, fmt.Errorf(errVaultClient, err)
  134. }
  135. return p.initClient(ctx, vStore, client, cfg, vaultSpec)
  136. }
  137. func (p *Provider) initClient(ctx context.Context, c *client, client vaultutil.Client, cfg *vault.Config, vaultSpec *esv1.VaultProvider) (esv1.SecretsClient, error) {
  138. if vaultSpec.Namespace != nil {
  139. client.SetNamespace(*vaultSpec.Namespace)
  140. }
  141. if vaultSpec.Headers != nil {
  142. for hKey, hValue := range vaultSpec.Headers {
  143. client.AddHeader(hKey, hValue)
  144. }
  145. }
  146. if vaultSpec.ReadYourWrites && vaultSpec.ForwardInconsistent {
  147. client.AddHeader("X-Vault-Inconsistent", "forward-active-node")
  148. }
  149. c.client = client
  150. c.auth = client.Auth()
  151. c.logical = client.Logical()
  152. c.token = client.AuthToken()
  153. // allow SecretStore controller validation to pass
  154. // when using referent namespace.
  155. if c.storeKind == esv1.ClusterSecretStoreKind && c.namespace == "" && isReferentSpec(vaultSpec) {
  156. return c, nil
  157. }
  158. // set auth also sets the token expiry value
  159. if err := c.setAuth(ctx, cfg); err != nil {
  160. return nil, err
  161. }
  162. return c, nil
  163. }
  164. func (p *Provider) prepareConfig(
  165. ctx context.Context,
  166. kube kclient.Client,
  167. corev1 typedcorev1.CoreV1Interface,
  168. vaultSpec *esv1.VaultProvider,
  169. retrySettings *esv1.SecretStoreRetrySettings,
  170. namespace, storeKind string,
  171. ) (*client, *vault.Config, error) {
  172. c := &client{
  173. kube: kube,
  174. corev1: corev1,
  175. store: vaultSpec,
  176. log: logger,
  177. namespace: namespace,
  178. storeKind: storeKind,
  179. }
  180. cfg, err := c.newConfig(ctx)
  181. if err != nil {
  182. return nil, nil, err
  183. }
  184. // Setup retry options if present
  185. if retrySettings != nil {
  186. if retrySettings.MaxRetries != nil {
  187. cfg.MaxRetries = int(*retrySettings.MaxRetries)
  188. } else {
  189. // By default we rely only on the reconciliation process for retrying
  190. cfg.MaxRetries = 0
  191. }
  192. if retrySettings.RetryInterval != nil {
  193. retryWait, err := time.ParseDuration(*retrySettings.RetryInterval)
  194. if err != nil {
  195. return nil, nil, err
  196. }
  197. cfg.MinRetryWait = retryWait
  198. cfg.MaxRetryWait = retryWait
  199. }
  200. }
  201. return c, cfg, nil
  202. }
  203. func getVaultClient(p *Provider, store esv1.GenericStore, cfg *vault.Config, namespace string) (vaultutil.Client, error) {
  204. vaultProvider := store.GetSpec().Provider.Vault
  205. auth := vaultProvider.Auth
  206. isStaticToken := auth != nil && auth.TokenSecretRef != nil
  207. useCache := enableCache && !isStaticToken
  208. keyNamespace := store.GetObjectMeta().Namespace
  209. // A single ClusterSecretStore may need to spawn separate vault clients for each namespace.
  210. if store.GetTypeMeta().Kind == esv1.ClusterSecretStoreKind && namespace != "" && isReferentSpec(vaultProvider) {
  211. keyNamespace = namespace
  212. }
  213. key := cache.Key{
  214. Name: store.GetObjectMeta().Name,
  215. Namespace: keyNamespace,
  216. Kind: store.GetTypeMeta().Kind,
  217. }
  218. if useCache {
  219. client, ok := clientCache.Get(store.GetObjectMeta().ResourceVersion, key)
  220. if ok {
  221. return client, nil
  222. }
  223. }
  224. client, err := p.NewVaultClient(cfg)
  225. if err != nil {
  226. return nil, fmt.Errorf(errVaultClient, err)
  227. }
  228. if useCache && !clientCache.Contains(key) {
  229. clientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
  230. }
  231. return client, nil
  232. }
  233. func isReferentSpec(prov *esv1.VaultProvider) bool {
  234. if prov.Auth == nil {
  235. return false
  236. }
  237. if prov.Auth.TokenSecretRef != nil && prov.Auth.TokenSecretRef.Namespace == nil {
  238. return true
  239. }
  240. if prov.Auth.AppRole != nil && prov.Auth.AppRole.SecretRef.Namespace == nil {
  241. return true
  242. }
  243. if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.SecretRef != nil && prov.Auth.Kubernetes.SecretRef.Namespace == nil {
  244. return true
  245. }
  246. if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.ServiceAccountRef != nil && prov.Auth.Kubernetes.ServiceAccountRef.Namespace == nil {
  247. return true
  248. }
  249. if prov.Auth.Ldap != nil && prov.Auth.Ldap.SecretRef.Namespace == nil {
  250. return true
  251. }
  252. if prov.Auth.UserPass != nil && prov.Auth.UserPass.SecretRef.Namespace == nil {
  253. return true
  254. }
  255. if prov.Auth.Jwt != nil && prov.Auth.Jwt.SecretRef != nil && prov.Auth.Jwt.SecretRef.Namespace == nil {
  256. return true
  257. }
  258. if prov.Auth.Jwt != nil && prov.Auth.Jwt.KubernetesServiceAccountToken != nil && prov.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef.Namespace == nil {
  259. return true
  260. }
  261. if prov.Auth.Cert != nil && prov.Auth.Cert.SecretRef.Namespace == nil {
  262. return true
  263. }
  264. if prov.Auth.Iam != nil && prov.Auth.Iam.JWTAuth != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef.Namespace == nil {
  265. return true
  266. }
  267. if prov.Auth.Iam != nil && prov.Auth.Iam.SecretRef != nil &&
  268. (prov.Auth.Iam.SecretRef.AccessKeyID.Namespace == nil ||
  269. prov.Auth.Iam.SecretRef.SecretAccessKey.Namespace == nil ||
  270. (prov.Auth.Iam.SecretRef.SessionToken != nil && prov.Auth.Iam.SecretRef.SessionToken.Namespace == nil)) {
  271. return true
  272. }
  273. return false
  274. }
  275. func initCache(size int) {
  276. logger.Info("initializing vault cache", "size", size)
  277. clientCache = cache.Must(size, func(client vaultutil.Client) {
  278. err := revokeTokenIfValid(context.Background(), client)
  279. if err != nil {
  280. logger.Error(err, "unable to revoke cached token on eviction")
  281. }
  282. })
  283. }
  284. func init() {
  285. var (
  286. vaultTokenCacheSize int
  287. experimentalEnableCache bool
  288. experimentalCacheSize int
  289. )
  290. fs := pflag.NewFlagSet("vault", pflag.ExitOnError)
  291. fs.BoolVar(
  292. &enableCache,
  293. "enable-vault-token-cache",
  294. false,
  295. "Enable Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.",
  296. )
  297. // max. 265k vault leases with 30bytes each ~= 7MB
  298. fs.IntVar(
  299. &vaultTokenCacheSize,
  300. "vault-token-cache-size",
  301. defaultCacheSize,
  302. "Maximum size of Vault token cache. Only used if --enable-vault-token-cache is set.",
  303. )
  304. fs.BoolVar(
  305. &experimentalEnableCache,
  306. "experimental-enable-vault-token-cache",
  307. false,
  308. "Enable Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.",
  309. )
  310. // max. 265k vault leases with 30bytes each ~= 7MB
  311. fs.IntVar(
  312. &experimentalCacheSize,
  313. "experimental-vault-token-cache-size",
  314. defaultCacheSize,
  315. "Maximum size of Vault token cache. Only used if --experimental-enable-vault-token-cache is set.",
  316. )
  317. feature.Register(feature.Feature{
  318. Flags: fs,
  319. Initialize: func() {
  320. // Check for deprecated experimental flags and warn users
  321. if experimentalEnableCache {
  322. logger.Info("DEPRECATION WARNING: --experimental-enable-vault-token-cache is deprecated. Please use --enable-vault-token-cache instead. This flag will be removed in a future release.")
  323. enableCache = true
  324. }
  325. if experimentalCacheSize > 0 {
  326. logger.Info("DEPRECATION WARNING: --experimental-vault-token-cache-size is deprecated. Please use --vault-token-cache-size instead. This flag will be removed in a future release.")
  327. vaultTokenCacheSize = experimentalCacheSize
  328. }
  329. initCache(vaultTokenCacheSize)
  330. },
  331. })
  332. }
  333. // NewProvider creates a new Provider instance.
  334. func NewProvider() esv1.Provider {
  335. return &Provider{
  336. NewVaultClient: NewVaultClient,
  337. }
  338. }
  339. // ProviderSpec returns the provider specification for registration.
  340. func ProviderSpec() *esv1.SecretStoreProvider {
  341. return &esv1.SecretStoreProvider{
  342. Vault: &esv1.VaultProvider{},
  343. }
  344. }
  345. // MaintenanceStatus returns the maintenance status of the provider.
  346. func MaintenanceStatus() esv1.MaintenanceStatus {
  347. return esv1.MaintenanceStatusMaintained
  348. }