Skip to content

Commit b71bb38

Browse files
authored
Port gcp join method to new join service (#61205)
* Port `gcp` join method to new join service This ports the `gcp` join method to the new join service. It moves join-related code from `lib/gcp` to `lib/join/gcp`, extracts common joining logic to `lib/join/gcp` for reuse between the legacy and new entrypoints, and adds a compatibility layer for use by the legacy client. See also: [RFD 27e](https://github.com/gravitational/teleport.e/blob/master/rfd/0027e-auth-assigned-uuids.md) * Add GCP to new join service whitelist * Rename checkAndSetDefaults() to validate()
1 parent 21d1d0b commit b71bb38

File tree

13 files changed

+477
-289
lines changed

13 files changed

+477
-289
lines changed

lib/auth/auth.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ import (
111111
"github.com/gravitational/teleport/lib/devicetrust/assertserver"
112112
dtconfig "github.com/gravitational/teleport/lib/devicetrust/config"
113113
"github.com/gravitational/teleport/lib/events"
114-
"github.com/gravitational/teleport/lib/gcp"
115114
"github.com/gravitational/teleport/lib/integrations/awsra/createsession"
116115
"github.com/gravitational/teleport/lib/inventory"
117116
iterstream "github.com/gravitational/teleport/lib/itertools/stream"
@@ -120,6 +119,7 @@ import (
120119
joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair"
121120
"github.com/gravitational/teleport/lib/join/ec2join"
122121
"github.com/gravitational/teleport/lib/join/env0"
122+
"github.com/gravitational/teleport/lib/join/gcp"
123123
"github.com/gravitational/teleport/lib/join/githubactions"
124124
"github.com/gravitational/teleport/lib/join/gitlab"
125125
kubetoken "github.com/gravitational/teleport/lib/kube/token"
@@ -1293,7 +1293,7 @@ type Server struct {
12931293

12941294
// gcpIDTokenValidator allows ID tokens from GCP to be validated by the auth
12951295
// server. It can be overridden for the purpose of tests.
1296-
gcpIDTokenValidator gcpIDTokenValidator
1296+
gcpIDTokenValidator gcp.Validator
12971297

12981298
// terraformIDTokenValidator allows JWTs from Terraform Cloud to be
12991299
// validated by the auth server using a known JWKS. It can be overridden for

lib/auth/export_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,6 @@ func (a *Server) SetCircleCITokenValidate(validator func(ctx context.Context, or
205205
a.circleCITokenValidate = validator
206206
}
207207

208-
func (a *Server) SetGCPIDTokenValidator(validator gcpIDTokenValidator) {
209-
a.gcpIDTokenValidator = validator
210-
}
211-
212208
func (a *Server) SetK8sTokenReviewValidator(validator k8sTokenReviewValidator) {
213209
a.k8sTokenReviewValidator = validator
214210
}
@@ -361,10 +357,6 @@ func ValidateGithubAuthCallbackHelper(ctx context.Context, m GitHubManager, diag
361357
return validateGithubAuthCallbackHelper(ctx, m, diagCtx, q, emitter, logger)
362358
}
363359

364-
func IsGCPZoneInLocation(rawLocation, rawZone string) bool {
365-
return isGCPZoneInLocation(rawLocation, rawZone)
366-
}
367-
368360
func FormatHeaderFromMap(m map[string]string) http.Header {
369361
return formatHeaderFromMap(m)
370362
}

lib/auth/join/join.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,11 @@ func Register(ctx context.Context, params RegisterParams) (result *RegisterResul
346346
return nil, trace.Wrap(err)
347347
}
348348
case types.JoinMethodGCP:
349-
params.IDToken, err = gcp.GetIDToken(ctx)
350-
if err != nil {
351-
return nil, trace.Wrap(err)
349+
if params.IDToken == "" {
350+
params.IDToken, err = gcp.GetIDToken(ctx)
351+
if err != nil {
352+
return nil, trace.Wrap(err)
353+
}
352354
}
353355
case types.JoinMethodSpacelift:
354356
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()

lib/auth/join_gcp.go

Lines changed: 19 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -20,130 +20,36 @@ package auth
2020

2121
import (
2222
"context"
23-
"slices"
24-
"strings"
2523

2624
"github.com/gravitational/trace"
2725

2826
"github.com/gravitational/teleport/api/types"
29-
"github.com/gravitational/teleport/lib/gcp"
27+
"github.com/gravitational/teleport/lib/join/gcp"
3028
)
3129

32-
type gcpIDTokenValidator interface {
33-
Validate(ctx context.Context, token string) (*gcp.IDTokenClaims, error)
30+
// GetGCPIDTokenValidator returns the server's configured GCP ID token
31+
// validator.
32+
func (a *Server) GetGCPIDTokenValidator() gcp.Validator {
33+
return a.gcpIDTokenValidator
34+
}
35+
36+
// SetGCPIDTokenValidator sets a new GCP ID token validator, used in tests.
37+
func (a *Server) SetGCPIDTokenValidator(validator gcp.Validator) {
38+
a.gcpIDTokenValidator = validator
3439
}
3540

3641
func (a *Server) checkGCPJoinRequest(
3742
ctx context.Context,
3843
req *types.RegisterUsingTokenRequest,
3944
pt types.ProvisionToken,
4045
) (*gcp.IDTokenClaims, error) {
41-
if req.IDToken == "" {
42-
return nil, trace.BadParameter("IDToken not provided for GCP join request")
43-
}
44-
token, ok := pt.(*types.ProvisionTokenV2)
45-
if !ok {
46-
return nil, trace.BadParameter("gcp join method only supports ProvisionTokenV2, '%T' was provided", pt)
47-
}
48-
49-
claims, err := a.gcpIDTokenValidator.Validate(ctx, req.IDToken)
50-
if err != nil {
51-
a.logger.WarnContext(ctx, "Unable to validate GCP IDToken",
52-
"error", err,
53-
"claims", claims,
54-
"token", pt.GetName(),
55-
)
56-
return nil, trace.Wrap(err)
57-
}
58-
59-
a.logger.InfoContext(ctx, "GCP VM trying to join cluster",
60-
"claims", claims,
61-
"token", pt.GetName(),
62-
)
63-
64-
if err := checkGCPAllowRules(token, claims); err != nil {
65-
return nil, trace.Wrap(err)
66-
}
67-
68-
return claims, nil
69-
}
70-
71-
func checkGCPAllowRules(token *types.ProvisionTokenV2, claims *gcp.IDTokenClaims) error {
72-
compute := claims.Google.ComputeEngine
73-
// unmatchedLocation is true if the location restriction is set and the "google.compute_engine.zone"
74-
// claim is not present in the IDToken. This happens when the joining node is not a GCE VM.
75-
unmatchedLocation := false
76-
// If a single rule passes, accept the IDToken.
77-
for _, rule := range token.Spec.GCP.Allow {
78-
if !slices.Contains(rule.ProjectIDs, compute.ProjectID) {
79-
continue
80-
}
81-
82-
if len(rule.ServiceAccounts) > 0 && !slices.Contains(rule.ServiceAccounts, claims.Email) {
83-
continue
84-
}
85-
86-
if len(rule.Locations) > 0 && !slices.ContainsFunc(rule.Locations, func(location string) bool {
87-
return isGCPZoneInLocation(location, compute.Zone)
88-
}) {
89-
unmatchedLocation = true
90-
continue
91-
}
92-
93-
// All provided rules met.
94-
return nil
95-
}
96-
97-
// If the location restriction is set and the "google.compute_engine.zone" claim is not present in the IDToken,
98-
// return a more specific error message.
99-
if unmatchedLocation && compute.Zone == "" {
100-
return trace.CompareFailed("id token %q claim is empty and didn't match the %q. "+
101-
"Services running outside of GCE VM instances are incompatible with %q restriction.", "google.compute_engine.zone", "locations", "location")
102-
}
103-
return trace.AccessDenied("id token claims did not match any allow rules")
104-
}
105-
106-
type gcpLocation struct {
107-
globalLocation string
108-
region string
109-
zone string
110-
}
111-
112-
func parseGCPLocation(location string) (*gcpLocation, error) {
113-
parts := strings.Split(location, "-")
114-
if len(parts) < 2 || len(parts) > 3 {
115-
return nil, trace.BadParameter("location %q is not a valid GCP region or zone", location)
116-
}
117-
globalLocation, region := parts[0], parts[1]
118-
var zone string
119-
if len(parts) == 3 {
120-
zone = parts[2]
121-
}
122-
return &gcpLocation{
123-
globalLocation: globalLocation,
124-
region: region,
125-
zone: zone,
126-
}, nil
127-
}
128-
129-
// isGCPZoneInLocation checks if a zone belongs to a location, which can be
130-
// either a zone or region.
131-
func isGCPZoneInLocation(rawLocation, rawZone string) bool {
132-
location, err := parseGCPLocation(rawLocation)
133-
if err != nil {
134-
return false
135-
}
136-
zone, err := parseGCPLocation(rawZone)
137-
if err != nil {
138-
return false
139-
}
140-
// Make sure zone is, in fact, a zone.
141-
if zone.zone == "" {
142-
return false
143-
}
144-
145-
if location.globalLocation != zone.globalLocation || location.region != zone.region {
146-
return false
147-
}
148-
return location.zone == "" || location.zone == zone.zone
46+
claims, err := gcp.CheckIDToken(ctx, &gcp.CheckIDTokenParams{
47+
ProvisionToken: pt,
48+
IDToken: []byte(req.IDToken),
49+
Validator: a.gcpIDTokenValidator,
50+
})
51+
52+
// Where possible, try to return any extracted claims along with the error
53+
// to improve audit logs for failed join attempts.
54+
return claims, trace.Wrap(err)
14955
}

lib/gcp/gcp.go

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)