iamauth.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. // Mostly sourced from ~/external-secrets/pkg/provider/aws/auth
  13. package iamauth
  14. import (
  15. "context"
  16. "fmt"
  17. "os"
  18. "github.com/aws/aws-sdk-go/aws"
  19. "github.com/aws/aws-sdk-go/aws/credentials"
  20. "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
  21. "github.com/aws/aws-sdk-go/aws/defaults"
  22. "github.com/aws/aws-sdk-go/aws/endpoints"
  23. "github.com/aws/aws-sdk-go/aws/request"
  24. "github.com/aws/aws-sdk-go/aws/session"
  25. "github.com/aws/aws-sdk-go/service/sts"
  26. "github.com/aws/aws-sdk-go/service/sts/stsiface"
  27. authv1 "k8s.io/api/authentication/v1"
  28. v1 "k8s.io/api/core/v1"
  29. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  30. "k8s.io/apimachinery/pkg/types"
  31. "k8s.io/client-go/kubernetes"
  32. k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  33. ctrl "sigs.k8s.io/controller-runtime"
  34. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  35. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  36. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  37. awsutil "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  38. "github.com/external-secrets/external-secrets/pkg/provider/vault/util"
  39. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  40. )
  41. var (
  42. logger = ctrl.Log.WithName("provider").WithName("vault")
  43. )
  44. const (
  45. roleARNAnnotation = "eks.amazonaws.com/role-arn"
  46. audienceAnnotation = "eks.amazonaws.com/audience"
  47. defaultTokenAudience = "sts.amazonaws.com"
  48. STSEndpointEnv = "AWS_STS_ENDPOINT"
  49. AWSWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
  50. )
  51. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  52. // controller-runtime/client does not support TokenRequest or other subresource APIs
  53. // so we need to construct our own client and use it to fetch tokens.
  54. func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) {
  55. cfg, err := ctrlcfg.GetConfig()
  56. if err != nil {
  57. return nil, err
  58. }
  59. clientset, err := kubernetes.NewForConfig(cfg)
  60. if err != nil {
  61. return nil, err
  62. }
  63. handlers := defaults.Handlers()
  64. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  65. awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
  66. if region != "" {
  67. awscfg.WithRegion(region)
  68. }
  69. sess, err := session.NewSessionWithOptions(session.Options{
  70. Config: *awscfg,
  71. SharedConfigState: session.SharedConfigDisable,
  72. Handlers: handlers,
  73. })
  74. if err != nil {
  75. return nil, awsutil.SanitizeErr(err)
  76. }
  77. tokenFetcher := &authTokenFetcher{
  78. Namespace: namespace,
  79. Audiences: aud,
  80. ServiceAccount: name,
  81. k8sClient: clientset.CoreV1(),
  82. }
  83. return stscreds.NewWebIdentityRoleProviderWithOptions(
  84. sts.New(sess), roleArn, "external-secrets-provider-vault", tokenFetcher), nil
  85. }
  86. // ResolveEndpoint returns a ResolverFunc with
  87. // customizable endpoints.
  88. func ResolveEndpoint() endpoints.ResolverFunc {
  89. customEndpoints := make(map[string]string)
  90. if v := os.Getenv(STSEndpointEnv); v != "" {
  91. customEndpoints["sts"] = v
  92. }
  93. return ResolveEndpointWithServiceMap(customEndpoints)
  94. }
  95. func ResolveEndpointWithServiceMap(customEndpoints map[string]string) endpoints.ResolverFunc {
  96. defaultResolver := endpoints.DefaultResolver()
  97. return func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
  98. if ep, ok := customEndpoints[service]; ok {
  99. return endpoints.ResolvedEndpoint{
  100. URL: ep,
  101. }, nil
  102. }
  103. return defaultResolver.EndpointFor(service, region, opts...)
  104. }
  105. }
  106. // mostly taken from:
  107. // https://github.com/aws/secrets-store-csi-driver-provider-aws/blob/main/auth/auth.go#L140-L145
  108. type authTokenFetcher struct {
  109. Namespace string
  110. // Audience is the token aud claim
  111. // which is verified by the aws oidc provider
  112. // see: https://github.com/external-secrets/external-secrets/issues/1251#issuecomment-1161745849
  113. Audiences []string
  114. ServiceAccount string
  115. k8sClient k8scorev1.CoreV1Interface
  116. }
  117. // FetchToken satisfies the stscreds.TokenFetcher interface
  118. // it is used to generate service account tokens which are consumed by the aws sdk.
  119. func (p authTokenFetcher) FetchToken(ctx credentials.Context) ([]byte, error) {
  120. logger.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount)
  121. tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(ctx, p.ServiceAccount, &authv1.TokenRequest{
  122. Spec: authv1.TokenRequestSpec{
  123. Audiences: p.Audiences,
  124. },
  125. }, metav1.CreateOptions{})
  126. if err != nil {
  127. return nil, fmt.Errorf("error creating service account token: %w", err)
  128. }
  129. return []byte(tokRsp.Status.Token), nil
  130. }
  131. // CredsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  132. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  133. // in the ServiceAccount annotation.
  134. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  135. // If the ClusterSecretStore defines the namespace it will take precedence.
  136. func CredsFromServiceAccount(ctx context.Context, auth esv1.VaultIamAuth, region string, isClusterKind bool, kube kclient.Client, namespace string, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) {
  137. name := auth.JWTAuth.ServiceAccountRef.Name
  138. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  139. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  140. }
  141. sa := v1.ServiceAccount{}
  142. err := kube.Get(ctx, types.NamespacedName{
  143. Name: name,
  144. Namespace: namespace,
  145. }, &sa)
  146. if err != nil {
  147. return nil, err
  148. }
  149. // the service account is expected to have a well-known annotation
  150. // this is used as input to assumeRoleWithWebIdentity
  151. roleArn := sa.Annotations[roleARNAnnotation]
  152. if roleArn == "" {
  153. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  154. }
  155. tokenAud := sa.Annotations[audienceAnnotation]
  156. if tokenAud == "" {
  157. tokenAud = defaultTokenAudience
  158. }
  159. audiences := []string{tokenAud}
  160. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  161. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  162. }
  163. jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
  164. if err != nil {
  165. return nil, err
  166. }
  167. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  168. return credentials.NewCredentials(jwtProv), nil
  169. }
  170. func CredsFromControllerServiceAccount(ctx context.Context, saname, ns, region string, kube kclient.Client, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) {
  171. name := saname
  172. nmspc := ns
  173. sa := v1.ServiceAccount{}
  174. err := kube.Get(ctx, types.NamespacedName{
  175. Name: name,
  176. Namespace: nmspc,
  177. }, &sa)
  178. if err != nil {
  179. return nil, err
  180. }
  181. // the service account is expected to have a well-known annotation
  182. // this is used as input to assumeRoleWithWebIdentity
  183. roleArn := sa.Annotations[roleARNAnnotation]
  184. if roleArn == "" {
  185. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, nmspc)
  186. }
  187. tokenAud := sa.Annotations[audienceAnnotation]
  188. if tokenAud == "" {
  189. tokenAud = defaultTokenAudience
  190. }
  191. audiences := []string{tokenAud}
  192. jwtProv, err := jwtProvider(name, nmspc, roleArn, audiences, region)
  193. if err != nil {
  194. return nil, err
  195. }
  196. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  197. return credentials.NewCredentials(jwtProv), nil
  198. }
  199. // CredsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  200. // construct a aws.Credentials object
  201. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  202. // If the ClusterSecretStore defines a namespace it will take precedence.
  203. func CredsFromSecretRef(ctx context.Context, auth esv1.VaultIamAuth, storeKind string, kube kclient.Client, namespace string) (*credentials.Credentials, error) {
  204. akid, err := resolvers.SecretKeyRef(
  205. ctx,
  206. kube,
  207. storeKind,
  208. namespace,
  209. &auth.SecretRef.AccessKeyID,
  210. )
  211. if err != nil {
  212. return nil, err
  213. }
  214. sak, err := resolvers.SecretKeyRef(
  215. ctx,
  216. kube,
  217. storeKind,
  218. namespace,
  219. &auth.SecretRef.SecretAccessKey,
  220. )
  221. if err != nil {
  222. return nil, err
  223. }
  224. // session token is optional
  225. sessionToken, _ := resolvers.SecretKeyRef(
  226. ctx,
  227. kube,
  228. storeKind,
  229. namespace,
  230. auth.SecretRef.SessionToken,
  231. )
  232. return credentials.NewStaticCredentials(akid, sak, sessionToken), err
  233. }
  234. type STSProvider func(*session.Session) stsiface.STSAPI
  235. func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
  236. return sts.New(sess)
  237. }
  238. // getAWSSession returns the aws session or an error.
  239. func GetAWSSession(config *aws.Config) (*session.Session, error) {
  240. handlers := defaults.Handlers()
  241. handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
  242. sess, err := session.NewSessionWithOptions(session.Options{
  243. Config: *config,
  244. Handlers: handlers,
  245. SharedConfigState: session.SharedConfigDisable,
  246. })
  247. if err != nil {
  248. return nil, err
  249. }
  250. return sess, nil
  251. }