Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,11 @@ message ProvisionTokenSpecV2GitLab {
// `gitlab.com` - but can be set to the domain of your self-hosted GitLab
// e.g `gitlab.example.com`.
string Domain = 2 [(gogoproto.jsontag) = "domain,omitempty"];
// StaticJWKS disables fetching of the GitLab signing keys via the JWKS/OIDC
// endpoints, and allows them to be directly specified. This allows joining
// from GitLab CI instances that are not reachable by the Teleport Auth
// Service.
string StaticJWKS = 3 [(gogoproto.jsontag) = "static_jwks,omitempty"];
}

// ProvisionTokenSpecV2CircleCI contains the CircleCI-specific part of the
Expand Down
3,745 changes: 1,897 additions & 1,848 deletions api/types/types.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|---|---|---|
|allow|[][object](#specgitlaballow-items)|Allow is a list of TokenRules, nodes using this token must match one allow rule to use this token.|
|domain|string|Domain is the domain of your GitLab instance. This will default to `gitlab.com` - but can be set to the domain of your self-hosted GitLab e.g `gitlab.example.com`.|
|static_jwks|string|StaticJWKS disables fetching of the GitLab signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitLab CI instances that are not reachable by the Teleport Auth Service.|

### spec.gitlab.allow items

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ Optional:

- `allow` (Attributes List) Allow is a list of TokenRules, nodes using this token must match one allow rule to use this token. (see [below for nested schema](#nested-schema-for-specgitlaballow))
- `domain` (String) Domain is the domain of your GitLab instance. This will default to `gitlab.com` - but can be set to the domain of your self-hosted GitLab e.g `gitlab.example.com`.
- `static_jwks` (String) StaticJWKS disables fetching of the GitLab signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitLab CI instances that are not reachable by the Teleport Auth Service.

### Nested Schema for `spec.gitlab.allow`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ Optional:

- `allow` (Attributes List) Allow is a list of TokenRules, nodes using this token must match one allow rule to use this token. (see [below for nested schema](#nested-schema-for-specgitlaballow))
- `domain` (String) Domain is the domain of your GitLab instance. This will default to `gitlab.com` - but can be set to the domain of your self-hosted GitLab e.g `gitlab.example.com`.
- `static_jwks` (String) StaticJWKS disables fetching of the GitLab signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitLab CI instances that are not reachable by the Teleport Auth Service.

### Nested Schema for `spec.gitlab.allow`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ spec:
will default to `gitlab.com` - but can be set to the domain
of your self-hosted GitLab e.g `gitlab.example.com`.
type: string
static_jwks:
description: StaticJWKS disables fetching of the GitLab signing
keys via the JWKS/OIDC endpoints, and allows them to be directly
specified. This allows joining from GitLab CI instances that
are not reachable by the Teleport Auth Service.
type: string
type: object
join_method:
description: 'JoinMethod is the joining method required in order to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ spec:
will default to `gitlab.com` - but can be set to the domain
of your self-hosted GitLab e.g `gitlab.example.com`.
type: string
static_jwks:
description: StaticJWKS disables fetching of the GitLab signing
keys via the JWKS/OIDC endpoints, and allows them to be directly
specified. This allows joining from GitLab CI instances that
are not reachable by the Teleport Auth Service.
type: string
type: object
join_method:
description: 'JoinMethod is the joining method required in order to
Expand Down
44 changes: 44 additions & 0 deletions integrations/terraform/tfschema/token/types_terraform.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions lib/auth/join_gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type gitlabIDTokenValidator interface {
Validate(
ctx context.Context, domain string, token string,
) (*gitlab.IDTokenClaims, error)
ValidateTokenWithJWKS(
ctx context.Context, jwks []byte, token string,
) (*gitlab.IDTokenClaims, error)
}

func (a *Server) checkGitLabJoinRequest(ctx context.Context, req *types.RegisterUsingTokenRequest) (*gitlab.IDTokenClaims, error) {
Expand All @@ -49,11 +52,21 @@ func (a *Server) checkGitLabJoinRequest(ctx context.Context, req *types.Register
return nil, trace.BadParameter("gitlab join method only supports ProvisionTokenV2, '%T' was provided", pt)
}

claims, err := a.gitlabIDTokenValidator.Validate(
ctx, token.Spec.GitLab.Domain, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err)
var claims *gitlab.IDTokenClaims
if token.Spec.GitLab.StaticJWKS != "" {
claims, err = a.gitlabIDTokenValidator.ValidateTokenWithJWKS(
ctx, []byte(token.Spec.GitLab.StaticJWKS), req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err, "validating with static jwks")
}
} else {
claims, err = a.gitlabIDTokenValidator.Validate(
ctx, token.Spec.GitLab.Domain, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err, "validating with oidc")
}
}

a.logger.InfoContext(ctx, "GitLab CI run trying to join cluster",
Expand Down
53 changes: 53 additions & 0 deletions lib/auth/join_gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
type mockGitLabTokenValidator struct {
tokens map[string]gitlab.IDTokenClaims
lastCalledDomain string
lastCalledJWKS []byte
}

func (m *mockGitLabTokenValidator) Validate(
Expand All @@ -49,6 +50,19 @@ func (m *mockGitLabTokenValidator) Validate(
return &claims, nil
}

func (m *mockGitLabTokenValidator) ValidateTokenWithJWKS(
_ context.Context, jwks []byte, token string,
) (*gitlab.IDTokenClaims, error) {
m.lastCalledJWKS = jwks

claims, ok := m.tokens[token]
if !ok {
return nil, errMockInvalidToken
}

return &claims, nil
}

func TestAuth_RegisterUsingToken_GitLab(t *testing.T) {
validIDToken := "test.fake.jwt"
idTokenValidator := &mockGitLabTokenValidator{
Expand Down Expand Up @@ -490,6 +504,36 @@ func TestAuth_RegisterUsingToken_GitLab(t *testing.T) {
request: newRequest(validIDToken),
assertError: allowRulesNotMatched,
},
{
name: "success with JWKS",
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodGitLab,
Roles: []types.SystemRole{types.RoleNode},
GitLab: &types.ProvisionTokenSpecV2GitLab{
Allow: []*types.ProvisionTokenSpecV2GitLab_Rule{
allowRule(nil),
},
StaticJWKS: "xyzzy",
},
},
request: newRequest(validIDToken),
assertError: require.NoError,
},
{
name: "failure with JWKS",
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodGitLab,
Roles: []types.SystemRole{types.RoleNode},
GitLab: &types.ProvisionTokenSpecV2GitLab{
Allow: []*types.ProvisionTokenSpecV2GitLab_Rule{
allowRule(nil),
},
StaticJWKS: "xyzzy",
},
},
request: newRequest("invalidjwt"),
assertError: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -510,6 +554,15 @@ func TestAuth_RegisterUsingToken_GitLab(t *testing.T) {
idTokenValidator.lastCalledDomain,
)
}
if tt.tokenSpec.GitLab.StaticJWKS != "" {
require.Equal(
t,
[]byte(tt.tokenSpec.GitLab.StaticJWKS),
idTokenValidator.lastCalledJWKS,
)
} else {
require.Nil(t, idTokenValidator.lastCalledJWKS)
}
})
}
}
Expand Down
49 changes: 49 additions & 0 deletions lib/gitlab/token_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ package gitlab

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/coreos/go-oidc"
"github.com/go-jose/go-jose/v3"
josejwt "github.com/go-jose/go-jose/v3/jwt"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

Expand Down Expand Up @@ -118,3 +121,49 @@ func (id *IDTokenValidator) Validate(
}
return &claims, nil
}

// ValidateTokenWithJWKS validates a token using the provided JWKS data.
// Used in cases where GitLab is not reachable from the Teleport cluster.
func (id *IDTokenValidator) ValidateTokenWithJWKS(
ctx context.Context,
jwksData []byte,
token string,
) (*IDTokenClaims, error) {
parsed, err := josejwt.ParseSigned(token)
if err != nil {
return nil, trace.Wrap(err, "parsing jwt")
}

jwks := jose.JSONWebKeySet{}
if err := json.Unmarshal(jwksData, &jwks); err != nil {
return nil, trace.Wrap(err, "parsing provided jwks")
}

stdClaims := josejwt.Claims{}
if err := parsed.Claims(jwks, &stdClaims); err != nil {
return nil, trace.Wrap(err, "validating jwt signature")
}

clusterNameResource, err := id.ClusterNameGetter.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err, "getting cluster name")
}

leeway := time.Second * 10
err = stdClaims.ValidateWithLeeway(josejwt.Expected{
Audience: []string{
clusterNameResource.GetClusterName(),
},
Time: id.Clock.Now(),
}, leeway)
if err != nil {
return nil, trace.Wrap(err, "validating standard claims")
}

claims := IDTokenClaims{}
if err := parsed.Claims(jwks, &claims); err != nil {
return nil, trace.Wrap(err, "validating custom claims")
}

return &claims, nil
}
Loading
Loading