auth.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 auth
  13. import (
  14. "context"
  15. "fmt"
  16. "github.com/aws/aws-sdk-go/aws"
  17. "github.com/aws/aws-sdk-go/aws/credentials"
  18. "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
  19. "github.com/aws/aws-sdk-go/aws/defaults"
  20. "github.com/aws/aws-sdk-go/aws/request"
  21. "github.com/aws/aws-sdk-go/aws/session"
  22. "github.com/aws/aws-sdk-go/service/sts"
  23. "github.com/aws/aws-sdk-go/service/sts/stsiface"
  24. "github.com/spf13/pflag"
  25. v1 "k8s.io/api/core/v1"
  26. "k8s.io/apimachinery/pkg/types"
  27. "k8s.io/client-go/kubernetes"
  28. ctrl "sigs.k8s.io/controller-runtime"
  29. "sigs.k8s.io/controller-runtime/pkg/client"
  30. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  31. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  32. "github.com/external-secrets/external-secrets/pkg/cache"
  33. "github.com/external-secrets/external-secrets/pkg/feature"
  34. "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  35. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  36. )
  37. // Config contains configuration to create a new AWS provider.
  38. type Config struct {
  39. AssumeRole string
  40. Region string
  41. APIRetries int
  42. }
  43. var (
  44. log = ctrl.Log.WithName("provider").WithName("aws")
  45. enableSessionCache bool
  46. sessionCache *cache.Cache[*session.Session]
  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, "Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request.")
  59. feature.Register(feature.Feature{
  60. Flags: fs,
  61. })
  62. sessionCache = cache.Must[*session.Session](1024, nil)
  63. }
  64. // New creates a new aws session based on the provided store
  65. // it uses the following authentication mechanisms in order:
  66. // * service-account token authentication via AssumeRoleWithWebIdentity
  67. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  68. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  69. func New(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
  70. prov, err := util.GetAWSProvider(store)
  71. if err != nil {
  72. return nil, err
  73. }
  74. var creds *credentials.Credentials
  75. isClusterKind := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
  76. // use credentials via service account token
  77. jwtAuth := prov.Auth.JWTAuth
  78. if jwtAuth != nil {
  79. creds, err = credsFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, kube, namespace, jwtProvider)
  80. if err != nil {
  81. return nil, err
  82. }
  83. }
  84. // use credentials from secretRef
  85. secretRef := prov.Auth.SecretRef
  86. if secretRef != nil {
  87. log.V(1).Info("using credentials from secretRef")
  88. creds, err = credsFromSecretRef(ctx, prov.Auth, store.GetKind(), kube, namespace)
  89. if err != nil {
  90. return nil, err
  91. }
  92. }
  93. config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  94. if creds != nil {
  95. config.WithCredentials(creds)
  96. }
  97. if prov.Region != "" {
  98. config.WithRegion(prov.Region)
  99. }
  100. sess, err := getAWSSession(config, enableSessionCache, store.GetName(), store.GetTypeMeta().Kind, namespace, store.GetObjectMeta().ResourceVersion)
  101. if err != nil {
  102. return nil, err
  103. }
  104. for _, aRole := range prov.AdditionalRoles {
  105. stsclient := assumeRoler(sess)
  106. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, aRole))
  107. }
  108. sessExtID := prov.ExternalID
  109. sessTransitiveTagKeys := prov.TransitiveTagKeys
  110. sessTags := make([]*sts.Tag, len(prov.SessionTags))
  111. for i, tag := range prov.SessionTags {
  112. sessTags[i] = &sts.Tag{
  113. Key: aws.String(tag.Key),
  114. Value: aws.String(tag.Value),
  115. }
  116. }
  117. if prov.Role != "" {
  118. stsclient := assumeRoler(sess)
  119. if sessExtID != "" || sessTags != nil {
  120. var setAssumeRoleOptions = func(p *stscreds.AssumeRoleProvider) {
  121. if sessExtID != "" {
  122. p.ExternalID = aws.String(sessExtID)
  123. }
  124. if sessTags != nil {
  125. p.Tags = sessTags
  126. if len(sessTransitiveTagKeys) > 0 {
  127. p.TransitiveTagKeys = sessTransitiveTagKeys
  128. }
  129. }
  130. }
  131. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, prov.Role, setAssumeRoleOptions))
  132. } else {
  133. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, prov.Role))
  134. }
  135. }
  136. log.Info("using aws session", "region", *sess.Config.Region, "external id", sessExtID, "credentials", creds)
  137. return sess, nil
  138. }
  139. // NewSession creates a new aws session based on the provided store
  140. // it uses the following authentication mechanisms in order:
  141. // * service-account token authentication via AssumeRoleWithWebIdentity
  142. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  143. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  144. func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, region string, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
  145. var creds *credentials.Credentials
  146. var err error
  147. // use credentials via service account token
  148. jwtAuth := auth.JWTAuth
  149. if jwtAuth != nil {
  150. creds, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
  151. if err != nil {
  152. return nil, err
  153. }
  154. }
  155. // use credentials from secretRef
  156. secretRef := auth.SecretRef
  157. if secretRef != nil {
  158. log.V(1).Info("using credentials from secretRef")
  159. creds, err = credsFromSecretRef(ctx, auth, "", kube, namespace)
  160. if err != nil {
  161. return nil, err
  162. }
  163. }
  164. config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  165. if creds != nil {
  166. config.WithCredentials(creds)
  167. }
  168. if region != "" {
  169. config.WithRegion(region)
  170. }
  171. sess, err := getAWSSession(config, false, "", "", "", "")
  172. if err != nil {
  173. return nil, err
  174. }
  175. if role != "" {
  176. stsclient := assumeRoler(sess)
  177. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, role))
  178. }
  179. log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
  180. return sess, nil
  181. }
  182. // credsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  183. // construct a aws.Credentials object
  184. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  185. // If the ClusterSecretStore defines a namespace it will take precedence.
  186. func credsFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, storeKind string, kube client.Client, namespace string) (*credentials.Credentials, error) {
  187. sak, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.SecretAccessKey)
  188. if err != nil {
  189. return nil, fmt.Errorf(errFetchSAKSecret, err)
  190. }
  191. aks, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.AccessKeyID)
  192. if err != nil {
  193. return nil, fmt.Errorf(errFetchAKIDSecret, err)
  194. }
  195. var sessionToken string
  196. if auth.SecretRef.SessionToken != nil {
  197. sessionToken, err = resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, auth.SecretRef.SessionToken)
  198. if err != nil {
  199. return nil, fmt.Errorf(errFetchSTSecret, err)
  200. }
  201. }
  202. return credentials.NewStaticCredentials(aks, sak, sessionToken), err
  203. }
  204. // credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  205. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  206. // in the ServiceAccount annotation.
  207. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  208. // If the ClusterSecretStore defines the namespace it will take precedence.
  209. func credsFromServiceAccount(ctx context.Context, auth esv1beta1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) {
  210. name := auth.JWTAuth.ServiceAccountRef.Name
  211. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  212. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  213. }
  214. sa := v1.ServiceAccount{}
  215. err := kube.Get(ctx, types.NamespacedName{
  216. Name: name,
  217. Namespace: namespace,
  218. }, &sa)
  219. if err != nil {
  220. return nil, err
  221. }
  222. // the service account is expected to have a well-known annotation
  223. // this is used as input to assumeRoleWithWebIdentity
  224. roleArn := sa.Annotations[roleARNAnnotation]
  225. if roleArn == "" {
  226. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  227. }
  228. tokenAud := sa.Annotations[audienceAnnotation]
  229. if tokenAud == "" {
  230. tokenAud = defaultTokenAudience
  231. }
  232. audiences := []string{tokenAud}
  233. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  234. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  235. }
  236. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  237. if err != nil {
  238. return nil, err
  239. }
  240. log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  241. return credentials.NewCredentials(jwtProv), nil
  242. }
  243. type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error)
  244. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  245. // controller-runtime/client does not support TokenRequest or other subresource APIs
  246. // so we need to construct our own client and use it to fetch tokens.
  247. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) {
  248. cfg, err := ctrlcfg.GetConfig()
  249. if err != nil {
  250. return nil, err
  251. }
  252. clientset, err := kubernetes.NewForConfig(cfg)
  253. if err != nil {
  254. return nil, err
  255. }
  256. handlers := defaults.Handlers()
  257. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  258. awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  259. if region != "" {
  260. awscfg.WithRegion(region)
  261. }
  262. sess, err := session.NewSessionWithOptions(session.Options{
  263. Config: *awscfg,
  264. SharedConfigState: session.SharedConfigDisable,
  265. Handlers: handlers,
  266. })
  267. if err != nil {
  268. return nil, err
  269. }
  270. tokenFetcher := &authTokenFetcher{
  271. Namespace: namespace,
  272. Audiences: aud,
  273. ServiceAccount: name,
  274. k8sClient: clientset.CoreV1(),
  275. }
  276. return stscreds.NewWebIdentityRoleProviderWithOptions(
  277. sts.New(sess), roleArn, "external-secrets-provider-aws", tokenFetcher), nil
  278. }
  279. type STSProvider func(*session.Session) stsiface.STSAPI
  280. func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
  281. return sts.New(sess)
  282. }
  283. // getAWSSession checks if an AWS session should be reused
  284. // it returns the aws session or an error.
  285. func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
  286. key := cache.Key{
  287. Name: name,
  288. Namespace: namespace,
  289. Kind: kind,
  290. }
  291. if enableCache {
  292. sess, ok := sessionCache.Get(resourceVersion, key)
  293. if ok {
  294. log.Info("reusing aws session", "SecretStore", key.Name, "namespace", key.Namespace, "kind", key.Kind, "resourceversion", resourceVersion)
  295. return sess, nil
  296. }
  297. }
  298. handlers := defaults.Handlers()
  299. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  300. sess, err := session.NewSessionWithOptions(session.Options{
  301. Config: *config,
  302. Handlers: handlers,
  303. })
  304. if err != nil {
  305. return nil, err
  306. }
  307. if enableCache {
  308. sessionCache.Add(resourceVersion, key, sess.Copy())
  309. }
  310. return sess, nil
  311. }