auth.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. "encoding/json"
  17. "errors"
  18. "fmt"
  19. vault "github.com/hashicorp/vault/api"
  20. authv1 "k8s.io/api/authentication/v1"
  21. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  22. typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  25. vaultiamauth "github.com/external-secrets/external-secrets/providers/v1/vault/iamauth"
  26. vaultutil "github.com/external-secrets/external-secrets/providers/v1/vault/util"
  27. "github.com/external-secrets/external-secrets/runtime/constants"
  28. "github.com/external-secrets/external-secrets/runtime/metrics"
  29. )
  30. const (
  31. errAuthFormat = "cannot initialize Vault client: no valid auth method specified"
  32. errVaultToken = "cannot parse Vault authentication token: %w"
  33. errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
  34. errVaultRevokeToken = "error while revoking token: %w"
  35. )
  36. // setAuth gets a new token using the configured mechanism.
  37. // If there's already a valid token, does nothing.
  38. func (c *client) setAuth(ctx context.Context, cfg *vault.Config) error {
  39. if c.store.Auth == nil {
  40. return nil
  41. }
  42. if c.store.Namespace != nil { // set namespace before checking the need for AuthNamespace
  43. c.client.SetNamespace(*c.store.Namespace)
  44. }
  45. // Switch to auth namespace if different from the provider namespace
  46. restoreNamespace := c.useAuthNamespace(ctx)
  47. defer restoreNamespace()
  48. tokenExists := false
  49. var err error
  50. if c.client.Token() != "" {
  51. tokenExists, err = checkToken(ctx, c.token)
  52. }
  53. if tokenExists {
  54. c.log.V(1).Info("Re-using existing token")
  55. return err
  56. }
  57. tokenExists, err = setSecretKeyToken(ctx, c)
  58. if tokenExists {
  59. c.log.V(1).Info("Set token from secret")
  60. return err
  61. }
  62. tokenExists, err = setAppRoleToken(ctx, c)
  63. if tokenExists {
  64. c.log.V(1).Info("Retrieved new token using AppRole auth")
  65. return err
  66. }
  67. tokenExists, err = setKubernetesAuthToken(ctx, c)
  68. if tokenExists {
  69. c.log.V(1).Info("Retrieved new token using Kubernetes auth")
  70. return err
  71. }
  72. tokenExists, err = setLdapAuthToken(ctx, c)
  73. if tokenExists {
  74. c.log.V(1).Info("Retrieved new token using LDAP auth")
  75. return err
  76. }
  77. tokenExists, err = setUserPassAuthToken(ctx, c)
  78. if tokenExists {
  79. c.log.V(1).Info("Retrieved new token using userPass auth")
  80. return err
  81. }
  82. tokenExists, err = setJwtAuthToken(ctx, c)
  83. if tokenExists {
  84. c.log.V(1).Info("Retrieved new token using JWT auth")
  85. return err
  86. }
  87. tokenExists, err = setCertAuthToken(ctx, c, cfg)
  88. if tokenExists {
  89. c.log.V(1).Info("Retrieved new token using certificate auth")
  90. return err
  91. }
  92. tokenExists, err = setIamAuthToken(ctx, c, vaultiamauth.DefaultJWTProvider, vaultiamauth.DefaultSTSProvider)
  93. if tokenExists {
  94. c.log.V(1).Info("Retrieved new token using IAM auth")
  95. return err
  96. }
  97. tokenExists, err = setGcpAuthToken(ctx, c)
  98. if tokenExists {
  99. c.log.V(1).Info("Retrieved new token using GCP auth")
  100. return err
  101. }
  102. return errors.New(errAuthFormat)
  103. }
  104. func createServiceAccountToken(
  105. ctx context.Context,
  106. corev1Client typedcorev1.CoreV1Interface,
  107. storeKind string,
  108. namespace string,
  109. serviceAccountRef esmeta.ServiceAccountSelector,
  110. additionalAud []string,
  111. expirationSeconds int64) (string, error) {
  112. audiences := serviceAccountRef.Audiences
  113. if len(additionalAud) > 0 {
  114. audiences = append(audiences, additionalAud...)
  115. }
  116. tokenRequest := &authv1.TokenRequest{
  117. ObjectMeta: metav1.ObjectMeta{
  118. Namespace: namespace,
  119. },
  120. Spec: authv1.TokenRequestSpec{
  121. Audiences: audiences,
  122. ExpirationSeconds: &expirationSeconds,
  123. },
  124. }
  125. if (storeKind == esv1.ClusterSecretStoreKind) &&
  126. (serviceAccountRef.Namespace != nil) {
  127. tokenRequest.Namespace = *serviceAccountRef.Namespace
  128. }
  129. tokenResponse, err := corev1Client.ServiceAccounts(tokenRequest.Namespace).
  130. CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
  131. if err != nil {
  132. return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
  133. }
  134. return tokenResponse.Status.Token, nil
  135. }
  136. // checkToken does a lookup and checks if the provided token exists.
  137. func checkToken(ctx context.Context, token vaultutil.Token) (bool, error) {
  138. // https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self
  139. resp, err := token.LookupSelfWithContext(ctx)
  140. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLookupSelf, err)
  141. if err != nil {
  142. return false, err
  143. }
  144. // LookupSelfWithContext() calls ParseSecret(), which has several places
  145. // that return no data and no error, including when a token is expired.
  146. if resp == nil {
  147. return false, errors.New("no response nor error for token lookup")
  148. }
  149. t, ok := resp.Data["type"]
  150. if !ok {
  151. return false, errors.New("could not assert token type")
  152. }
  153. tokenType := t.(string)
  154. if tokenType == "batch" {
  155. return false, nil
  156. }
  157. ttl, ok := resp.Data["ttl"]
  158. if !ok {
  159. return false, errors.New("no TTL found in response")
  160. }
  161. ttlInt, err := ttl.(json.Number).Int64()
  162. if err != nil {
  163. return false, fmt.Errorf("invalid token TTL: %v: %w", ttl, err)
  164. }
  165. expireTime, ok := resp.Data["expire_time"]
  166. if !ok {
  167. return false, errors.New("no expiration time found in response")
  168. }
  169. if ttlInt < 60 && expireTime != nil {
  170. // Treat expirable tokens that are about to expire as already expired.
  171. // This ensures that the token won't expire in between this check and
  172. // performing the actual operation.
  173. return false, nil
  174. }
  175. return true, nil
  176. }
  177. func revokeTokenIfValid(ctx context.Context, client vaultutil.Client) error {
  178. valid, err := checkToken(ctx, client.AuthToken())
  179. if err != nil {
  180. return fmt.Errorf(errVaultRevokeToken, err)
  181. }
  182. if valid {
  183. err = client.AuthToken().RevokeSelfWithContext(ctx, client.Token())
  184. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultRevokeSelf, err)
  185. if err != nil {
  186. return fmt.Errorf(errVaultRevokeToken, err)
  187. }
  188. client.ClearToken()
  189. }
  190. return nil
  191. }
  192. func (c *client) useAuthNamespace(_ context.Context) func() {
  193. ns := ""
  194. if c.store != nil && c.store.Namespace != nil {
  195. ns = *c.store.Namespace
  196. }
  197. if c.store.Auth != nil && c.store.Auth.Namespace != nil {
  198. // Different Auth Vault Namespace than Secret Vault Namespace
  199. // Switch namespaces then switch back at the end
  200. if c.store.Auth.Namespace != nil && *c.store.Auth.Namespace != ns {
  201. c.log.V(1).Info("Using namespace=%s for the vault login", *c.store.Auth.Namespace)
  202. c.client.SetNamespace(*c.store.Auth.Namespace)
  203. // use this as a defer to reset the namespace
  204. return func() {
  205. c.log.V(1).Info("Restoring client namespace to namespace=%s", ns)
  206. c.client.SetNamespace(ns)
  207. }
  208. }
  209. }
  210. return func() {
  211. // no-op
  212. }
  213. }