Skip to content

Commit

Permalink
[v16] Support for joining Actions in un-reachable GitHub Enterprise S…
Browse files Browse the repository at this point in the history
…ervers via Static JWKS (#48973) (#49052)

* Support for joining Actions in un-reachable GitHub Enterprise Servers via Static JWKS (#48973)

* Add JWKS based validator for github tokens

* Extend proto

* Update auth srvr github join impl to support static jwks

* Ignore govet

* initialize with jwks validator

* Update docs

* Update terraform shizz

* Regenerate various things

* Fix key generation
  • Loading branch information
strideynet authored Nov 18, 2024
1 parent ab97907 commit fe210a9
Show file tree
Hide file tree
Showing 13 changed files with 2,123 additions and 1,751 deletions.
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 @@ -1407,6 +1407,11 @@ message ProvisionTokenSpecV2GitHub {
// See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise
// for more information about customized issuer values.
string EnterpriseSlug = 3 [(gogoproto.jsontag) = "enterprise_slug,omitempty"];
// StaticJWKS disables fetching of the GHES signing keys via the JWKS/OIDC
// endpoints, and allows them to be directly specified. This allows joining
// from GitHub Actions in GHES instances that are not reachable by the
// Teleport Auth Service.
string StaticJWKS = 4 [(gogoproto.jsontag) = "static_jwks,omitempty"];
}

// ProvisionTokenSpecV2GitLab contains the GitLab-specific part of the
Expand Down
3,541 changes: 1,795 additions & 1,746 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 @@ -100,6 +100,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|allow|[][object](#specgithuballow-items)|Allow is a list of TokenRules, nodes using this token must match one allow rule to use this token.|
|enterprise_server_host|string|EnterpriseServerHost allows joining from runners associated with a GitHub Enterprise Server instance. When unconfigured, tokens will be validated against github.com, but when configured to the host of a GHES instance, then the tokens will be validated against host. This value should be the hostname of the GHES instance, and should not include the scheme or a path. The instance must be accessible over HTTPS at this hostname and the certificate must be trusted by the Auth Service.|
|enterprise_slug|string|EnterpriseSlug allows the slug of a GitHub Enterprise organisation to be included in the expected issuer of the OIDC tokens. This is for compatibility with the `include_enterprise_slug` option in GHE. This field should be set to the slug of your enterprise if this is enabled. If this is not enabled, then this field must be left empty. This field cannot be specified if `enterprise_server_host` is specified. See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise for more information about customized issuer values.|
|static_jwks|string|StaticJWKS disables fetching of the GHES signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitHub Actions in GHES instances that are not reachable by the Teleport Auth Service.|

### spec.github.allow items

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,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-specgithuballow))
- `enterprise_server_host` (String) EnterpriseServerHost allows joining from runners associated with a GitHub Enterprise Server instance. When unconfigured, tokens will be validated against github.com, but when configured to the host of a GHES instance, then the tokens will be validated against host. This value should be the hostname of the GHES instance, and should not include the scheme or a path. The instance must be accessible over HTTPS at this hostname and the certificate must be trusted by the Auth Service.
- `enterprise_slug` (String) EnterpriseSlug allows the slug of a GitHub Enterprise organisation to be included in the expected issuer of the OIDC tokens. This is for compatibility with the `include_enterprise_slug` option in GHE. This field should be set to the slug of your enterprise if this is enabled. If this is not enabled, then this field must be left empty. This field cannot be specified if `enterprise_server_host` is specified. See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise for more information about customized issuer values.
- `static_jwks` (String) StaticJWKS disables fetching of the GHES signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitHub Actions in GHES instances that are not reachable by the Teleport Auth Service.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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-specgithuballow))
- `enterprise_server_host` (String) EnterpriseServerHost allows joining from runners associated with a GitHub Enterprise Server instance. When unconfigured, tokens will be validated against github.com, but when configured to the host of a GHES instance, then the tokens will be validated against host. This value should be the hostname of the GHES instance, and should not include the scheme or a path. The instance must be accessible over HTTPS at this hostname and the certificate must be trusted by the Auth Service.
- `enterprise_slug` (String) EnterpriseSlug allows the slug of a GitHub Enterprise organisation to be included in the expected issuer of the OIDC tokens. This is for compatibility with the `include_enterprise_slug` option in GHE. This field should be set to the slug of your enterprise if this is enabled. If this is not enabled, then this field must be left empty. This field cannot be specified if `enterprise_server_host` is specified. See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise for more information about customized issuer values.
- `static_jwks` (String) StaticJWKS disables fetching of the GHES signing keys via the JWKS/OIDC endpoints, and allows them to be directly specified. This allows joining from GitHub Actions in GHES instances that are not reachable by the Teleport Auth Service.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ spec:
if `enterprise_server_host` is specified. See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise
for more information about customized issuer values.
type: string
static_jwks:
description: StaticJWKS disables fetching of the GHES signing
keys via the JWKS/OIDC endpoints, and allows them to be directly
specified. This allows joining from GitHub Actions in GHES instances
that are not reachable by the Teleport Auth Service.
type: string
type: object
gitlab:
description: GitLab allows the configuration of options specific to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ spec:
if `enterprise_server_host` is specified. See https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise
for more information about customized issuer values.
type: string
static_jwks:
description: StaticJWKS disables fetching of the GHES signing
keys via the JWKS/OIDC endpoints, and allows them to be directly
specified. This allows joining from GitHub Actions in GHES instances
that are not reachable by the Teleport Auth Service.
type: string
type: object
gitlab:
description: GitLab allows the configuration of options specific 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.

