Skip to content

Commit

Permalink
Terraform Cloud joining: Support Terraform Enterprise issuers (#46051)
Browse files Browse the repository at this point in the history
This adds support for hostname/issuer overrides, needed to support
on-prem Terraform Enterprise installs. When the new `hostname` field
is unset, behavior is changed, but when set, the JWT is validated
against it instead of `app.terraform.io`.

Additionally, this renames `join_terraform.go` to
`join_terraformcloud.go`, since that was missed during the rename
in #45574.
  • Loading branch information
timothyb89 committed Sep 13, 2024
1 parent 70f143a commit 1003159
Show file tree
Hide file tree
Showing 12 changed files with 1,906 additions and 1,916 deletions.
7 changes: 7 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,13 @@ message ProvisionTokenSpecV2TerraformCloud {
// Terraform Cloud, this value should be `foo`. If the variable is set to
// match the cluster name, it does not need to be set here.
string Audience = 2 [(gogoproto.jsontag) = "audience,omitempty"];

// Hostname is the hostname of the Terraform Enterprise instance expected to
// issue JWTs allowed by this token. This may be unset for regular Terraform
// Cloud use, in which case it will be assumed to be `app.terraform.io`.
// Otherwise, it must both match the `iss` (issuer) field included in JWTs,
// and provide standard JWKS endpoints.
string Hostname = 3 [(gogoproto.jsontag) = "hostname,omitempty"];
}

// StaticTokensV2 implements the StaticTokens interface.
Expand Down
3,469 changes: 1,759 additions & 1,710 deletions api/types/types.pb.go

Large diffs are not rendered by default.

This file was deleted.

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

- `allow` (Attributes List) Allow is a list of Rules, nodes using this token must match one allow rule to use this token. (see [below for nested schema](#nested-schema-for-specterraform_cloudallow))
- `audience` (String) Audience is the JWT audience as configured in the TFC_WORKLOAD_IDENTITY_AUDIENCE(_$TAG) variable in Terraform Cloud. If unset, defaults to the Teleport cluster name. For example, if `TFC_WORKLOAD_IDENTITY_AUDIENCE_TELEPORT=foo` is set in Terraform Cloud, this value should be `foo`. If the variable is set to match the cluster name, it does not need to be set here.
- `hostname` (String) Hostname is the hostname of the Terraform Enterprise instance expected to issue JWTs allowed by this token. This may be unset for regular Terraform Cloud use, in which case it will be assumed to be `app.terraform.io`. Otherwise, it must both match the `iss` (issuer) field included in JWTs, and provide standard JWKS endpoints.

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

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

- `allow` (Attributes List) Allow is a list of Rules, nodes using this token must match one allow rule to use this token. (see [below for nested schema](#nested-schema-for-specterraform_cloudallow))
- `audience` (String) Audience is the JWT audience as configured in the TFC_WORKLOAD_IDENTITY_AUDIENCE(_$TAG) variable in Terraform Cloud. If unset, defaults to the Teleport cluster name. For example, if `TFC_WORKLOAD_IDENTITY_AUDIENCE_TELEPORT=foo` is set in Terraform Cloud, this value should be `foo`. If the variable is set to match the cluster name, it does not need to be set here.
- `hostname` (String) Hostname is the hostname of the Terraform Enterprise instance expected to issue JWTs allowed by this token. This may be unset for regular Terraform Cloud use, in which case it will be assumed to be `app.terraform.io`. Otherwise, it must both match the `iss` (issuer) field included in JWTs, and provide standard JWKS endpoints.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ spec:
is set to match the cluster name, it does not need to be set
here.
type: string
hostname:
description: Hostname is the hostname of the Terraform Enterprise
instance expected to issue JWTs allowed by this token. This
may be unset for regular Terraform Cloud use, in which case
it will be assumed to be `app.terraform.io`. Otherwise, it must
both match the `iss` (issuer) field included in JWTs, and provide
standard JWKS endpoints.
type: string
type: object
tpm:
description: TPM allows the configuration of options specific to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ spec:
is set to match the cluster name, it does not need to be set
here.
type: string
hostname:
description: Hostname is the hostname of the Terraform Enterprise
instance expected to issue JWTs allowed by this token. This
may be unset for regular Terraform Cloud use, in which case
it will be assumed to be `app.terraform.io`. Otherwise, it must
both match the `iss` (issuer) field included in JWTs, and provide
standard JWKS endpoints.
type: string
type: object
tpm:
description: TPM allows the configuration of options specific to the
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.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (

type terraformCloudIDTokenValidator interface {
Validate(
ctx context.Context, audience string, token string,
ctx context.Context, audience, hostname, token string,
) (*terraformcloud.IDTokenClaims, error)
}

Expand Down Expand Up @@ -68,7 +68,7 @@ func (a *Server) checkTerraformCloudJoinRequest(ctx context.Context, req *types.
}

claims, err := a.terraformIDTokenValidator.Validate(
ctx, aud, req.IDToken,
ctx, aud, token.Spec.TerraformCloud.Hostname, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ type mockTerraformTokenValidator struct {
}

func (m *mockTerraformTokenValidator) Validate(
_ context.Context, audience string, token string,
_ context.Context, audience, hostname, token string,
) (*terraformcloud.IDTokenClaims, error) {
if audience != "test.localhost" {
return nil, fmt.Errorf("bad audience: %s", audience)
}

// Hostname override always fails, but that's okay: we're just making sure
// the value gets plumbed through to here.
if hostname != "" {
return nil, fmt.Errorf("bad issuer: %s", hostname)
}

claims, ok := m.tokens[token]
if !ok {
return nil, errMockInvalidToken
Expand Down Expand Up @@ -363,6 +369,27 @@ func TestAuth_RegisterUsingToken_Terraform(t *testing.T) {
require.ErrorContains(t, err, "bad audience")
},
},
{
name: "overridden hostname is honored",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodTerraformCloud,
Roles: []types.SystemRole{types.RoleNode},
TerraformCloud: &types.ProvisionTokenSpecV2TerraformCloud{
Allow: []*types.ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationID: "example-organization-id",
WorkspaceID: "example-workspace-id",
},
},
Hostname: "example.com",
},
},
request: newRequest(validIDToken),
assertError: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "bad issuer: example.com")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Loading

0 comments on commit 1003159

Please sign in to comment.