auth.go 13 KB

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