iamauth.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package iamauth provides utilities for AWS IAM authentication using Kubernetes Service Accounts.
  14. // Mostly sourced from ~/external-secrets/pkg/provider/aws/auth
  15. package iamauth
  16. import (
  17. "context"
  18. "fmt"
  19. "net/url"
  20. "os"
  21. "github.com/aws/aws-sdk-go-v2/aws"
  22. "github.com/aws/aws-sdk-go-v2/config"
  23. "github.com/aws/aws-sdk-go-v2/credentials"
  24. "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
  25. "github.com/aws/aws-sdk-go-v2/service/sts"
  26. smithy "github.com/aws/smithy-go/endpoints"
  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/providers/v1/aws/util"
  38. vaultutil "github.com/external-secrets/external-secrets/providers/v1/vault/util"
  39. "github.com/external-secrets/external-secrets/runtime/esutils/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 is the environment variable that can be used to override the default STS endpoint.
  49. STSEndpointEnv = "AWS_STS_ENDPOINT"
  50. // AWSWebIdentityTokenFileEnvVar is the environment variable that points to the service account token file.
  51. AWSWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
  52. // AWSContainerCredentialsFullURIEnvVar is the environment variable that points to the full credentials URI for ECS tasks.
  53. AWSContainerCredentialsFullURIEnvVar = "AWS_CONTAINER_CREDENTIALS_FULL_URI"
  54. )
  55. // DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
  56. // controller-runtime/client does not support TokenRequest or other subresource APIs
  57. // so we need to construct our own client and use it to fetch tokens.
  58. func DefaultJWTProvider(ctx context.Context, name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error) {
  59. cfg, err := ctrlcfg.GetConfig()
  60. if err != nil {
  61. return nil, err
  62. }
  63. clientset, err := kubernetes.NewForConfig(cfg)
  64. if err != nil {
  65. return nil, err
  66. }
  67. var loadCfgOpts []func(*config.LoadOptions) error
  68. loadCfgOpts = append(loadCfgOpts,
  69. config.WithAppID("external-secrets"),
  70. config.WithSharedConfigFiles([]string{}),
  71. config.WithSharedCredentialsFiles([]string{}),
  72. )
  73. if region != "" {
  74. loadCfgOpts = append(loadCfgOpts, config.WithRegion(region))
  75. }
  76. awscfg, err := config.LoadDefaultConfig(context.TODO(), loadCfgOpts...)
  77. if err != nil {
  78. return nil, awsutil.SanitizeErr(err)
  79. }
  80. tokenFetcher := &authTokenFetcher{
  81. Namespace: namespace,
  82. Audiences: aud,
  83. ServiceAccount: name,
  84. Context: ctx,
  85. k8sClient: clientset.CoreV1(),
  86. }
  87. stsClient := sts.NewFromConfig(awscfg, func(o *sts.Options) {
  88. o.EndpointResolverV2 = customEndpointResolver{}
  89. })
  90. return stscreds.NewWebIdentityRoleProvider(
  91. stsClient, roleArn, tokenFetcher, func(opts *stscreds.WebIdentityRoleOptions) {
  92. opts.RoleSessionName = "external-secrets-provider-vault"
  93. }), nil
  94. }
  95. // customEndpointResolver implements sts.EndpointResolverV2 for custom STS endpoint.
  96. type customEndpointResolver struct{}
  97. // ResolveEndpoint resolves the STS endpoint using custom configuration if available.
  98. func (r customEndpointResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (smithy.Endpoint, error) {
  99. if v := os.Getenv(STSEndpointEnv); v != "" {
  100. uri, err := url.Parse(v)
  101. if err != nil {
  102. return smithy.Endpoint{}, err
  103. }
  104. return smithy.Endpoint{
  105. URI: *uri,
  106. }, nil
  107. }
  108. // Fall back to default resolver
  109. return sts.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
  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. Context context.Context
  115. Namespace string
  116. // Audience is the token aud claim
  117. // which is verified by the aws oidc provider
  118. // see: https://github.com/external-secrets/external-secrets/issues/1251#issuecomment-1161745849
  119. Audiences []string
  120. ServiceAccount string
  121. k8sClient k8scorev1.CoreV1Interface
  122. }
  123. // GetIdentityToken satisfies the stscreds.IdentityTokenRetriever interface
  124. // it is used to generate service account tokens which are consumed by the aws sdk.
  125. func (p *authTokenFetcher) GetIdentityToken() ([]byte, error) {
  126. logger.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount)
  127. tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(p.Context, p.ServiceAccount, &authv1.TokenRequest{
  128. Spec: authv1.TokenRequestSpec{
  129. Audiences: p.Audiences,
  130. },
  131. }, metav1.CreateOptions{})
  132. if err != nil {
  133. return nil, fmt.Errorf("error creating service account token: %w", err)
  134. }
  135. return []byte(tokRsp.Status.Token), nil
  136. }
  137. // CredsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
  138. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  139. // in the ServiceAccount annotation.
  140. // If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
  141. // If the ClusterSecretStore defines the namespace it will take precedence.
  142. func CredsFromServiceAccount(
  143. ctx context.Context,
  144. auth esv1.VaultIamAuth,
  145. region string,
  146. isClusterKind bool,
  147. kube kclient.Client,
  148. namespace string,
  149. jwtProvider vaultutil.JwtProviderFactory,
  150. ) (aws.CredentialsProvider, error) {
  151. name := auth.JWTAuth.ServiceAccountRef.Name
  152. if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
  153. namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
  154. }
  155. sa := v1.ServiceAccount{}
  156. err := kube.Get(ctx, types.NamespacedName{
  157. Name: name,
  158. Namespace: namespace,
  159. }, &sa)
  160. if err != nil {
  161. return nil, err
  162. }
  163. // the service account is expected to have a well-known annotation
  164. // this is used as input to assumeRoleWithWebIdentity
  165. roleArn := sa.Annotations[roleARNAnnotation]
  166. if roleArn == "" {
  167. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
  168. }
  169. tokenAud := sa.Annotations[audienceAnnotation]
  170. if tokenAud == "" {
  171. tokenAud = defaultTokenAudience
  172. }
  173. audiences := []string{tokenAud}
  174. if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
  175. audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
  176. }
  177. jwtProv, err := jwtProvider(ctx, name, namespace, roleArn, audiences, region)
  178. if err != nil {
  179. return nil, err
  180. }
  181. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  182. return jwtProv, nil
  183. }
  184. // CredsFromControllerServiceAccount uses a Kubernetes Service Account to acquire temporary
  185. // credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
  186. // in the ServiceAccount annotation.
  187. // The namespace of the controller service account is used.
  188. func CredsFromControllerServiceAccount(ctx context.Context, saName, ns, region string, kube kclient.Client, jwtProvider vaultutil.JwtProviderFactory) (aws.CredentialsProvider, error) {
  189. sa := v1.ServiceAccount{}
  190. err := kube.Get(ctx, types.NamespacedName{
  191. Name: saName,
  192. Namespace: ns,
  193. }, &sa)
  194. if err != nil {
  195. return nil, err
  196. }
  197. // the service account is expected to have a well-known annotation
  198. // this is used as input to assumeRoleWithWebIdentity
  199. roleArn := sa.Annotations[roleARNAnnotation]
  200. if roleArn == "" {
  201. return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", saName, ns)
  202. }
  203. tokenAud := sa.Annotations[audienceAnnotation]
  204. if tokenAud == "" {
  205. tokenAud = defaultTokenAudience
  206. }
  207. audiences := []string{tokenAud}
  208. jwtProv, err := jwtProvider(ctx, saName, ns, roleArn, audiences, region)
  209. if err != nil {
  210. return nil, err
  211. }
  212. logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
  213. return jwtProv, nil
  214. }
  215. // CredsFromSecretRef pulls access-key / secret-access-key from a secretRef to
  216. // construct a aws.Credentials object
  217. // The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
  218. // If the ClusterSecretStore defines a namespace it will take precedence.
  219. func CredsFromSecretRef(ctx context.Context, auth esv1.VaultIamAuth, storeKind string, kube kclient.Client, namespace string) (aws.CredentialsProvider, error) {
  220. akid, err := resolvers.SecretKeyRef(
  221. ctx,
  222. kube,
  223. storeKind,
  224. namespace,
  225. &auth.SecretRef.AccessKeyID,
  226. )
  227. if err != nil {
  228. return nil, err
  229. }
  230. sak, err := resolvers.SecretKeyRef(
  231. ctx,
  232. kube,
  233. storeKind,
  234. namespace,
  235. &auth.SecretRef.SecretAccessKey,
  236. )
  237. if err != nil {
  238. return nil, err
  239. }
  240. // session token is optional
  241. sessionToken, _ := resolvers.SecretKeyRef(
  242. ctx,
  243. kube,
  244. storeKind,
  245. namespace,
  246. auth.SecretRef.SessionToken,
  247. )
  248. return credentials.NewStaticCredentialsProvider(akid, sak, sessionToken), err
  249. }
  250. // STSProvider is a function type that returns an STS client.
  251. type STSProvider func(*aws.Config) *sts.Client
  252. // DefaultSTSProvider returns the default sts client.
  253. func DefaultSTSProvider(cfg *aws.Config) *sts.Client {
  254. return sts.NewFromConfig(*cfg, func(o *sts.Options) {
  255. o.EndpointResolverV2 = customEndpointResolver{}
  256. })
  257. }
  258. // GetAWSConfig returns the aws config or an error.
  259. func GetAWSConfig(cfg *aws.Config) (*aws.Config, error) {
  260. return cfg, nil
  261. }