auth_iam.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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 vault
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "github.com/aws/aws-sdk-go/aws"
  20. "github.com/aws/aws-sdk-go/aws/credentials"
  21. "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
  22. "github.com/golang-jwt/jwt/v5"
  23. authaws "github.com/hashicorp/vault/api/auth/aws"
  24. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  25. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  26. "github.com/external-secrets/external-secrets/pkg/constants"
  27. "github.com/external-secrets/external-secrets/pkg/metrics"
  28. vaultiamauth "github.com/external-secrets/external-secrets/pkg/provider/vault/iamauth"
  29. "github.com/external-secrets/external-secrets/pkg/provider/vault/util"
  30. )
  31. const (
  32. defaultAWSRegion = "us-east-1"
  33. defaultAWSAuthMountPath = "aws"
  34. errNoAWSAuthMethodFound = "no AWS authentication method found: expected either IRSA or Pod Identity"
  35. errIrsaTokenFileNotFoundOnPod = "web identity token file not found at %s location: %w"
  36. errIrsaTokenFileNotReadable = "could not read the web identity token from the file %s: %w"
  37. errIrsaTokenNotValidJWT = "could not parse web identity token available at %s. not a valid jwt?: %w"
  38. errIrsaTokenNotValidClaims = "could not find pod identity info on token %s"
  39. )
  40. func setIamAuthToken(ctx context.Context, v *client, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) (bool, error) {
  41. iamAuth := v.store.Auth.Iam
  42. isClusterKind := v.storeKind == esv1.ClusterSecretStoreKind
  43. if iamAuth != nil {
  44. err := v.requestTokenWithIamAuth(ctx, iamAuth, isClusterKind, v.kube, v.namespace, jwtProvider, assumeRoler)
  45. if err != nil {
  46. return true, err
  47. }
  48. return true, nil
  49. }
  50. return false, nil
  51. }
  52. func (c *client) requestTokenWithIamAuth(ctx context.Context, iamAuth *esv1.VaultIamAuth, isClusterKind bool, k kclient.Client, n string, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) error {
  53. jwtAuth := iamAuth.JWTAuth
  54. secretRefAuth := iamAuth.SecretRef
  55. regionAWS := c.getRegionOrDefault(iamAuth.Region)
  56. awsAuthMountPath := c.getAuthMountPathOrDefault(iamAuth.Path)
  57. var creds *credentials.Credentials
  58. var err error
  59. if jwtAuth != nil { // use credentials from a sa explicitly defined and referenced. Highest preference is given to this method/configuration.
  60. creds, err = vaultiamauth.CredsFromServiceAccount(ctx, *iamAuth, regionAWS, isClusterKind, k, n, jwtProvider)
  61. if err != nil {
  62. return err
  63. }
  64. } else if secretRefAuth != nil { // if jwtAuth is not defined, check if secretRef is defined. Second preference.
  65. logger.V(1).Info("using credentials from secretRef")
  66. creds, err = vaultiamauth.CredsFromSecretRef(ctx, *iamAuth, c.storeKind, k, n)
  67. if err != nil {
  68. return err
  69. }
  70. }
  71. // Neither of jwtAuth or secretRefAuth defined. Last preference.
  72. // Default to controller pod's identity
  73. if jwtAuth == nil && secretRefAuth == nil {
  74. creds, err = c.getControllerPodCredentials(ctx, regionAWS, k, jwtProvider)
  75. if err != nil {
  76. return err
  77. }
  78. }
  79. config := aws.NewConfig().WithEndpointResolver(vaultiamauth.ResolveEndpoint())
  80. if creds != nil {
  81. config.WithCredentials(creds)
  82. }
  83. if regionAWS != "" {
  84. config.WithRegion(regionAWS)
  85. }
  86. sess, err := vaultiamauth.GetAWSSession(config)
  87. if err != nil {
  88. return err
  89. }
  90. if iamAuth.AWSIAMRole != "" {
  91. stsclient := assumeRoler(sess)
  92. if iamAuth.ExternalID != "" {
  93. var setExternalID = func(p *stscreds.AssumeRoleProvider) {
  94. p.ExternalID = aws.String(iamAuth.ExternalID)
  95. }
  96. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole, setExternalID))
  97. } else {
  98. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole))
  99. }
  100. }
  101. getCreds, err := sess.Config.Credentials.Get()
  102. if err != nil {
  103. return err
  104. }
  105. // Set environment variables. These would be fetched by Login
  106. _ = os.Setenv("AWS_ACCESS_KEY_ID", getCreds.AccessKeyID)
  107. _ = os.Setenv("AWS_SECRET_ACCESS_KEY", getCreds.SecretAccessKey)
  108. _ = os.Setenv("AWS_SESSION_TOKEN", getCreds.SessionToken)
  109. var awsAuthClient *authaws.AWSAuth
  110. if iamAuth.VaultAWSIAMServerID != "" {
  111. awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath), authaws.WithIAMServerIDHeader(iamAuth.VaultAWSIAMServerID))
  112. if err != nil {
  113. return err
  114. }
  115. } else {
  116. awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath))
  117. if err != nil {
  118. return err
  119. }
  120. }
  121. _, err = c.auth.Login(ctx, awsAuthClient)
  122. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
  123. if err != nil {
  124. return err
  125. }
  126. return nil
  127. }
  128. func (c *client) getRegionOrDefault(region string) string {
  129. if region != "" {
  130. return region
  131. }
  132. return defaultAWSRegion
  133. }
  134. func (c *client) getAuthMountPathOrDefault(path string) string {
  135. if path != "" {
  136. return path
  137. }
  138. return defaultAWSAuthMountPath
  139. }
  140. func (c *client) getControllerPodCredentials(ctx context.Context, region string, k kclient.Client, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) {
  141. // First try IRSA (Web Identity Token) - checking if controller pod's service account is IRSA enabled
  142. tokenFile := os.Getenv(vaultiamauth.AWSWebIdentityTokenFileEnvVar)
  143. if tokenFile != "" {
  144. logger.V(1).Info("using IRSA token for authentication")
  145. return c.getCredsFromIRSAToken(ctx, tokenFile, region, k, jwtProvider)
  146. }
  147. // Check for Pod Identity environment variables.
  148. podIdentityURI := os.Getenv(vaultiamauth.AWSContainerCredentialsFullURIEnvVar)
  149. if podIdentityURI != "" {
  150. logger.V(1).Info("using Pod Identity for authentication")
  151. // Return nil to let AWS SDK v1 container credential provider handle Pod Identity automatically
  152. return nil, nil
  153. }
  154. // No IRSA or Pod Identity found.
  155. return nil, errors.New(errNoAWSAuthMethodFound)
  156. }
  157. func (c *client) getCredsFromIRSAToken(ctx context.Context, tokenFile, region string, k kclient.Client, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) {
  158. // IRSA enabled service account, let's check that the jwt token filemount and file exists
  159. if _, err := os.Stat(filepath.Clean(tokenFile)); err != nil {
  160. return nil, fmt.Errorf(errIrsaTokenFileNotFoundOnPod, tokenFile, err)
  161. }
  162. // everything looks good so far, let's fetch the jwt token from AWS_WEB_IDENTITY_TOKEN_FILE
  163. jwtByte, err := os.ReadFile(filepath.Clean(tokenFile))
  164. if err != nil {
  165. return nil, fmt.Errorf(errIrsaTokenFileNotReadable, tokenFile, err)
  166. }
  167. // let's parse the jwt token
  168. parser := jwt.NewParser(jwt.WithoutClaimsValidation())
  169. token, _, err := parser.ParseUnverified(string(jwtByte), jwt.MapClaims{})
  170. if err != nil {
  171. return nil, fmt.Errorf(errIrsaTokenNotValidJWT, tokenFile, err) // JWT token parser error
  172. }
  173. var ns string
  174. var sa string
  175. // let's fetch the namespace and serviceaccount from parsed jwt token
  176. claims, ok := token.Claims.(jwt.MapClaims)
  177. if !ok {
  178. return nil, fmt.Errorf(errIrsaTokenNotValidClaims, tokenFile)
  179. }
  180. k8s, ok := claims["kubernetes.io"].(map[string]any)
  181. if !ok {
  182. return nil, fmt.Errorf(errIrsaTokenNotValidClaims, tokenFile)
  183. }
  184. ns, ok = k8s["namespace"].(string)
  185. if !ok {
  186. return nil, fmt.Errorf(errIrsaTokenNotValidClaims, tokenFile)
  187. }
  188. saMap, ok := k8s["serviceaccount"].(map[string]any)
  189. if !ok {
  190. return nil, fmt.Errorf(errIrsaTokenNotValidClaims, tokenFile)
  191. }
  192. sa, ok = saMap["name"].(string)
  193. if !ok {
  194. return nil, fmt.Errorf(errIrsaTokenNotValidClaims, tokenFile)
  195. }
  196. return vaultiamauth.CredsFromControllerServiceAccount(ctx, sa, ns, region, k, jwtProvider)
  197. }