auth.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. if prov.Role != "" {
  112. stsclient := assumeRoler(sess)
  113. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, prov.Role))
  114. }
  115. log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
  116. return sess, nil
  117. }
  118. // NewSession creates a new aws session based on the provided store
  119. // it uses the following authentication mechanisms in order:
  120. // * service-account token authentication via AssumeRoleWithWebIdentity
  121. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  122. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  123. func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, region string, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
  124. var creds *credentials.Credentials
  125. var err error
  126. // use credentials via service account token
  127. jwtAuth := auth.JWTAuth
  128. if jwtAuth != nil {
  129. creds, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
  130. if err != nil {
  131. return nil, err
  132. }
  133. }
  134. // use credentials from sercretRef
  135. secretRef := auth.SecretRef
  136. if secretRef != nil {
  137. log.V(1).Info("using credentials from secretRef")
  138. creds, err = credsFromSecretRef(ctx, auth, false, kube, namespace)
  139. if err != nil {
  140. return nil, err
  141. }
  142. }
  143. config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  144. if creds != nil {
  145. config.WithCredentials(creds)
  146. }
  147. if region != "" {
  148. config.WithRegion(region)
  149. }
  150. sess, err := getAWSSession(config, false, "", "", "", "")
  151. if err != nil {
  152. return nil, err
  153. }
  154. if role != "" {
  155. stsclient := assumeRoler(sess)
  156. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, role))
  157. }
  158. log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
  159. return sess, nil
  160. }
  161. // credsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  162. // construct a aws.Credentials object
  163. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  164. // If the ClusterSecretStore defines a namespace it will take precedence.
  165. func credsFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isClusterKind bool, kube client.Client, namespace string) (*credentials.Credentials, error) {
  166. ke := client.ObjectKey{
  167. Name: auth.SecretRef.AccessKeyID.Name,
  168. Namespace: namespace,
  169. }
  170. if isClusterKind && auth.SecretRef.AccessKeyID.Namespace != nil {
  171. ke.Namespace = *auth.SecretRef.AccessKeyID.Namespace
  172. }
  173. akSecret := v1.Secret{}
  174. err := kube.Get(ctx, ke, &akSecret)
  175. if err != nil {
  176. return nil, fmt.Errorf(errFetchAKIDSecret, err)
  177. }
  178. ke = client.ObjectKey{
  179. Name: auth.SecretRef.SecretAccessKey.Name,
  180. Namespace: namespace,
  181. }
  182. if isClusterKind && auth.SecretRef.SecretAccessKey.Namespace != nil {
  183. ke.Namespace = *auth.SecretRef.SecretAccessKey.Namespace
  184. }
  185. sakSecret := v1.Secret{}
  186. err = kube.Get(ctx, ke, &sakSecret)
  187. if err != nil {
  188. return nil, fmt.Errorf(errFetchSAKSecret, err)
  189. }
  190. sak := string(sakSecret.Data[auth.SecretRef.SecretAccessKey.Key])
  191. aks := string(akSecret.Data[auth.SecretRef.AccessKeyID.Key])
  192. if sak == "" {
  193. return nil, fmt.Errorf(errMissingSAK)
  194. }
  195. if aks == "" {
  196. return nil, fmt.Errorf(errMissingAKID)
  197. }
  198. var sessionToken string
  199. if auth.SecretRef.SessionToken != nil {
  200. ke = client.ObjectKey{
  201. Name: auth.SecretRef.SessionToken.Name,
  202. Namespace: namespace,
  203. }
  204. if isClusterKind && auth.SecretRef.SessionToken.Namespace != nil {
  205. ke.Namespace = *auth.SecretRef.SessionToken.Namespace
  206. }
  207. stSecret := v1.Secret{}
  208. err = kube.Get(ctx, ke, &stSecret)
  209. if err != nil {
  210. return nil, fmt.Errorf(errFetchSTSecret, err)
  211. }
  212. sessionToken = string(stSecret.Data[auth.SecretRef.SessionToken.Key])
  213. }
  214. return credentials.NewStaticCredentials(aks, sak, sessionToken), err
  215. }
  216. // credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  217. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  218. // in the ServiceAccount annotation.
  219. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  220. // If the ClusterSecretStore defines the namespace it will take precedence.
  221. func credsFromServiceAccount(ctx context.Context, auth esv1beta1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) {
  222. name := auth.JWTAuth.ServiceAccountRef.Name
  223. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  224. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  225. }
  226. sa := v1.ServiceAccount{}
  227. err := kube.Get(ctx, types.NamespacedName{
  228. Name: name,
  229. Namespace: namespace,
  230. }, &sa)
  231. if err != nil {
  232. return nil, err
  233. }
  234. // the service account is expected to have a well-known annotation
  235. // this is used as input to assumeRoleWithWebIdentity
  236. roleArn := sa.Annotations[roleARNAnnotation]
  237. if roleArn == "" {
  238. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  239. }
  240. tokenAud := sa.Annotations[audienceAnnotation]
  241. if tokenAud == "" {
  242. tokenAud = defaultTokenAudience
  243. }
  244. audiences := []string{tokenAud}
  245. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  246. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  247. }
  248. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  249. if err != nil {
  250. return nil, err
  251. }
  252. log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  253. return credentials.NewCredentials(jwtProv), nil
  254. }
  255. type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error)
  256. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  257. // controller-runtime/client does not support TokenRequest or other subresource APIs
  258. // so we need to construct our own client and use it to fetch tokens.
  259. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) {
  260. cfg, err := ctrlcfg.GetConfig()
  261. if err != nil {
  262. return nil, err
  263. }
  264. clientset, err := kubernetes.NewForConfig(cfg)
  265. if err != nil {
  266. return nil, err
  267. }
  268. handlers := defaults.Handlers()
  269. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  270. awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  271. if region != "" {
  272. awscfg.WithRegion(region)
  273. }
  274. sess, err := session.NewSessionWithOptions(session.Options{
  275. Config: *awscfg,
  276. SharedConfigState: session.SharedConfigDisable,
  277. Handlers: handlers,
  278. })
  279. if err != nil {
  280. return nil, err
  281. }
  282. tokenFetcher := &authTokenFetcher{
  283. Namespace: namespace,
  284. Audiences: aud,
  285. ServiceAccount: name,
  286. k8sClient: clientset.CoreV1(),
  287. }
  288. return stscreds.NewWebIdentityRoleProviderWithOptions(
  289. sts.New(sess), roleArn, "external-secrets-provider-aws", tokenFetcher), nil
  290. }
  291. type STSProvider func(*session.Session) stsiface.STSAPI
  292. func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
  293. return sts.New(sess)
  294. }
  295. // getAWSSession checks if an AWS session should be reused
  296. // it returns the aws session or an error.
  297. func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
  298. key := cache.Key{
  299. Name: name,
  300. Namespace: namespace,
  301. Kind: kind,
  302. }
  303. if enableCache {
  304. sess, ok := sessionCache.Get(resourceVersion, key)
  305. if ok {
  306. log.Info("reusing aws session", "SecretStore", key.Name, "namespace", key.Namespace, "kind", key.Kind, "resourceversion", resourceVersion)
  307. return sess, nil
  308. }
  309. }
  310. handlers := defaults.Handlers()
  311. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  312. sess, err := session.NewSessionWithOptions(session.Options{
  313. Config: *config,
  314. Handlers: handlers,
  315. SharedConfigState: session.SharedConfigDisable,
  316. })
  317. if err != nil {
  318. return nil, err
  319. }
  320. if enableCache {
  321. sessionCache.Add(resourceVersion, key, sess)
  322. }
  323. return sess, nil
  324. }