mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Port spacelift join method to new join service (#61652)
* Port `spacelift` join method to new join service This ports the `spacelift` join method to the new join service. It moves the core validation logic from `lib/spacelift` into `lib/join`, and adds a small compatibility layer to allow it to be reused between the new and legacy join services. Where possible, existing logic remains untouched. See also: [RFD 27e](https://github.com/gravitational/teleport.e/blob/master/rfd/0027e-auth-assigned-uuids.md) * Remove duped error declaration * Move errMockInvalidToken again
This commit is contained in:
+2
-2
@@ -122,6 +122,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/join/gcp"
|
||||
"github.com/gravitational/teleport/lib/join/githubactions"
|
||||
"github.com/gravitational/teleport/lib/join/gitlab"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
"github.com/gravitational/teleport/lib/join/terraformcloud"
|
||||
"github.com/gravitational/teleport/lib/join/tpmjoin"
|
||||
kubetoken "github.com/gravitational/teleport/lib/kube/token"
|
||||
@@ -138,7 +139,6 @@ import (
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
"github.com/gravitational/teleport/lib/services/readonly"
|
||||
"github.com/gravitational/teleport/lib/spacelift"
|
||||
"github.com/gravitational/teleport/lib/sshca"
|
||||
"github.com/gravitational/teleport/lib/sshutils"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
@@ -1276,7 +1276,7 @@ type Server struct {
|
||||
|
||||
// spaceliftIDTokenValidator allows ID tokens from Spacelift to be validated
|
||||
// by the auth server. It can be overridden for the purpose of tests.
|
||||
spaceliftIDTokenValidator spaceliftIDTokenValidator
|
||||
spaceliftIDTokenValidator spacelift.Validator
|
||||
|
||||
// gitlabIDTokenValidator allows ID tokens from GitLab CI to be validated by
|
||||
// the auth server. It can be overridden for the purpose of tests.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -78,6 +79,8 @@ import (
|
||||
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
||||
)
|
||||
|
||||
var errMockInvalidToken = errors.New("invalid token")
|
||||
|
||||
func renewBotCerts(
|
||||
ctx context.Context,
|
||||
srv *authtest.TLSServer,
|
||||
|
||||
@@ -199,10 +199,6 @@ func (a *Server) SetK8sTokenReviewValidator(validator k8sTokenReviewValidator) {
|
||||
a.k8sTokenReviewValidator = validator
|
||||
}
|
||||
|
||||
func (a *Server) SetSpaceliftIDTokenValidator(validator spaceliftIDTokenValidator) {
|
||||
a.spaceliftIDTokenValidator = validator
|
||||
}
|
||||
|
||||
func (a *Server) SetCreateBoundKeypairValidator(validator boundkeypair.CreateBoundKeypairValidator) {
|
||||
a.createBoundKeypairValidator = validator
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ import (
|
||||
"github.com/gravitational/teleport/lib/join/circleci"
|
||||
"github.com/gravitational/teleport/lib/join/githubactions"
|
||||
"github.com/gravitational/teleport/lib/join/gitlab"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
"github.com/gravitational/teleport/lib/join/terraformcloud"
|
||||
"github.com/gravitational/teleport/lib/jwt"
|
||||
kubetoken "github.com/gravitational/teleport/lib/kube/token"
|
||||
"github.com/gravitational/teleport/lib/spacelift"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/tpm"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -361,9 +361,11 @@ func Register(ctx context.Context, params RegisterParams) (result *RegisterResul
|
||||
}
|
||||
}
|
||||
case types.JoinMethodSpacelift:
|
||||
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
if params.IDToken == "" {
|
||||
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
case types.JoinMethodTerraformCloud:
|
||||
if params.IDToken == "" {
|
||||
|
||||
+19
-78
@@ -20,20 +20,23 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/join/joinutils"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/spacelift"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
)
|
||||
|
||||
type spaceliftIDTokenValidator interface {
|
||||
Validate(
|
||||
ctx context.Context, domain string, token string,
|
||||
) (*spacelift.IDTokenClaims, error)
|
||||
// GetSpaceliftIDTokenValidator returns the server's currently configured
|
||||
// Spacelift OIDC token validator.
|
||||
func (a *Server) GetSpaceliftIDTokenValidator() spacelift.Validator {
|
||||
return a.spaceliftIDTokenValidator
|
||||
}
|
||||
|
||||
// SetSpaceliftIDTokenValidator sets the current Spacelift OIDC token validator,
|
||||
// used in tests.
|
||||
func (a *Server) SetSpaceliftIDTokenValidator(validator spacelift.Validator) {
|
||||
a.spaceliftIDTokenValidator = validator
|
||||
}
|
||||
|
||||
func (a *Server) checkSpaceliftJoinRequest(
|
||||
@@ -41,75 +44,13 @@ func (a *Server) checkSpaceliftJoinRequest(
|
||||
req *types.RegisterUsingTokenRequest,
|
||||
pt types.ProvisionToken,
|
||||
) (*spacelift.IDTokenClaims, error) {
|
||||
if req.IDToken == "" {
|
||||
return nil, trace.BadParameter("id_token not provided for spacelift join request")
|
||||
}
|
||||
token, ok := pt.(*types.ProvisionTokenV2)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("spacelift join method only supports ProvisionTokenV2, '%T' was provided", pt)
|
||||
}
|
||||
claims, err := spacelift.CheckIDToken(ctx, &spacelift.CheckIDTokenParams{
|
||||
ProvisionToken: pt,
|
||||
IDToken: []byte(req.IDToken),
|
||||
Validator: a.spaceliftIDTokenValidator,
|
||||
})
|
||||
|
||||
if modules.GetModules().BuildType() != modules.BuildEnterprise {
|
||||
return nil, fmt.Errorf(
|
||||
"spacelift joining: %w",
|
||||
ErrRequiresEnterprise,
|
||||
)
|
||||
}
|
||||
|
||||
claims, err := a.spaceliftIDTokenValidator.Validate(
|
||||
ctx, token.Spec.Spacelift.Hostname, req.IDToken,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
a.logger.InfoContext(ctx, "Spacelift run trying to join cluster",
|
||||
"claims", claims,
|
||||
"token", pt.GetName(),
|
||||
)
|
||||
|
||||
return claims, trace.Wrap(checkSpaceliftAllowRules(token, claims))
|
||||
}
|
||||
|
||||
func checkSpaceliftAllowRules(token *types.ProvisionTokenV2, claims *spacelift.IDTokenClaims) error {
|
||||
globCheck := func(want string, got string) (bool, error) {
|
||||
if token.Spec.Spacelift.EnableGlobMatching {
|
||||
return joinutils.GlobMatchAllowEmptyPattern(want, got)
|
||||
}
|
||||
if want == "" {
|
||||
return true, nil
|
||||
}
|
||||
return want == got, nil
|
||||
}
|
||||
|
||||
// If a single rule passes, accept the IDToken
|
||||
for i, rule := range token.Spec.Spacelift.Allow {
|
||||
// Please consider keeping these field validators in the same order they
|
||||
// are defined within the ProvisionTokenSpecV2Spacelift proto spec.
|
||||
spaceIDMatch, err := globCheck(rule.SpaceID, claims.SpaceID)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "evaluating rule (%d) space_id glob match", i)
|
||||
}
|
||||
if !spaceIDMatch {
|
||||
continue
|
||||
}
|
||||
callerIDMatch, err := globCheck(rule.CallerID, claims.CallerID)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "evaluating rule (%d) caller_id glob match", i)
|
||||
}
|
||||
if !callerIDMatch {
|
||||
continue
|
||||
}
|
||||
if rule.CallerType != "" && claims.CallerType != rule.CallerType {
|
||||
continue
|
||||
}
|
||||
if rule.Scope != "" && claims.Scope != rule.Scope {
|
||||
continue
|
||||
}
|
||||
|
||||
// All provided rules met.
|
||||
return nil
|
||||
}
|
||||
|
||||
return trace.AccessDenied("id token claims did not match any allow rules")
|
||||
// Attempt to return any claims along with the error, used to improve audit
|
||||
// logging on failed join attempts.
|
||||
return claims, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -16,29 +16,28 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package auth_test
|
||||
package join_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/auth"
|
||||
"github.com/gravitational/teleport/lib/auth/authtest"
|
||||
"github.com/gravitational/teleport/lib/auth/state"
|
||||
"github.com/gravitational/teleport/lib/auth/testauthority"
|
||||
"github.com/gravitational/teleport/lib/join/joinclient"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/modules/modulestest"
|
||||
"github.com/gravitational/teleport/lib/spacelift"
|
||||
)
|
||||
|
||||
var errMockInvalidToken = errors.New("invalid token")
|
||||
|
||||
type mockSpaceliftTokenValidator struct {
|
||||
tokens map[string]spacelift.IDTokenClaims
|
||||
}
|
||||
@@ -58,7 +57,7 @@ func (m *mockSpaceliftTokenValidator) Validate(
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
func TestJoinSpacelift(t *testing.T) {
|
||||
validIDToken := "test.fake.jwt"
|
||||
idTokenValidator := &mockSpaceliftTokenValidator{
|
||||
tokens: map[string]spacelift.IDTokenClaims{
|
||||
@@ -73,18 +72,19 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var withTokenValidator auth.ServerOption = func(server *auth.Server) error {
|
||||
server.SetSpaceliftIDTokenValidator(idTokenValidator)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
p, err := newTestPack(ctx, testPackOptions{
|
||||
DataDir: t.TempDir(),
|
||||
MutateAuth: withTokenValidator,
|
||||
|
||||
authServer, err := authtest.NewTestServer(authtest.ServerConfig{
|
||||
Auth: authtest.AuthServerConfig{
|
||||
Dir: t.TempDir(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
auth := p.a
|
||||
t.Cleanup(func() { assert.NoError(t, authServer.Shutdown(t.Context())) })
|
||||
auth := authServer.Auth()
|
||||
|
||||
auth.SetSpaceliftIDTokenValidator(idTokenValidator)
|
||||
|
||||
// helper for creating RegisterUsingTokenRequest
|
||||
sshPrivateKey, sshPublicKey, err := testauthority.New().GenerateKeyPair()
|
||||
@@ -142,7 +142,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "success with glob",
|
||||
name: "success-with-glob",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -162,7 +162,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "fail with glob",
|
||||
name: "fail-with-glob",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -181,7 +181,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "fail with disabled glob",
|
||||
name: "fail-with-disabled-glob",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -201,7 +201,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "missing enterprise",
|
||||
name: "missing-enterprise",
|
||||
setEnterprise: false,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -219,7 +219,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple allow rules",
|
||||
name: "multiple-allow-rules",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -238,7 +238,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "incorrect space_id",
|
||||
name: "incorrect-space_id",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -256,7 +256,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "incorrect caller_id",
|
||||
name: "incorrect-caller_id",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -274,7 +274,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "incorrect caller_type",
|
||||
name: "incorrect-caller_type",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -292,7 +292,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "incorrect scope",
|
||||
name: "incorrect-scope",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -310,7 +310,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
assertError: allowRulesNotMatched,
|
||||
},
|
||||
{
|
||||
name: "invalid token",
|
||||
name: "invalid-token",
|
||||
setEnterprise: true,
|
||||
tokenSpec: types.ProvisionTokenSpecV2{
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
@@ -324,7 +324,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
},
|
||||
request: newRequest("some other token"),
|
||||
assertError: func(t require.TestingT, err error, i ...any) {
|
||||
require.ErrorIs(t, err, errMockInvalidToken)
|
||||
require.ErrorContains(t, err, "invalid token")
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -344,8 +344,42 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
|
||||
require.NoError(t, auth.CreateToken(ctx, token))
|
||||
tt.request.Token = tt.name
|
||||
|
||||
_, err = auth.RegisterUsingToken(ctx, tt.request)
|
||||
tt.assertError(t, err)
|
||||
nopClient, err := authServer.NewClient(authtest.TestNop())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("legacy", func(t *testing.T) {
|
||||
_, err = auth.RegisterUsingToken(ctx, tt.request)
|
||||
tt.assertError(t, err)
|
||||
})
|
||||
|
||||
t.Run("legacy joinclient", func(t *testing.T) {
|
||||
_, err := joinclient.LegacyJoin(t.Context(), joinclient.JoinParams{
|
||||
Token: tt.request.Token,
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
ID: state.IdentityID{
|
||||
Role: tt.request.Role,
|
||||
NodeName: "testnode",
|
||||
HostUUID: tt.request.HostID,
|
||||
},
|
||||
IDToken: tt.request.IDToken,
|
||||
AuthClient: nopClient,
|
||||
})
|
||||
tt.assertError(t, err)
|
||||
})
|
||||
|
||||
t.Run("new joinclient", func(t *testing.T) {
|
||||
_, err := joinclient.Join(t.Context(), joinclient.JoinParams{
|
||||
Token: tt.request.Token,
|
||||
JoinMethod: types.JoinMethodSpacelift,
|
||||
ID: state.IdentityID{
|
||||
Role: types.RoleInstance, // RoleNode is not allowed
|
||||
NodeName: "testnode",
|
||||
},
|
||||
IDToken: tt.request.IDToken,
|
||||
AuthClient: nopClient,
|
||||
})
|
||||
tt.assertError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/join/gitlab"
|
||||
"github.com/gravitational/teleport/lib/join/internal/messages"
|
||||
"github.com/gravitational/teleport/lib/join/joinv1"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
"github.com/gravitational/teleport/lib/join/terraformcloud"
|
||||
"github.com/gravitational/teleport/lib/utils/hostid"
|
||||
)
|
||||
@@ -213,6 +214,7 @@ func joinWithClient(ctx context.Context, params JoinParams, client *joinv1.Clien
|
||||
types.JoinMethodGitLab,
|
||||
types.JoinMethodIAM,
|
||||
types.JoinMethodOracle,
|
||||
types.JoinMethodSpacelift,
|
||||
types.JoinMethodTPM,
|
||||
types.JoinMethodTerraformCloud:
|
||||
joinMethod := string(params.JoinMethod)
|
||||
@@ -371,6 +373,15 @@ func joinWithMethod(
|
||||
}
|
||||
}
|
||||
|
||||
return oidcJoin(stream, joinParams, clientParams)
|
||||
case types.JoinMethodSpacelift:
|
||||
if joinParams.IDToken == "" {
|
||||
joinParams.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return oidcJoin(stream, joinParams, clientParams)
|
||||
case types.JoinMethodTPM:
|
||||
return tpmJoin(ctx, stream, joinParams, clientParams)
|
||||
|
||||
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/join/joinutils"
|
||||
"github.com/gravitational/teleport/lib/join/oraclejoin"
|
||||
"github.com/gravitational/teleport/lib/join/provision"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
"github.com/gravitational/teleport/lib/join/terraformcloud"
|
||||
"github.com/gravitational/teleport/lib/join/tpmjoin"
|
||||
"github.com/gravitational/teleport/lib/scopes/joining"
|
||||
@@ -97,6 +98,7 @@ type AuthService interface {
|
||||
GetGHAIDTokenJWKSValidator() githubactions.GithubIDTokenJWKSValidator
|
||||
GetGitlabIDTokenValidator() gitlab.Validator
|
||||
GetTPMValidator() tpmjoin.TPMValidator
|
||||
GetSpaceliftIDTokenValidator() spacelift.Validator
|
||||
GetTerraformIDTokenValidator() terraformcloud.Validator
|
||||
services.Presence
|
||||
}
|
||||
@@ -314,6 +316,8 @@ func (s *Server) handleJoinMethod(
|
||||
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateGithubToken)
|
||||
case types.JoinMethodGitLab:
|
||||
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateGitlabToken)
|
||||
case types.JoinMethodSpacelift:
|
||||
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateSpaceliftToken)
|
||||
case types.JoinMethodTPM:
|
||||
return s.handleTPMJoin(stream, authCtx, clientInit, token)
|
||||
case types.JoinMethodTerraformCloud:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package join
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
workloadidentityv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
||||
"github.com/gravitational/teleport/lib/join/provision"
|
||||
"github.com/gravitational/teleport/lib/join/spacelift"
|
||||
)
|
||||
|
||||
func (a *Server) validateSpaceliftToken(
|
||||
ctx context.Context,
|
||||
pt provision.Token,
|
||||
idToken []byte,
|
||||
) (any, *workloadidentityv1.JoinAttrs, error) {
|
||||
claims, err := spacelift.CheckIDToken(ctx, &spacelift.CheckIDTokenParams{
|
||||
ProvisionToken: pt,
|
||||
IDToken: idToken,
|
||||
Validator: a.cfg.AuthService.GetSpaceliftIDTokenValidator(),
|
||||
})
|
||||
|
||||
// If possible, attach claims and workload ID attrs regardless of the error
|
||||
// return. If the token fails to validate, these claims will ensure audit
|
||||
// events remain useful.
|
||||
var workloadIDAttrs *workloadidentityv1.JoinAttrs
|
||||
if claims != nil {
|
||||
workloadIDAttrs = &workloadidentityv1.JoinAttrs{
|
||||
Spacelift: claims.JoinAttrs(),
|
||||
}
|
||||
}
|
||||
|
||||
return claims, workloadIDAttrs, trace.Wrap(err)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package spacelift
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/join/joinutils"
|
||||
"github.com/gravitational/teleport/lib/join/provision"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||||
)
|
||||
|
||||
var log = logutils.NewPackageLogger(teleport.ComponentKey, "spacelift")
|
||||
|
||||
type Validator interface {
|
||||
Validate(
|
||||
ctx context.Context, domain string, token string,
|
||||
) (*IDTokenClaims, error)
|
||||
}
|
||||
|
||||
// IDTokenClaims
|
||||
// See the following for the structure:
|
||||
// https://docs.spacelift.io/integrations/cloud-providers/oidc/#standard-claims
|
||||
type IDTokenClaims struct {
|
||||
oidc.TokenClaims
|
||||
|
||||
// Sub provides some information about the Spacelift run that generated this
|
||||
// token.
|
||||
// space:<space_id>:(stack|module):<stack_id|module_id>:run_type:<run_type>:scope:<read|write>
|
||||
Sub string `json:"sub"`
|
||||
// SpaceID is the ID of the space in which the run that owns the token was
|
||||
// executed.
|
||||
SpaceID string `json:"spaceId"`
|
||||
// CallerType is the type of the caller, ie. the entity that owns the run -
|
||||
// either stack or module.
|
||||
CallerType string `json:"callerType"`
|
||||
// CallerID is the ID of the caller, ie. the stack or module that generated
|
||||
// the run.
|
||||
CallerID string `json:"callerId"`
|
||||
// RunType is the type of the run.
|
||||
// (PROPOSED, TRACKED, TASK, TESTING or DESTROY)
|
||||
RunType string `json:"runType"`
|
||||
// RunID is the ID of the run that owns the token.
|
||||
RunID string `json:"runId"`
|
||||
// Scope is the scope of the token - either read or write.
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
func (c *IDTokenClaims) GetSubject() string {
|
||||
return c.Sub
|
||||
}
|
||||
|
||||
// JoinAttrs returns the protobuf representation of the attested identity.
|
||||
// This is used for auditing and for evaluation of WorkloadIdentity rules and
|
||||
// templating.
|
||||
func (c *IDTokenClaims) JoinAttrs() *workloadidentityv1pb.JoinAttrsSpacelift {
|
||||
return &workloadidentityv1pb.JoinAttrsSpacelift{
|
||||
Sub: c.Sub,
|
||||
SpaceId: c.SpaceID,
|
||||
CallerType: c.CallerType,
|
||||
CallerId: c.CallerID,
|
||||
RunType: c.RunType,
|
||||
RunId: c.RunID,
|
||||
Scope: c.Scope,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckIDTokenParams are parameters used to validate Spacelift OIDC tokens.
|
||||
type CheckIDTokenParams struct {
|
||||
ProvisionToken provision.Token
|
||||
IDToken []byte
|
||||
Validator Validator
|
||||
}
|
||||
|
||||
func (p *CheckIDTokenParams) validate() error {
|
||||
switch {
|
||||
case p.ProvisionToken == nil:
|
||||
return trace.BadParameter("ProvisionToken is required")
|
||||
case len(p.IDToken) == 0:
|
||||
return trace.BadParameter("IDToken is required")
|
||||
case p.Validator == nil:
|
||||
return trace.BadParameter("Validator is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckIDToken validates a Spacelift OIDC token, verifying both the validity of
|
||||
// the OIDC token itself, as well as ensuring claims match any configured allow
|
||||
// rules in the provided provision token.
|
||||
func CheckIDToken(
|
||||
ctx context.Context,
|
||||
params *CheckIDTokenParams,
|
||||
) (*IDTokenClaims, error) {
|
||||
if err := params.validate(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
token, ok := params.ProvisionToken.(*types.ProvisionTokenV2)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("spacelift join method only supports ProvisionTokenV2, '%T' was provided", params.ProvisionToken)
|
||||
}
|
||||
|
||||
if modules.GetModules().BuildType() != modules.BuildEnterprise {
|
||||
return nil, trace.Wrap(services.ErrRequiresEnterprise, "spacelift joining")
|
||||
}
|
||||
|
||||
claims, err := params.Validator.Validate(
|
||||
ctx, token.Spec.Spacelift.Hostname, string(params.IDToken),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "Spacelift run trying to join cluster",
|
||||
"claims", claims,
|
||||
"token", token.GetName(),
|
||||
)
|
||||
|
||||
return claims, trace.Wrap(checkSpaceliftAllowRules(token, claims))
|
||||
}
|
||||
|
||||
func checkSpaceliftAllowRules(token *types.ProvisionTokenV2, claims *IDTokenClaims) error {
|
||||
globCheck := func(want string, got string) (bool, error) {
|
||||
if token.Spec.Spacelift.EnableGlobMatching {
|
||||
return joinutils.GlobMatchAllowEmptyPattern(want, got)
|
||||
}
|
||||
if want == "" {
|
||||
return true, nil
|
||||
}
|
||||
return want == got, nil
|
||||
}
|
||||
|
||||
// If a single rule passes, accept the IDToken
|
||||
for i, rule := range token.Spec.Spacelift.Allow {
|
||||
// Please consider keeping these field validators in the same order they
|
||||
// are defined within the ProvisionTokenSpecV2Spacelift proto spec.
|
||||
spaceIDMatch, err := globCheck(rule.SpaceID, claims.SpaceID)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "evaluating rule (%d) space_id glob match", i)
|
||||
}
|
||||
if !spaceIDMatch {
|
||||
continue
|
||||
}
|
||||
callerIDMatch, err := globCheck(rule.CallerID, claims.CallerID)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "evaluating rule (%d) caller_id glob match", i)
|
||||
}
|
||||
if !callerIDMatch {
|
||||
continue
|
||||
}
|
||||
if rule.CallerType != "" && claims.CallerType != rule.CallerType {
|
||||
continue
|
||||
}
|
||||
if rule.Scope != "" && claims.Scope != rule.Scope {
|
||||
continue
|
||||
}
|
||||
|
||||
// All provided rules met.
|
||||
return nil
|
||||
}
|
||||
|
||||
return trace.AccessDenied("id token claims did not match any allow rules")
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package spacelift
|
||||
|
||||
import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
||||
)
|
||||
|
||||
// IDTokenClaims
|
||||
// See the following for the structure:
|
||||
// https://docs.spacelift.io/integrations/cloud-providers/oidc/#standard-claims
|
||||
type IDTokenClaims struct {
|
||||
oidc.TokenClaims
|
||||
|
||||
// Sub provides some information about the Spacelift run that generated this
|
||||
// token.
|
||||
// space:<space_id>:(stack|module):<stack_id|module_id>:run_type:<run_type>:scope:<read|write>
|
||||
Sub string `json:"sub"`
|
||||
// SpaceID is the ID of the space in which the run that owns the token was
|
||||
// executed.
|
||||
SpaceID string `json:"spaceId"`
|
||||
// CallerType is the type of the caller, ie. the entity that owns the run -
|
||||
// either stack or module.
|
||||
CallerType string `json:"callerType"`
|
||||
// CallerID is the ID of the caller, ie. the stack or module that generated
|
||||
// the run.
|
||||
CallerID string `json:"callerId"`
|
||||
// RunType is the type of the run.
|
||||
// (PROPOSED, TRACKED, TASK, TESTING or DESTROY)
|
||||
RunType string `json:"runType"`
|
||||
// RunID is the ID of the run that owns the token.
|
||||
RunID string `json:"runId"`
|
||||
// Scope is the scope of the token - either read or write.
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
func (c *IDTokenClaims) GetSubject() string {
|
||||
return c.Sub
|
||||
}
|
||||
|
||||
// JoinAttrs returns the protobuf representation of the attested identity.
|
||||
// This is used for auditing and for evaluation of WorkloadIdentity rules and
|
||||
// templating.
|
||||
func (c *IDTokenClaims) JoinAttrs() *workloadidentityv1pb.JoinAttrsSpacelift {
|
||||
return &workloadidentityv1pb.JoinAttrsSpacelift{
|
||||
Sub: c.Sub,
|
||||
SpaceId: c.SpaceID,
|
||||
CallerType: c.CallerType,
|
||||
CallerId: c.CallerID,
|
||||
RunType: c.RunType,
|
||||
RunId: c.RunID,
|
||||
Scope: c.Scope,
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ func Generate(ctx context.Context, joinMethod types.JoinMethod) (string, error)
|
||||
types.JoinMethodAzure,
|
||||
types.JoinMethodGCP,
|
||||
types.JoinMethodTPM,
|
||||
types.JoinMethodSpacelift,
|
||||
types.JoinMethodTerraformCloud,
|
||||
types.JoinMethodOracle,
|
||||
types.JoinMethodEnv0:
|
||||
|
||||
Reference in New Issue
Block a user