auth.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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-v2/aws"
  17. "github.com/aws/aws-sdk-go-v2/config"
  18. "github.com/aws/aws-sdk-go-v2/credentials"
  19. "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
  20. "github.com/aws/aws-sdk-go-v2/service/sts"
  21. stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
  22. "github.com/spf13/pflag"
  23. v1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. "k8s.io/client-go/kubernetes"
  26. ctrl "sigs.k8s.io/controller-runtime"
  27. "sigs.k8s.io/controller-runtime/pkg/client"
  28. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  29. "github.com/external-secrets/external-secrets/pkg/feature"
  30. "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  31. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  32. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  33. )
  34. // Config contains configuration to create a new AWS provider.
  35. type Config struct {
  36. AssumeRole string
  37. Region string
  38. APIRetries int
  39. }
  40. var (
  41. log = ctrl.Log.WithName("provider").WithName("aws")
  42. enableSessionCache bool
  43. )
  44. const (
  45. roleARNAnnotation = "eks.amazonaws.com/role-arn"
  46. audienceAnnotation = "eks.amazonaws.com/audience"
  47. defaultTokenAudience = "sts.amazonaws.com"
  48. errFetchAKIDSecret = "could not fetch accessKeyID secret: %w"
  49. errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
  50. errFetchSTSecret = "could not fetch SessionToken secret: %w"
  51. )
  52. func init() {
  53. fs := pflag.NewFlagSet("aws-auth", pflag.ExitOnError)
  54. fs.BoolVar(&enableSessionCache, "experimental-enable-aws-session-cache", false, "DEPRECATED: this flag is no longer used and will be removed since aws sdk v2 has its own session cache.")
  55. feature.Register(feature.Feature{
  56. Flags: fs,
  57. })
  58. }
  59. // Opts define options for New function.
  60. type Opts struct {
  61. Store esv1.GenericStore
  62. Kube client.Client
  63. Namespace string
  64. AssumeRoler STSProvider
  65. JWTProvider jwtProviderFactory
  66. }
  67. // New creates a new aws config 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, opts Opts) (*aws.Config, error) {
  73. prov, err := util.GetAWSProvider(opts.Store)
  74. if err != nil {
  75. return nil, err
  76. }
  77. var credsProvider aws.CredentialsProvider
  78. isClusterKind := opts.Store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind
  79. credsProvider, err = constructCredsProvider(ctx, prov, isClusterKind, opts)
  80. if err != nil {
  81. return nil, err
  82. }
  83. // global endpoint resolver is deprecated, should we EndpointResolverV2 field on service client options
  84. var loadCfgOpts []func(*config.LoadOptions) error
  85. if credsProvider != nil {
  86. loadCfgOpts = append(loadCfgOpts, config.WithCredentialsProvider(credsProvider))
  87. }
  88. if prov.Region != "" {
  89. loadCfgOpts = append(loadCfgOpts, config.WithRegion(prov.Region))
  90. }
  91. return createConfiguration(prov, opts.AssumeRoler, loadCfgOpts)
  92. }
  93. func createConfiguration(prov *esv1.AWSProvider, assumeRoler STSProvider, loadCfgOpts []func(*config.LoadOptions) error) (*aws.Config, error) {
  94. cfg, err := config.LoadDefaultConfig(context.TODO(), loadCfgOpts...)
  95. if err != nil {
  96. return nil, err
  97. }
  98. for _, aRole := range prov.AdditionalRoles {
  99. stsclient := assumeRoler(cfg)
  100. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, aRole)
  101. }
  102. sessExtID := prov.ExternalID
  103. sessTransitiveTagKeys := prov.TransitiveTagKeys
  104. sessTags := make([]stsTypes.Tag, len(prov.SessionTags))
  105. for i, tag := range prov.SessionTags {
  106. sessTags[i] = stsTypes.Tag{
  107. Key: aws.String(tag.Key),
  108. Value: aws.String(tag.Value),
  109. }
  110. }
  111. if prov.Role != "" {
  112. stsclient := assumeRoler(cfg)
  113. if sessExtID != "" || sessTags != nil {
  114. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role, setAssumeRoleOptionFn(sessExtID, sessTags, sessTransitiveTagKeys))
  115. } else {
  116. cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role)
  117. }
  118. }
  119. log.Info("using aws config", "region", cfg.Region, "external id", sessExtID, "credentials", cfg.Credentials)
  120. return &cfg, nil
  121. }
  122. func setAssumeRoleOptionFn(sessExtID string, sessTags []stsTypes.Tag, sessTransitiveTagKeys []string) func(p *stscreds.AssumeRoleOptions) {
  123. return func(p *stscreds.AssumeRoleOptions) {
  124. if sessExtID != "" {
  125. p.ExternalID = aws.String(sessExtID)
  126. }
  127. if sessTags != nil {
  128. p.Tags = sessTags
  129. if len(sessTransitiveTagKeys) > 0 {
  130. p.TransitiveTagKeys = sessTransitiveTagKeys
  131. }
  132. }
  133. }
  134. }
  135. func constructCredsProvider(ctx context.Context, prov *esv1.AWSProvider, isClusterKind bool, opts Opts) (aws.CredentialsProvider, error) {
  136. switch {
  137. case prov.Auth.JWTAuth != nil:
  138. return credsFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, opts.Kube, opts.Namespace, opts.JWTProvider)
  139. case prov.Auth.SecretRef != nil:
  140. log.V(1).Info("using credentials from secretRef")
  141. return credsFromSecretRef(ctx, prov.Auth, opts.Store.GetKind(), opts.Kube, opts.Namespace)
  142. default:
  143. return nil, nil
  144. }
  145. }
  146. // NewGeneratorSession creates a new aws session based on the provided store
  147. // it uses the following authentication mechanisms in order:
  148. // * service-account token authentication via AssumeRoleWithWebIdentity
  149. // * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
  150. // * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
  151. func NewGeneratorSession(ctx context.Context, auth esv1.AWSAuth, role, region string, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*aws.Config, error) {
  152. var (
  153. credsProvider aws.CredentialsProvider
  154. err error
  155. )
  156. // use credentials via service account token
  157. jwtAuth := auth.JWTAuth
  158. if jwtAuth != nil {
  159. credsProvider, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
  160. if err != nil {
  161. return nil, err
  162. }
  163. }
  164. // use credentials from secretRef
  165. secretRef := auth.SecretRef
  166. if secretRef != nil {
  167. log.V(1).Info("using credentials from secretRef")
  168. credsProvider, err = credsFromSecretRef(ctx, auth, "", kube, namespace)
  169. if err != nil {
  170. return nil, err
  171. }
  172. }
  173. config := aws.NewConfig()
  174. if credsProvider != nil {
  175. config.Credentials = credsProvider
  176. }
  177. if region != "" {
  178. config.Region = region
  179. }
  180. if role != "" {
  181. stsclient := assumeRoler(*config)
  182. config.Credentials = stscreds.NewAssumeRoleProvider(stsclient, role)
  183. }
  184. log.Info("using aws config", "region", config.Region, "credentials", config.Credentials)
  185. return config, nil
  186. }
  187. // credsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  188. // construct a aws.Credentials object
  189. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  190. // If the ClusterSecretStore defines a namespace it will take precedence.
  191. func credsFromSecretRef(ctx context.Context, auth esv1.AWSAuth, storeKind string, kube client.Client, namespace string) (aws.CredentialsProvider, error) {
  192. sak, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.SecretAccessKey)
  193. if err != nil {
  194. return nil, fmt.Errorf(errFetchSAKSecret, err)
  195. }
  196. aks, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.AccessKeyID)
  197. if err != nil {
  198. return nil, fmt.Errorf(errFetchAKIDSecret, err)
  199. }
  200. var sessionToken string
  201. if auth.SecretRef.SessionToken != nil {
  202. sessionToken, err = resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, auth.SecretRef.SessionToken)
  203. if err != nil {
  204. return nil, fmt.Errorf(errFetchSTSecret, err)
  205. }
  206. }
  207. var credsProvider aws.CredentialsProvider = credentials.NewStaticCredentialsProvider(aks, sak, sessionToken)
  208. return credsProvider, nil
  209. }
  210. // credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  211. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  212. // in the ServiceAccount annotation.
  213. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  214. // If the ClusterSecretStore defines the namespace it will take precedence.
  215. func credsFromServiceAccount(ctx context.Context, auth esv1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (aws.CredentialsProvider, error) {
  216. name := auth.JWTAuth.ServiceAccountRef.Name
  217. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  218. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  219. }
  220. sa := v1.ServiceAccount{}
  221. err := kube.Get(ctx, types.NamespacedName{
  222. Name: name,
  223. Namespace: namespace,
  224. }, &sa)
  225. if err != nil {
  226. return nil, err
  227. }
  228. // the service account is expected to have a well-known annotation
  229. // this is used as input to assumeRoleWithWebIdentity
  230. roleArn := sa.Annotations[roleARNAnnotation]
  231. if roleArn == "" {
  232. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  233. }
  234. tokenAud := sa.Annotations[audienceAnnotation]
  235. if tokenAud == "" {
  236. tokenAud = defaultTokenAudience
  237. }
  238. audiences := []string{tokenAud}
  239. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  240. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  241. }
  242. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  243. if err != nil {
  244. return nil, err
  245. }
  246. log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  247. return jwtProv, nil
  248. }
  249. type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error)
  250. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  251. // controller-runtime/client does not support TokenRequest or other subresource APIs
  252. // so we need to construct our own client and use it to fetch tokens.
  253. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error) {
  254. cfg, err := ctrlcfg.GetConfig()
  255. if err != nil {
  256. return nil, err
  257. }
  258. clientset, err := kubernetes.NewForConfig(cfg)
  259. if err != nil {
  260. return nil, err
  261. }
  262. awscfg, err := config.LoadDefaultConfig(context.TODO(), config.WithAppID("external-secrets"),
  263. config.WithRegion(region),
  264. config.WithSharedConfigFiles([]string{}), // Disable shared config files:
  265. config.WithSharedCredentialsFiles([]string{}))
  266. if err != nil {
  267. return nil, err
  268. }
  269. tokenFetcher := authTokenFetcher{
  270. Namespace: namespace,
  271. Audiences: aud,
  272. ServiceAccount: name,
  273. k8sClient: clientset.CoreV1(),
  274. }
  275. stsClient := sts.NewFromConfig(awscfg, func(o *sts.Options) {
  276. o.EndpointResolverV2 = customEndpointResolver{}
  277. })
  278. return stscreds.NewWebIdentityRoleProvider(
  279. stsClient, roleArn, tokenFetcher, func(opts *stscreds.WebIdentityRoleOptions) {
  280. opts.RoleSessionName = "external-secrets-provider-aws"
  281. }), nil
  282. }
  283. type STSprovider interface {
  284. AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
  285. AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error)
  286. AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error)
  287. AssumeRoot(ctx context.Context, params *sts.AssumeRootInput, optFns ...func(*sts.Options)) (*sts.AssumeRootOutput, error)
  288. DecodeAuthorizationMessage(ctx context.Context, params *sts.DecodeAuthorizationMessageInput, optFns ...func(*sts.Options)) (*sts.DecodeAuthorizationMessageOutput, error)
  289. }
  290. type STSProvider func(aws.Config) STSprovider
  291. func DefaultSTSProvider(cfg aws.Config) STSprovider {
  292. stsClient := sts.NewFromConfig(cfg, func(o *sts.Options) {
  293. o.EndpointResolverV2 = customEndpointResolver{}
  294. })
  295. return stsClient
  296. }