auth.go 13 KB

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