auth.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. errMissingSAK = "missing SecretAccessKey"
  59. errMissingAKID = "missing AccessKeyID"
  60. )
  61. // New creates a new aws session based on the provided store
  62. // it uses the following authentication mechanisms in order:
  63. // * service-account token authentication via AssumeRoleWithWebIdentity
  64. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  65. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  66. func New(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
  67. prov, err := util.GetAWSProvider(store)
  68. if err != nil {
  69. return nil, err
  70. }
  71. var creds *credentials.Credentials
  72. isClusterKind := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
  73. // use credentials via service account token
  74. jwtAuth := prov.Auth.JWTAuth
  75. if jwtAuth != nil {
  76. creds, err = sessionFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, kube, namespace, jwtProvider)
  77. if err != nil {
  78. return nil, err
  79. }
  80. }
  81. // use credentials from sercretRef
  82. secretRef := prov.Auth.SecretRef
  83. if secretRef != nil {
  84. log.V(1).Info("using credentials from secretRef")
  85. creds, err = sessionFromSecretRef(ctx, prov.Auth, isClusterKind, kube, namespace)
  86. if err != nil {
  87. return nil, err
  88. }
  89. }
  90. config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  91. if creds != nil {
  92. config.WithCredentials(creds)
  93. }
  94. if prov.Region != "" {
  95. config.WithRegion(prov.Region)
  96. }
  97. sess, err := getAWSSession(config, EnableCache, store.GetName(), store.GetTypeMeta().Kind, namespace, store.GetObjectMeta().ResourceVersion)
  98. if err != nil {
  99. return nil, err
  100. }
  101. if prov.Role != "" {
  102. stsclient := assumeRoler(sess)
  103. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, prov.Role))
  104. }
  105. log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
  106. return sess, nil
  107. }
  108. // NewSession creates a new aws session based on the provided store
  109. // it uses the following authentication mechanisms in order:
  110. // * service-account token authentication via AssumeRoleWithWebIdentity
  111. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  112. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  113. func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, region string, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
  114. var creds *credentials.Credentials
  115. var err error
  116. // use credentials via service account token
  117. jwtAuth := auth.JWTAuth
  118. if jwtAuth != nil {
  119. creds, err = sessionFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
  120. if err != nil {
  121. return nil, err
  122. }
  123. }
  124. // use credentials from sercretRef
  125. secretRef := auth.SecretRef
  126. if secretRef != nil {
  127. log.V(1).Info("using credentials from secretRef")
  128. creds, err = sessionFromSecretRef(ctx, auth, false, kube, namespace)
  129. if err != nil {
  130. return nil, err
  131. }
  132. }
  133. config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  134. if creds != nil {
  135. config.WithCredentials(creds)
  136. }
  137. if region != "" {
  138. config.WithRegion(region)
  139. }
  140. sess, err := getAWSSession(config, false, "", "", "", "")
  141. if err != nil {
  142. return nil, err
  143. }
  144. if role != "" {
  145. stsclient := assumeRoler(sess)
  146. sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, role))
  147. }
  148. log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
  149. return sess, nil
  150. }
  151. func sessionFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isClusterKind bool, kube client.Client, namespace string) (*credentials.Credentials, error) {
  152. ke := client.ObjectKey{
  153. Name: auth.SecretRef.AccessKeyID.Name,
  154. Namespace: namespace, // default to ExternalSecret namespace
  155. }
  156. // only ClusterStore is allowed to set namespace (and then it's required)
  157. if isClusterKind {
  158. if auth.SecretRef.AccessKeyID.Namespace == nil {
  159. return nil, fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
  160. }
  161. ke.Namespace = *auth.SecretRef.AccessKeyID.Namespace
  162. }
  163. akSecret := v1.Secret{}
  164. err := kube.Get(ctx, ke, &akSecret)
  165. if err != nil {
  166. return nil, fmt.Errorf(errFetchAKIDSecret, err)
  167. }
  168. ke = client.ObjectKey{
  169. Name: auth.SecretRef.SecretAccessKey.Name,
  170. Namespace: namespace, // default to ExternalSecret namespace
  171. }
  172. // only ClusterStore is allowed to set namespace (and then it's required)
  173. if isClusterKind {
  174. if auth.SecretRef.SecretAccessKey.Namespace == nil {
  175. return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
  176. }
  177. ke.Namespace = *auth.SecretRef.SecretAccessKey.Namespace
  178. }
  179. sakSecret := v1.Secret{}
  180. err = kube.Get(ctx, ke, &sakSecret)
  181. if err != nil {
  182. return nil, fmt.Errorf(errFetchSAKSecret, err)
  183. }
  184. sak := string(sakSecret.Data[auth.SecretRef.SecretAccessKey.Key])
  185. aks := string(akSecret.Data[auth.SecretRef.AccessKeyID.Key])
  186. if sak == "" {
  187. return nil, fmt.Errorf(errMissingSAK)
  188. }
  189. if aks == "" {
  190. return nil, fmt.Errorf(errMissingAKID)
  191. }
  192. return credentials.NewStaticCredentials(aks, sak, ""), err
  193. }
  194. func sessionFromServiceAccount(ctx context.Context, auth esv1beta1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) {
  195. name := auth.JWTAuth.ServiceAccountRef.Name
  196. if isClusterKind {
  197. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  198. }
  199. sa := v1.ServiceAccount{}
  200. err := kube.Get(ctx, types.NamespacedName{
  201. Name: name,
  202. Namespace: namespace,
  203. }, &sa)
  204. if err != nil {
  205. return nil, err
  206. }
  207. // the service account is expected to have a well-known annotation
  208. // this is used as input to assumeRoleWithWebIdentity
  209. roleArn := sa.Annotations[roleARNAnnotation]
  210. if roleArn == "" {
  211. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  212. }
  213. tokenAud := sa.Annotations[audienceAnnotation]
  214. if tokenAud == "" {
  215. tokenAud = defaultTokenAudience
  216. }
  217. audiences := []string{tokenAud}
  218. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  219. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  220. }
  221. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  222. if err != nil {
  223. return nil, err
  224. }
  225. log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  226. return credentials.NewCredentials(jwtProv), nil
  227. }
  228. type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error)
  229. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  230. // controller-runtime/client does not support TokenRequest or other subresource APIs
  231. // so we need to construct our own client and use it to fetch tokens.
  232. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) {
  233. cfg, err := ctrlcfg.GetConfig()
  234. if err != nil {
  235. return nil, err
  236. }
  237. clientset, err := kubernetes.NewForConfig(cfg)
  238. if err != nil {
  239. return nil, err
  240. }
  241. handlers := defaults.Handlers()
  242. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  243. awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  244. if region != "" {
  245. awscfg.WithRegion(region)
  246. }
  247. sess, err := session.NewSessionWithOptions(session.Options{
  248. Config: *awscfg,
  249. SharedConfigState: session.SharedConfigDisable,
  250. Handlers: handlers,
  251. })
  252. if err != nil {
  253. return nil, err
  254. }
  255. tokenFetcher := &authTokenFetcher{
  256. Namespace: namespace,
  257. Audiences: aud,
  258. ServiceAccount: name,
  259. k8sClient: clientset.CoreV1(),
  260. }
  261. return stscreds.NewWebIdentityRoleProviderWithOptions(
  262. sts.New(sess), roleArn, "external-secrets-provider-aws", tokenFetcher), nil
  263. }
  264. type STSProvider func(*session.Session) stsiface.STSAPI
  265. func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
  266. return sts.New(sess)
  267. }
  268. // getAWSSession check if an AWS session should be reused
  269. // it returns the aws session or an error.
  270. func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
  271. tmpSession := SessionCache{
  272. Name: name,
  273. Namespace: namespace,
  274. Kind: kind,
  275. ResourceVersion: resourceVersion,
  276. }
  277. if EnableCache {
  278. sess, ok := sessions[tmpSession]
  279. if ok {
  280. log.Info("reusing aws session", "SecretStore", tmpSession.Name, "namespace", tmpSession.Namespace, "kind", tmpSession.Kind, "resourceversion", tmpSession.ResourceVersion)
  281. return sess, nil
  282. }
  283. }
  284. handlers := defaults.Handlers()
  285. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  286. sess, err := session.NewSessionWithOptions(session.Options{
  287. Config: *config,
  288. Handlers: handlers,
  289. SharedConfigState: session.SharedConfigDisable,
  290. })
  291. if err != nil {
  292. return nil, err
  293. }
  294. if EnableCache {
  295. sessions[tmpSession] = sess
  296. }
  297. return sess, nil
  298. }