7 changes: 7 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,9 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
},
)
}
if as.ghaIDTokenJWKSValidator == nil {
as.ghaIDTokenJWKSValidator = githubactions.ValidateTokenWithJWKS
}
if as.spaceliftIDTokenValidator == nil {
as.spaceliftIDTokenValidator = spacelift.NewIDTokenValidator(
spacelift.IDTokenValidatorConfig{
Expand Down Expand Up @@ -952,6 +955,10 @@ type Server struct {
// ghaIDTokenValidator allows ID tokens from GitHub Actions to be validated
// by the auth server. It can be overridden for the purpose of tests.
ghaIDTokenValidator ghaIDTokenValidator
// ghaIDTokenJWKSValidator allows ID tokens from GitHub Actions to be
// validated by the auth server using a known JWKS. It can be overridden for
//the purpose of tests.
ghaIDTokenJWKSValidator ghaIDTokenJWKSValidator

// spaceliftIDTokenValidator allows ID tokens from Spacelift to be validated
// by the auth server. It can be overridden for the purpose of tests.
Expand Down
27 changes: 22 additions & 5 deletions lib/auth/join_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package auth
import (
"context"
"fmt"
"time"

"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
Expand All @@ -36,6 +37,10 @@ type ghaIDTokenValidator interface {
) (*githubactions.IDTokenClaims, error)
}

type ghaIDTokenJWKSValidator func(
now time.Time, jwksData []byte, token string,
) (*githubactions.IDTokenClaims, error)

func (a *Server) checkGitHubJoinRequest(ctx context.Context, req *types.RegisterUsingTokenRequest) (*githubactions.IDTokenClaims, error) {
if req.IDToken == "" {
return nil, trace.BadParameter("IDToken not provided for Github join request")
Expand Down Expand Up @@ -63,11 +68,23 @@ func (a *Server) checkGitHubJoinRequest(ctx context.Context, req *types.Register
}
}

claims, err := a.ghaIDTokenValidator.Validate(
ctx, enterpriseOverride, enterpriseSlug, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err)
var claims *githubactions.IDTokenClaims
if token.Spec.GitHub.StaticJWKS != "" {
claims, err = a.ghaIDTokenJWKSValidator(
a.clock.Now().UTC(),
[]byte(token.Spec.GitHub.StaticJWKS),
req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err, "validating with jwks")
}
} else {
claims, err = a.ghaIDTokenValidator.Validate(
ctx, enterpriseOverride, enterpriseSlug, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err, "validating with oidc")
}
}

log.WithFields(logrus.Fields{
Expand Down
49 changes: 49 additions & 0 deletions lib/auth/join_github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type mockIDTokenValidator struct {
tokens map[string]githubactions.IDTokenClaims
lastCalledGHESHost string
lastCalledEnterpriseSlug string
lastCalledJWKS string
}

var errMockInvalidToken = errors.New("invalid token")
Expand All @@ -57,6 +58,18 @@ func (m *mockIDTokenValidator) Validate(
func (m *mockIDTokenValidator) reset() {
m.lastCalledGHESHost = ""
m.lastCalledEnterpriseSlug = ""
m.lastCalledJWKS = ""
}

func (m *mockIDTokenValidator) ValidateJWKS(
_ time.Time, jwks []byte, token string,
) (*githubactions.IDTokenClaims, error) {
m.lastCalledJWKS = string(jwks)
claims, ok := m.tokens[token]
if !ok {
return nil, errMockInvalidToken
}
return &claims, nil
}

func TestAuth_RegisterUsingToken_GHA(t *testing.T) {
Expand All @@ -77,6 +90,7 @@ func TestAuth_RegisterUsingToken_GHA(t *testing.T) {
}
var withTokenValidator ServerOption = func(server *Server) error {
server.ghaIDTokenValidator = idTokenValidator
server.ghaIDTokenJWKSValidator = idTokenValidator.ValidateJWKS
return nil
}
ctx := context.Background()
Expand Down Expand Up @@ -141,6 +155,36 @@ func TestAuth_RegisterUsingToken_GHA(t *testing.T) {
request: newRequest(validIDToken),
assertError: require.NoError,
},
{
name: "success with jwks",
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodGitHub,
Roles: []types.SystemRole{types.RoleNode},
GitHub: &types.ProvisionTokenSpecV2GitHub{
Allow: []*types.ProvisionTokenSpecV2GitHub_Rule{
allowRule(nil),
},
StaticJWKS: "my-jwks",
},
},
request: newRequest(validIDToken),
assertError: require.NoError,
},
{
name: "failure with jwks",
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodGitHub,
Roles: []types.SystemRole{types.RoleNode},
GitHub: &types.ProvisionTokenSpecV2GitHub{
Allow: []*types.ProvisionTokenSpecV2GitHub_Rule{
allowRule(nil),
},
StaticJWKS: "my-jwks",
},
},
request: newRequest("invalid"),
assertError: require.Error,
},
{
name: "ghes override",
tokenSpec: types.ProvisionTokenSpecV2{
Expand Down Expand Up @@ -385,6 +429,11 @@ func TestAuth_RegisterUsingToken_GHA(t *testing.T) {
tt.tokenSpec.GitHub.EnterpriseSlug,
idTokenValidator.lastCalledEnterpriseSlug,
)
require.Equal(
t,
tt.tokenSpec.GitHub.StaticJWKS,
idTokenValidator.lastCalledJWKS,
)
})
}
}
45 changes: 45 additions & 0 deletions lib/githubactions/token_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ package githubactions

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 @@ -113,3 +116,45 @@ func (id *IDTokenValidator) Validate(
}
return &claims, nil
}

// ValidateTokenWithJWKS validates a GitHub Actions JWT using a configured
// JWKS rather than fetching from well-known. This supports cases where GHES
// is not accessible to the Teleport Auth Server.
func ValidateTokenWithJWKS(
now time.Time,
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")
}

leeway := time.Second * 10
err = stdClaims.ValidateWithLeeway(josejwt.Expected{
Audience: []string{
"teleport.cluster.local",
},
Time: 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

0 comments on commit fe210a9

Please sign in to comment.