iamauth.go 9.5 KB

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