auth.go 13 KB

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