auth.go 12 KB

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