iamauth.go 11 KB

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