auth.go 13 KB

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