auth.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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 auth provides authentication functionality for the AWS provider, handling
  14. // various authentication methods including static credentials, IAM roles,
  15. // and web identity tokens.
  16. package auth
  17. import (
  18. "context"
  19. "fmt"
  20. "github.com/aws/aws-sdk-go-v2/aws"
  21. "github.com/aws/aws-sdk-go-v2/config"
  22. "github.com/aws/aws-sdk-go-v2/credentials"
  23. "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
  24. "github.com/aws/aws-sdk-go-v2/service/sts"
  25. stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
  26. v1 "k8s.io/api/core/v1"
  27. "k8s.io/apimachinery/pkg/types"
  28. "k8s.io/client-go/kubernetes"
  29. ctrl "sigs.k8s.io/controller-runtime"
  30. "sigs.k8s.io/controller-runtime/pkg/client"
  31. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  32. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  33. awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
  34. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  35. )
  36. // Config contains configuration to create a new AWS provider.
  37. type Config struct {
  38. AssumeRole string
  39. Region string
  40. APIRetries int
  41. }
  42. var (
  43. log = ctrl.Log.WithName("provider").WithName("aws")
  44. )
  45. const (
  46. roleARNAnnotation = "eks.amazonaws.com/role-arn"
  47. audienceAnnotation = "eks.amazonaws.com/audience"
  48. defaultTokenAudience = "sts.amazonaws.com"
  49. errFetchAKIDSecret = "could not fetch accessKeyID secret: %w"
  50. errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
  51. errFetchSTSecret = "could not fetch SessionToken secret: %w"
  52. )
  53. // Opts define options for New function.
  54. type Opts struct {
  55. Store esv1.GenericStore
  56. Kube client.Client
  57. Namespace string
  58. AssumeRoler STSProvider
  59. JWTProvider jwtProviderFactory
  60. }
  61. // New creates a new aws config based on the provided store
  62. // it uses the following authentication mechanisms in order:
  63. // * service-account token authentication via AssumeRoleWithWebIdentity
  64. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  65. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  66. func New(ctx context.Context, opts Opts) (*aws.Config, error) {
  67. prov, err := awsutil.GetAWSProvider(opts.Store)
  68. if err != nil {
  69. return nil, err
  70. }
  71. var credsProvider aws.CredentialsProvider
  72. isClusterKind := opts.Store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind
  73. credsProvider, err = constructCredsProvider(ctx, prov, isClusterKind, opts)
  74. if err != nil {
  75. return nil, err
  76. }
  77. // global endpoint resolver is deprecated, should we EndpointResolverV2 field on service client options
  78. var loadCfgOpts []func(*config.LoadOptions) error
  79. if credsProvider != nil {
  80. loadCfgOpts = append(loadCfgOpts, config.WithCredentialsProvider(credsProvider))
  81. }
  82. if prov.Region != "" {
  83. loadCfgOpts = append(loadCfgOpts, config.WithRegion(prov.Region))
  84. }
  85. return createConfiguration(prov, opts.AssumeRoler, loadCfgOpts)
  86. }
  87. func createConfiguration(prov *esv1.AWSProvider, assumeRoler STSProvider, loadCfgOpts []func(*config.LoadOptions) error) (*aws.Config, error) {
  88. cfg, err := config.LoadDefaultConfig(context.TODO(), loadCfgOpts...)
  89. if err != nil {
  90. return nil, err
  91. }
  92. for _, aRole := range prov.AdditionalRoles {
  93. stsclient := assumeRoler(&cfg)
  94. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, aRole)
  95. }
  96. sessExtID := prov.ExternalID
  97. sessTransitiveTagKeys := prov.TransitiveTagKeys
  98. sessTags := make([]stsTypes.Tag, len(prov.SessionTags))
  99. for i, tag := range prov.SessionTags {
  100. sessTags[i] = stsTypes.Tag{
  101. Key: aws.String(tag.Key),
  102. Value: aws.String(tag.Value),
  103. }
  104. }
  105. if prov.Role != "" {
  106. stsclient := assumeRoler(&cfg)
  107. if sessExtID != "" || sessTags != nil {
  108. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role, setAssumeRoleOptionFn(sessExtID, sessTags, sessTransitiveTagKeys))
  109. } else {
  110. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role)
  111. }
  112. }
  113. log.Info("using aws config", "region", cfg.Region, "external id", sessExtID, "credentials", cfg.Credentials)
  114. return &cfg, nil
  115. }
  116. func setAssumeRoleOptionFn(sessExtID string, sessTags []stsTypes.Tag, sessTransitiveTagKeys []string) func(p *stscreds.AssumeRoleOptions) {
  117. return func(p *stscreds.AssumeRoleOptions) {
  118. if sessExtID != "" {
  119. p.ExternalID = aws.String(sessExtID)
  120. }
  121. if sessTags != nil {
  122. p.Tags = sessTags
  123. if len(sessTransitiveTagKeys) > 0 {
  124. p.TransitiveTagKeys = sessTransitiveTagKeys
  125. }
  126. }
  127. }
  128. }
  129. func constructCredsProvider(ctx context.Context, prov *esv1.AWSProvider, isClusterKind bool, opts Opts) (aws.CredentialsProvider, error) {
  130. switch {
  131. case prov.Auth.JWTAuth != nil:
  132. return credsFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, opts.Kube, opts.Namespace, opts.JWTProvider)
  133. case prov.Auth.SecretRef != nil:
  134. log.V(1).Info("using credentials from secretRef")
  135. return credsFromSecretRef(ctx, prov.Auth, opts.Store.GetKind(), opts.Kube, opts.Namespace)
  136. default:
  137. return nil, nil
  138. }
  139. }
  140. // NewGeneratorSession creates a new aws session based on the provided store
  141. // it uses the following authentication mechanisms in order:
  142. // * service-account token authentication via AssumeRoleWithWebIdentity
  143. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  144. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  145. func NewGeneratorSession(
  146. ctx context.Context,
  147. auth esv1.AWSAuth,
  148. role, region string,
  149. kube client.Client,
  150. namespace string,
  151. assumeRoler STSProvider,
  152. jwtProvider jwtProviderFactory,
  153. ) (*aws.Config, error) {
  154. var (
  155. credsProvider aws.CredentialsProvider
  156. err error
  157. )
  158. // use credentials via service account token
  159. jwtAuth := auth.JWTAuth
  160. if jwtAuth != nil {
  161. credsProvider, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
  162. if err != nil {
  163. return nil, err
  164. }
  165. }
  166. // use credentials from secretRef
  167. secretRef := auth.SecretRef
  168. if secretRef != nil {
  169. log.V(1).Info("using credentials from secretRef")
  170. credsProvider, err = credsFromSecretRef(ctx, auth, "", kube, namespace)
  171. if err != nil {
  172. return nil, err
  173. }
  174. }
  175. awscfg, err := config.LoadDefaultConfig(ctx)
  176. if err != nil {
  177. return nil, err
  178. }
  179. if credsProvider != nil {
  180. awscfg.Credentials = credsProvider
  181. }
  182. if region != "" {
  183. awscfg.Region = region
  184. }
  185. if role != "" {
  186. stsclient := assumeRoler(&awscfg)
  187. awscfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, role)
  188. }
  189. log.Info("using aws config", "region", awscfg.Region, "credentials", awscfg.Credentials)
  190. return &awscfg, nil
  191. }
  192. // credsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  193. // construct a aws.Credentials object
  194. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  195. // If the ClusterSecretStore defines a namespace it will take precedence.
  196. func credsFromSecretRef(ctx context.Context, auth esv1.AWSAuth, storeKind string, kube client.Client, namespace string) (aws.CredentialsProvider, error) {
  197. sak, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.SecretAccessKey)
  198. if err != nil {
  199. return nil, fmt.Errorf(errFetchSAKSecret, err)
  200. }
  201. aks, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.AccessKeyID)
  202. if err != nil {
  203. return nil, fmt.Errorf(errFetchAKIDSecret, err)
  204. }
  205. var sessionToken string
  206. if auth.SecretRef.SessionToken != nil {
  207. sessionToken, err = resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, auth.SecretRef.SessionToken)
  208. if err != nil {
  209. return nil, fmt.Errorf(errFetchSTSecret, err)
  210. }
  211. }
  212. var credsProvider aws.CredentialsProvider = credentials.NewStaticCredentialsProvider(aks, sak, sessionToken)
  213. return credsProvider, nil
  214. }
  215. // credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  216. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  217. // in the ServiceAccount annotation.
  218. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  219. // If the ClusterSecretStore defines the namespace it will take precedence.
  220. func credsFromServiceAccount(
  221. ctx context.Context,
  222. auth esv1.AWSAuth,
  223. region string,
  224. isClusterKind bool,
  225. kube client.Client,
  226. namespace string,
  227. jwtProvider jwtProviderFactory,
  228. ) (aws.CredentialsProvider, error) {
  229. name := auth.JWTAuth.ServiceAccountRef.Name
  230. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  231. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  232. }
  233. sa := v1.ServiceAccount{}
  234. err := kube.Get(ctx, types.NamespacedName{
  235. Name: name,
  236. Namespace: namespace,
  237. }, &sa)
  238. if err != nil {
  239. return nil, err
  240. }
  241. // the service account is expected to have a well-known annotation
  242. // this is used as input to assumeRoleWithWebIdentity
  243. roleArn := sa.Annotations[roleARNAnnotation]
  244. if roleArn == "" {
  245. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  246. }
  247. tokenAud := sa.Annotations[audienceAnnotation]
  248. if tokenAud == "" {
  249. tokenAud = defaultTokenAudience
  250. }
  251. audiences := []string{tokenAud}
  252. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  253. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  254. }
  255. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  256. if err != nil {
  257. return nil, err
  258. }
  259. log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  260. return jwtProv, nil
  261. }
  262. type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error)
  263. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  264. // controller-runtime/client does not support TokenRequest or other subresource APIs
  265. // so we need to construct our own client and use it to fetch tokens.
  266. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error) {
  267. cfg, err := ctrlcfg.GetConfig()
  268. if err != nil {
  269. return nil, err
  270. }
  271. clientset, err := kubernetes.NewForConfig(cfg)
  272. if err != nil {
  273. return nil, err
  274. }
  275. awscfg, err := config.LoadDefaultConfig(context.TODO(), config.WithAppID("external-secrets"),
  276. config.WithRegion(region),
  277. config.WithSharedConfigFiles([]string{}), // Disable shared config files:
  278. config.WithSharedCredentialsFiles([]string{}))
  279. if err != nil {
  280. return nil, err
  281. }
  282. tokenFetcher := authTokenFetcher{
  283. Namespace: namespace,
  284. Audiences: aud,
  285. ServiceAccount: name,
  286. k8sClient: clientset.CoreV1(),
  287. }
  288. stsClient := sts.NewFromConfig(awscfg, func(o *sts.Options) {
  289. o.EndpointResolverV2 = customEndpointResolver{}
  290. })
  291. return stscreds.NewWebIdentityRoleProvider(
  292. stsClient, roleArn, tokenFetcher, func(opts *stscreds.WebIdentityRoleOptions) {
  293. opts.RoleSessionName = "external-secrets-provider-aws"
  294. }), nil
  295. }
  296. // STSprovider defines the interface for interacting with AWS STS API operations.
  297. // This allows for mocking STS operations during testing.
  298. type STSprovider interface {
  299. AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
  300. AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error)
  301. AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error)
  302. AssumeRoot(ctx context.Context, params *sts.AssumeRootInput, optFns ...func(*sts.Options)) (*sts.AssumeRootOutput, error)
  303. DecodeAuthorizationMessage(ctx context.Context, params *sts.DecodeAuthorizationMessageInput, optFns ...func(*sts.Options)) (*sts.DecodeAuthorizationMessageOutput, error)
  304. }
  305. // STSProvider is a function type that returns an STSprovider implementation.
  306. // Used to inject custom or mock STS clients.
  307. type STSProvider func(*aws.Config) STSprovider
  308. // DefaultSTSProvider creates and returns a new STS client from the provided AWS config.
  309. func DefaultSTSProvider(cfg *aws.Config) STSprovider {
  310. stsClient := sts.NewFromConfig(*cfg, func(o *sts.Options) {
  311. o.EndpointResolverV2 = customEndpointResolver{}
  312. })
  313. return stsClient
  314. }