workload_identity.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 secretmanager
  13. import (
  14. "bytes"
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "time"
  21. "cloud.google.com/go/compute/metadata"
  22. iam "cloud.google.com/go/iam/credentials/apiv1"
  23. "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
  24. gsmapiv1 "cloud.google.com/go/secretmanager/apiv1"
  25. "github.com/googleapis/gax-go/v2"
  26. "golang.org/x/oauth2"
  27. "google.golang.org/api/option"
  28. "google.golang.org/grpc"
  29. "google.golang.org/grpc/credentials"
  30. "grpc.go4.org/credentials/oauth"
  31. authenticationv1 "k8s.io/api/authentication/v1"
  32. v1 "k8s.io/api/core/v1"
  33. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  34. "k8s.io/apimachinery/pkg/types"
  35. "k8s.io/client-go/kubernetes"
  36. clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  37. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  38. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  39. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  40. "github.com/external-secrets/external-secrets/pkg/constants"
  41. "github.com/external-secrets/external-secrets/pkg/metrics"
  42. )
  43. const (
  44. gcpSAAnnotation = "iam.gke.io/gcp-service-account"
  45. errFetchPodToken = "unable to fetch pod token: %w"
  46. errFetchIBToken = "unable to fetch identitybindingtoken: %w"
  47. errGenAccessToken = "unable to generate gcp access token: %w"
  48. errLookupIdentity = "unable to lookup workload identity: %w"
  49. errNoProjectID = "unable to find ProjectID in storeSpec"
  50. )
  51. var (
  52. // defaultUniverseDomain is the domain which will be used in the STS token URL.
  53. defaultUniverseDomain = "googleapis.com"
  54. // workloadIdentitySubjectTokenType is the STS token type used in Oauth2.0 token exchange.
  55. workloadIdentitySubjectTokenType = "urn:ietf:params:oauth:token-type:jwt"
  56. // workloadIdentitySubjectTokenType is the STS token type used in Oauth2.0 token exchange with AWS.
  57. workloadIdentitySubjectTokenTypeAWS = "urn:ietf:params:aws:token-type:aws4_request"
  58. // workloadIdentityTokenGrantType is the grant type for OAuth 2.0 token exchange .
  59. workloadIdentityTokenGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
  60. // workloadIdentityRequestedTokenType is the requested type for OAuth 2.0 access token.
  61. workloadIdentityRequestedTokenType = "urn:ietf:params:oauth:token-type:access_token"
  62. // workloadIdentityTokenURL is the token service endpoint.
  63. workloadIdentityTokenURL = "https://sts.googleapis.com/v1/token"
  64. // workloadIdentityTokenInfoURL is the STS introspection service endpoint.
  65. workloadIdentityTokenInfoURL = "https://sts.googleapis.com/v1/introspect"
  66. )
  67. // workloadIdentity holds all clients and generators needed
  68. // to create a gcp oauth token.
  69. type workloadIdentity struct {
  70. iamClient IamClient
  71. metadataClient MetadataClient
  72. idBindTokenGenerator idBindTokenGenerator
  73. saTokenGenerator saTokenGenerator
  74. clusterProjectID string
  75. }
  76. // interface to GCP IAM API.
  77. type IamClient interface {
  78. GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
  79. Close() error
  80. }
  81. // interface to GCP Metadata API.
  82. type MetadataClient interface {
  83. InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error)
  84. ProjectIDWithContext(ctx context.Context) (string, error)
  85. }
  86. // interface to securetoken/identitybindingtoken API.
  87. type idBindTokenGenerator interface {
  88. Generate(context.Context, *http.Client, string, string, string) (*oauth2.Token, error)
  89. }
  90. // interface to kubernetes serviceaccount token request API.
  91. type saTokenGenerator interface {
  92. Generate(context.Context, []string, string, string) (*authenticationv1.TokenRequest, error)
  93. }
  94. func newWorkloadIdentity(ctx context.Context, projectID string) (*workloadIdentity, error) {
  95. satg, err := newSATokenGenerator()
  96. if err != nil {
  97. return nil, err
  98. }
  99. iamc, err := newIAMClient(ctx)
  100. if err != nil {
  101. return nil, err
  102. }
  103. return &workloadIdentity{
  104. iamClient: iamc,
  105. metadataClient: newMetadataClient(),
  106. idBindTokenGenerator: newIDBindTokenGenerator(),
  107. saTokenGenerator: satg,
  108. clusterProjectID: projectID,
  109. }, nil
  110. }
  111. func (w *workloadIdentity) gcpWorkloadIdentity(ctx context.Context, id *esv1.GCPWorkloadIdentity) (string, string, error) {
  112. var err error
  113. projectID := id.ClusterProjectID
  114. if projectID == "" {
  115. if projectID, err = w.metadataClient.ProjectIDWithContext(ctx); err != nil {
  116. return "", "", fmt.Errorf("unable to get project id: %w", err)
  117. }
  118. }
  119. clusterLocation := id.ClusterLocation
  120. if clusterLocation == "" {
  121. if clusterLocation, err = w.metadataClient.InstanceAttributeValueWithContext(ctx, "cluster-location"); err != nil {
  122. return "", "", fmt.Errorf("unable to determine cluster location: %w", err)
  123. }
  124. }
  125. clusterName := id.ClusterName
  126. if clusterName == "" {
  127. if clusterName, err = w.metadataClient.InstanceAttributeValueWithContext(ctx, "cluster-name"); err != nil {
  128. return "", "", fmt.Errorf("unable to determine cluster name: %w", err)
  129. }
  130. }
  131. idPool := fmt.Sprintf("%s.svc.id.goog", projectID)
  132. idProvider := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s",
  133. projectID,
  134. clusterLocation,
  135. clusterName,
  136. )
  137. return idPool, idProvider, nil
  138. }
  139. func (w *workloadIdentity) TokenSource(ctx context.Context, auth esv1.GCPSMAuth, isClusterKind bool, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
  140. wi := auth.WorkloadIdentity
  141. if wi == nil {
  142. return nil, nil
  143. }
  144. saKey := types.NamespacedName{
  145. Name: wi.ServiceAccountRef.Name,
  146. Namespace: namespace,
  147. }
  148. // only ClusterStore is allowed to set namespace (and then it's required)
  149. if isClusterKind && wi.ServiceAccountRef.Namespace != nil {
  150. saKey.Namespace = *wi.ServiceAccountRef.Namespace
  151. }
  152. sa := &v1.ServiceAccount{}
  153. err := kube.Get(ctx, saKey, sa)
  154. if err != nil {
  155. return nil, err
  156. }
  157. idPool, idProvider, err := w.gcpWorkloadIdentity(ctx, wi)
  158. if err != nil {
  159. return nil, fmt.Errorf(errLookupIdentity, err)
  160. }
  161. audiences := []string{idPool}
  162. if len(wi.ServiceAccountRef.Audiences) > 0 {
  163. audiences = append(audiences, wi.ServiceAccountRef.Audiences...)
  164. }
  165. gcpSA := sa.Annotations[gcpSAAnnotation]
  166. resp, err := w.saTokenGenerator.Generate(ctx, audiences, saKey.Name, saKey.Namespace)
  167. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGenerateSAToken, err)
  168. if err != nil {
  169. return nil, fmt.Errorf(errFetchPodToken, err)
  170. }
  171. idBindToken, err := w.idBindTokenGenerator.Generate(ctx, http.DefaultClient, resp.Status.Token, idPool, idProvider)
  172. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGenerateIDBindToken, err)
  173. if err != nil {
  174. return nil, fmt.Errorf(errFetchIBToken, err)
  175. }
  176. // If no `iam.gke.io/gcp-service-account` annotation is present the
  177. // identitybindingtoken will be used directly, allowing bindings on secrets
  178. // of the form "serviceAccount:<project>.svc.id.goog[<namespace>/<sa>]".
  179. if gcpSA == "" {
  180. return oauth2.StaticTokenSource(idBindToken), nil
  181. }
  182. gcpSAResp, err := w.iamClient.GenerateAccessToken(ctx, &credentialspb.GenerateAccessTokenRequest{
  183. Name: fmt.Sprintf("projects/-/serviceAccounts/%s", gcpSA),
  184. Scope: gsmapiv1.DefaultAuthScopes(),
  185. }, gax.WithGRPCOptions(grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(idBindToken)})))
  186. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGenerateAccessToken, err)
  187. if err != nil {
  188. return nil, fmt.Errorf(errGenAccessToken, err)
  189. }
  190. return oauth2.StaticTokenSource(&oauth2.Token{
  191. AccessToken: gcpSAResp.GetAccessToken(),
  192. }), nil
  193. }
  194. func (w *workloadIdentity) Close() error {
  195. if w.iamClient != nil {
  196. return w.iamClient.Close()
  197. }
  198. return nil
  199. }
  200. func newIAMClient(ctx context.Context) (IamClient, error) {
  201. iamOpts := []option.ClientOption{
  202. option.WithUserAgent("external-secrets-operator"),
  203. // tell the secretmanager library to not add transport-level ADC since
  204. // we need to override on a per call basis
  205. option.WithoutAuthentication(),
  206. // grpc oauth TokenSource credentials require transport security, so
  207. // this must be set explicitly even though TLS is used
  208. option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewTLS(nil))),
  209. option.WithGRPCConnectionPool(5),
  210. }
  211. return iam.NewIamCredentialsClient(ctx, iamOpts...)
  212. }
  213. func newMetadataClient() MetadataClient {
  214. return metadata.NewClient(&http.Client{
  215. Timeout: 5 * time.Second,
  216. })
  217. }
  218. type k8sSATokenGenerator struct {
  219. corev1 clientcorev1.CoreV1Interface
  220. }
  221. func (g *k8sSATokenGenerator) Generate(ctx context.Context, audiences []string, name, namespace string) (*authenticationv1.TokenRequest, error) {
  222. // Request a serviceaccount token for the pod
  223. ttl := int64((15 * time.Minute).Seconds())
  224. return g.corev1.
  225. ServiceAccounts(namespace).
  226. CreateToken(ctx, name,
  227. &authenticationv1.TokenRequest{
  228. Spec: authenticationv1.TokenRequestSpec{
  229. ExpirationSeconds: &ttl,
  230. Audiences: audiences,
  231. },
  232. },
  233. metav1.CreateOptions{},
  234. )
  235. }
  236. func newSATokenGenerator() (saTokenGenerator, error) {
  237. cfg, err := ctrlcfg.GetConfig()
  238. if err != nil {
  239. return nil, err
  240. }
  241. clientset, err := kubernetes.NewForConfig(cfg)
  242. if err != nil {
  243. return nil, err
  244. }
  245. return &k8sSATokenGenerator{
  246. corev1: clientset.CoreV1(),
  247. }, nil
  248. }
  249. // Trades the kubernetes token for an identitybindingtoken token.
  250. type gcpIDBindTokenGenerator struct {
  251. targetURL string
  252. }
  253. func newIDBindTokenGenerator() idBindTokenGenerator {
  254. return &gcpIDBindTokenGenerator{
  255. targetURL: "https://securetoken.googleapis.com/v1/identitybindingtoken",
  256. }
  257. }
  258. func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
  259. body, err := json.Marshal(map[string]string{
  260. "grant_type": workloadIdentityTokenGrantType,
  261. "subject_token_type": workloadIdentitySubjectTokenType,
  262. "requested_token_type": workloadIdentityRequestedTokenType,
  263. "subject_token": k8sToken,
  264. "audience": fmt.Sprintf("identitynamespace:%s:%s", idPool, idProvider),
  265. "scope": CloudPlatformRole,
  266. })
  267. if err != nil {
  268. return nil, err
  269. }
  270. req, err := http.NewRequestWithContext(ctx, "POST", g.targetURL, bytes.NewBuffer(body))
  271. if err != nil {
  272. return nil, err
  273. }
  274. req.Header.Set("Content-Type", "application/json")
  275. resp, err := client.Do(req)
  276. if err != nil {
  277. return nil, err
  278. }
  279. if resp.StatusCode != http.StatusOK {
  280. return nil, fmt.Errorf("could not get idbindtoken token, status: %v", resp.StatusCode)
  281. }
  282. defer func() {
  283. _ = resp.Body.Close()
  284. }()
  285. respBody, err := io.ReadAll(resp.Body)
  286. if err != nil {
  287. return nil, err
  288. }
  289. idBindToken := &oauth2.Token{}
  290. if err := json.Unmarshal(respBody, idBindToken); err != nil {
  291. return nil, err
  292. }
  293. return idBindToken, nil
  294. }