Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16] Support for joining Actions in un-reachable GitHub Enterprise Servers via Static JWKS (#48973) #49052

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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
Loading