auth.go 14 KB

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