iamauth.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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 iamauth provides utilities for AWS IAM authentication using Kubernetes Service Accounts.
  14. // Mostly sourced from ~/external-secrets/pkg/provider/aws/auth
  15. package iamauth
  16. import (
  17. "context"
  18. "fmt"
  19. "os"
  20. "github.com/aws/aws-sdk-go/aws"
  21. "github.com/aws/aws-sdk-go/aws/credentials"
  22. "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
  23. "github.com/aws/aws-sdk-go/aws/defaults"
  24. "github.com/aws/aws-sdk-go/aws/endpoints"
  25. "github.com/aws/aws-sdk-go/aws/request"
  26. "github.com/aws/aws-sdk-go/aws/session"
  27. "github.com/aws/aws-sdk-go/service/sts"
  28. "github.com/aws/aws-sdk-go/service/sts/stsiface"
  29. authv1 "k8s.io/api/authentication/v1"
  30. v1 "k8s.io/api/core/v1"
  31. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  32. "k8s.io/apimachinery/pkg/types"
  33. "k8s.io/client-go/kubernetes"
  34. k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  35. ctrl "sigs.k8s.io/controller-runtime"
  36. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  37. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  38. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  39. "github.com/external-secrets/external-secrets/pkg/esutils/resolvers"
  40. "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  41. "github.com/external-secrets/external-secrets/pkg/provider/vault/util"
  42. )
  43. var (
  44. logger = ctrl.Log.WithName("provider").WithName("vault")
  45. )
  46. const (
  47. roleARNAnnotation = "eks.amazonaws.com/role-arn"
  48. audienceAnnotation = "eks.amazonaws.com/audience"
  49. defaultTokenAudience = "sts.amazonaws.com"
  50. // STSEndpointEnv is the environment variable that can be used to override the default STS endpoint.
  51. STSEndpointEnv = "AWS_STS_ENDPOINT"
  52. // AWSWebIdentityTokenFileEnvVar is the environment variable that points to the service account token file.
  53. AWSWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
  54. // AWSContainerCredentialsFullURIEnvVar is the environment variable that points to the full credentials URI for ECS tasks.
  55. AWSContainerCredentialsFullURIEnvVar = "AWS_CONTAINER_CREDENTIALS_FULL_URI"
  56. )
  57. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  58. // controller-runtime/client does not support TokenRequest or other subresource APIs
  59. // so we need to construct our own client and use it to fetch tokens.
  60. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) {
  61. cfg, err := ctrlcfg.GetConfig()
  62. if err != nil {
  63. return nil, err
  64. }
  65. clientset, err := kubernetes.NewForConfig(cfg)
  66. if err != nil {
  67. return nil, err
  68. }
  69. handlers := defaults.Handlers()
  70. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  71. awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  72. if region != "" {
  73. awscfg.WithRegion(region)
  74. }
  75. sess, err := session.NewSessionWithOptions(session.Options{
  76. Config: *awscfg,
  77. SharedConfigState: session.SharedConfigDisable,
  78. Handlers: handlers,
  79. })
  80. if err != nil {
  81. return nil, awsutil.SanitizeErr(err)
  82. }
  83. tokenFetcher := &authTokenFetcher{
  84. Namespace: namespace,
  85. Audiences: aud,
  86. ServiceAccount: name,
  87. k8sClient: clientset.CoreV1(),
  88. }
  89. return stscreds.NewWebIdentityRoleProviderWithOptions(
  90. sts.New(sess), roleArn, "external-secrets-provider-vault", tokenFetcher), nil
  91. }
  92. // ResolveEndpoint returns a ResolverFunc with
  93. // customizable endpoints.
  94. func ResolveEndpoint() endpoints.ResolverFunc {
  95. customEndpoints := make(map[string]string)
  96. if v := os.Getenv(STSEndpointEnv); v != "" {
  97. customEndpoints["sts"] = v
  98. }
  99. return ResolveEndpointWithServiceMap(customEndpoints)
  100. }
  101. // ResolveEndpointWithServiceMap returns a ResolverFunc with customizable endpoints for specific services.
  102. func ResolveEndpointWithServiceMap(customEndpoints map[string]string) endpoints.ResolverFunc {
  103. defaultResolver := endpoints.DefaultResolver()
  104. return func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
  105. if ep, ok := customEndpoints[service]; ok {
  106. return endpoints.ResolvedEndpoint{
  107. URL: ep,
  108. }, nil
  109. }
  110. return defaultResolver.EndpointFor(service, region, opts...)
  111. }
  112. }
  113. // mostly taken from:
  114. // https://github.com/aws/secrets-store-csi-driver-provider-aws/blob/main/auth/auth.go#L140-L145
  115. type authTokenFetcher struct {
  116. Namespace string
  117. // Audience is the token aud claim
  118. // which is verified by the aws oidc provider
  119. // see: https://github.com/external-secrets/external-secrets/issues/1251#issuecomment-1161745849
  120. Audiences []string
  121. ServiceAccount string
  122. k8sClient k8scorev1.CoreV1Interface
  123. }
  124. // FetchToken satisfies the stscreds.TokenFetcher interface
  125. // it is used to generate service account tokens which are consumed by the aws sdk.
  126. func (p authTokenFetcher) FetchToken(ctx credentials.Context) ([]byte, error) {
  127. logger.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount)
  128. tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(ctx, p.ServiceAccount, &authv1.TokenRequest{
  129. Spec: authv1.TokenRequestSpec{
  130. Audiences: p.Audiences,
  131. },
  132. }, metav1.CreateOptions{})
  133. if err != nil {
  134. return nil, fmt.Errorf("error creating service account token: %w", err)
  135. }
  136. return []byte(tokRsp.Status.Token), nil
  137. }
  138. // CredsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  139. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  140. // in the ServiceAccount annotation.
  141. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  142. // If the ClusterSecretStore defines the namespace it will take precedence.
  143. func CredsFromServiceAccount(ctx context.Context, auth esv1.VaultIamAuth, region string, isClusterKind bool, kube kclient.Client, namespace string, jwtProvider vaultutil.JwtProviderFactory) (*credentials.Credentials, error) {
  144. name := auth.JWTAuth.ServiceAccountRef.Name
  145. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  146. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  147. }
  148. sa := v1.ServiceAccount{}
  149. err := kube.Get(ctx, types.NamespacedName{
  150. Name: name,
  151. Namespace: namespace,
  152. }, &sa)
  153. if err != nil {
  154. return nil, err
  155. }
  156. // the service account is expected to have a well-known annotation
  157. // this is used as input to assumeRoleWithWebIdentity
  158. roleArn := sa.Annotations[roleARNAnnotation]
  159. if roleArn == "" {
  160. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  161. }
  162. tokenAud := sa.Annotations[audienceAnnotation]
  163. if tokenAud == "" {
  164. tokenAud = defaultTokenAudience
  165. }
  166. audiences := []string{tokenAud}
  167. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  168. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  169. }
  170. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  171. if err != nil {
  172. return nil, err
  173. }
  174. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  175. return credentials.NewCredentials(jwtProv), nil
  176. }
  177. // CredsFromControllerServiceAccount uses a Kubernetes Service Account to acquire temporary
  178. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  179. // in the ServiceAccount annotation.
  180. // The namespace of the controller service account is used.
  181. func CredsFromControllerServiceAccount(ctx context.Context, saName, ns, region string, kube kclient.Client, jwtProvider vaultutil.JwtProviderFactory) (*credentials.Credentials, error) {
  182. sa := v1.ServiceAccount{}
  183. err := kube.Get(ctx, types.NamespacedName{
  184. Name: saName,
  185. Namespace: ns,
  186. }, &sa)
  187. if err != nil {
  188. return nil, err
  189. }
  190. // the service account is expected to have a well-known annotation
  191. // this is used as input to assumeRoleWithWebIdentity
  192. roleArn := sa.Annotations[roleARNAnnotation]
  193. if roleArn == "" {
  194. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", saName, ns)
  195. }
  196. tokenAud := sa.Annotations[audienceAnnotation]
  197. if tokenAud == "" {
  198. tokenAud = defaultTokenAudience
  199. }
  200. audiences := []string{tokenAud}
  201. jwtProv, err := jwtProvider(saName, ns, roleArn, audiences, region)
  202. if err != nil {
  203. return nil, err
  204. }
  205. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  206. return credentials.NewCredentials(jwtProv), nil
  207. }
  208. // CredsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  209. // construct a aws.Credentials object
  210. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  211. // If the ClusterSecretStore defines a namespace it will take precedence.
  212. func CredsFromSecretRef(ctx context.Context, auth esv1.VaultIamAuth, storeKind string, kube kclient.Client, namespace string) (*credentials.Credentials, error) {
  213. akid, err := resolvers.SecretKeyRef(
  214. ctx,
  215. kube,
  216. storeKind,
  217. namespace,
  218. &auth.SecretRef.AccessKeyID,
  219. )
  220. if err != nil {
  221. return nil, err
  222. }
  223. sak, err := resolvers.SecretKeyRef(
  224. ctx,
  225. kube,
  226. storeKind,
  227. namespace,
  228. &auth.SecretRef.SecretAccessKey,
  229. )
  230. if err != nil {
  231. return nil, err
  232. }
  233. // session token is optional
  234. sessionToken, _ := resolvers.SecretKeyRef(
  235. ctx,
  236. kube,
  237. storeKind,
  238. namespace,
  239. auth.SecretRef.SessionToken,
  240. )
  241. return credentials.NewStaticCredentials(akid, sak, sessionToken), err
  242. }
  243. // STSProvider is a function type that returns an stsiface.STSAPI implementation.
  244. type STSProvider func(*session.Session) stsiface.STSAPI
  245. // DefaultSTSProvider returns the default sts client which implements stsiface.STSAPI.
  246. func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
  247. return sts.New(sess)
  248. }
  249. // GetAWSSession returns the aws session or an error.
  250. func GetAWSSession(config *aws.Config) (*session.Session, error) {
  251. handlers := defaults.Handlers()
  252. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  253. sess, err := session.NewSessionWithOptions(session.Options{
  254. Config: *config,
  255. Handlers: handlers,
  256. SharedConfigState: session.SharedConfigDisable,
  257. })
  258. if err != nil {
  259. return nil, err
  260. }
  261. return sess, nil
  262. }