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
4 changes: 4 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,8 @@ const (
EnvVarTerraformJoinMethod = "TF_TELEPORT_JOIN_METHOD"
// EnvVarTerraformJoinToken is the environment variable configuring the Terraform provider native MachineID join token.
EnvVarTerraformJoinToken = "TF_TELEPORT_JOIN_TOKEN"
// EnvVarTerraformCloudJoinAudienceTag is the environment variable configuring the Terraform provider's native Machine ID
// joining. The audience tag specifies the optional suffix for the TF_WORKLOAD_IDENTITY_AUDIENCE variable when
// specifically using the `terraform` join method.
EnvVarTerraformCloudJoinAudienceTag = "TF_TELEPORT_JOIN_AUDIENCE_TAG"
)
58 changes: 58 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,8 @@ message ProvisionTokenSpecV2 {
ProvisionTokenSpecV2Spacelift Spacelift = 14 [(gogoproto.jsontag) = "spacelift,omitempty"];
// TPM allows the configuration of options specific to the "tpm" join method.
ProvisionTokenSpecV2TPM TPM = 15 [(gogoproto.jsontag) = "tpm,omitempty"];
// TerraformCloud allows the configuration of options specific to the "terraform_cloud" join method.
ProvisionTokenSpecV2TerraformCloud TerraformCloud = 16 [(gogoproto.jsontag) = "terraform_cloud,omitempty"];
}

// ProvisionTokenSpecV2TPM contains the TPM-specific part of the
Expand Down Expand Up @@ -1579,6 +1581,62 @@ message ProvisionTokenSpecV2GCP {
repeated Rule Allow = 1 [(gogoproto.jsontag) = "allow,omitempty"];
}

// ProvisionTokenSpecV2Terraform contains Terraform-specific parts of the
// ProvisionTokenSpecV2.
message ProvisionTokenSpecV2TerraformCloud {
// Rule is a set of properties the Terraform-issued token might have to be
// allowed to use this ProvisionToken.
message Rule {
// OrganizationID is the ID of the HCP Terraform organization. At least
// one organization value is required, either ID or name.
string OrganizationID = 1 [(gogoproto.jsontag) = "organization_id,omitempty"];

// OrganizationName is the human-readable name of the HCP Terraform
// organization. At least one organization value is required, either ID or
// name.
string OrganizationName = 2 [(gogoproto.jsontag) = "organization_name,omitempty"];

// ProjectID is the ID of the HCP Terraform project. At least one project or
// workspace value is required, either ID or name.
string ProjectID = 3 [(gogoproto.jsontag) = "project_id,omitempty"];

// ProjectName is the human-readable name for the HCP Terraform project. At
// least one project or workspace value is required, either ID or name.
string ProjectName = 4 [(gogoproto.jsontag) = "project_name,omitempty"];

// WorkspaceID is the ID of the HCP Terraform workspace. At least one
// project or workspace value is required, either ID or name.
string WorkspaceID = 5 [(gogoproto.jsontag) = "workspace_id,omitempty"];

// WorkspaceName is the human-readable name of the HCP Terraform workspace.
// At least one project or workspace value is required, either ID or name.
string WorkspaceName = 6 [(gogoproto.jsontag) = "workspace_name,omitempty"];

// RunPhase is the phase of the run the token was issued for, e.g. `plan` or
// `apply`
string RunPhase = 7 [(gogoproto.jsontag) = "run_phase,omitempty"];
}

// Allow is a list of Rules, nodes using this token must match one
// allow rule to use this token.
repeated Rule Allow = 1 [(gogoproto.jsontag) = "allow,omitempty"];

// 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.
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.
message StaticTokensV2 {
option (gogoproto.goproto_stringer) = false;
Expand Down
45 changes: 45 additions & 0 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const (
// JoinMethodTPM indicates that the node will join with the TPM join method.
// The core implementation of this join method can be found in lib/tpm.
JoinMethodTPM JoinMethod = "tpm"
// JoinMethodTerraformCloud indicates that the node will join using the Terraform
// join method. See lib/terraformcloud for more.
JoinMethodTerraformCloud JoinMethod = "terraform_cloud"
)

var JoinMethods = []JoinMethod{
Expand All @@ -85,6 +88,7 @@ var JoinMethods = []JoinMethod{
JoinMethodSpacelift,
JoinMethodToken,
JoinMethodTPM,
JoinMethodTerraformCloud,
}

func ValidateJoinMethod(method JoinMethod) error {
Expand Down Expand Up @@ -348,6 +352,17 @@ func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
if err := providerCfg.validate(); err != nil {
return trace.Wrap(err, "spec.tpm: failed validation")
}
case JoinMethodTerraformCloud:
providerCfg := p.Spec.TerraformCloud
if providerCfg == nil {
return trace.BadParameter(
"spec.terraform_cloud: must be configured for the join method %q",
JoinMethodTerraformCloud,
)
}
if err := providerCfg.checkAndSetDefaults(); err != nil {
return trace.Wrap(err, "spec.terraform_cloud: failed validation")
}
default:
return trace.BadParameter("unknown join method %q", p.Spec.JoinMethod)
}
Expand Down Expand Up @@ -817,3 +832,33 @@ func (a *ProvisionTokenSpecV2TPM) validate() error {
}
return nil
}

func (a *ProvisionTokenSpecV2TerraformCloud) checkAndSetDefaults() error {
if len(a.Allow) == 0 {
return trace.BadParameter("the %q join method requires at least one token allow rule", JoinMethodTerraformCloud)
}

// Note: an empty audience will fall back to the cluster name.

for i, allowRule := range a.Allow {
orgSet := allowRule.OrganizationID != "" || allowRule.OrganizationName != ""
projectSet := allowRule.ProjectID != "" || allowRule.ProjectName != ""
workspaceSet := allowRule.WorkspaceID != "" || allowRule.WorkspaceName != ""

if !orgSet {
return trace.BadParameter(
"allow[%d]: one of ['organization_id', 'organization_name'] must be set",
i,
)
}

if !projectSet && !workspaceSet {
return trace.BadParameter(
"allow[%d]: at least one of ['project_id', 'project_name', 'workspace_id', 'workspace_name'] must be set",
i,
)
}
}

return nil
}
150 changes: 150 additions & 0 deletions api/types/provisioning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,156 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
},
wantErr: true,
},
{
desc: "terraform",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationName: "foo",
OrganizationID: "foo-id",
ProjectName: "bar",
ProjectID: "bar-id",
WorkspaceName: "baz",
WorkspaceID: "baz-id",
RunPhase: "apply",
},
},
},
},
},
expected: &ProvisionTokenV2{
Kind: "token",
Version: "v2",
Metadata: Metadata{
Name: "test",
Namespace: "default",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationName: "foo",
OrganizationID: "foo-id",
ProjectName: "bar",
ProjectID: "bar-id",
WorkspaceName: "baz",
WorkspaceID: "baz-id",
RunPhase: "apply",
},
},
},
},
},
},
{
desc: "terraform missing organization (id)",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
WorkspaceName: "foo",
},
},
},
},
},
wantErr: true,
},
{
desc: "terraform missing specific resource",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationName: "foo",
},
},
},
},
},
wantErr: true,
},
{
desc: "terraform only names",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationName: "foo",
ProjectName: "bar",
WorkspaceName: "baz",
},
},
},
},
},
wantErr: false,
},
{
desc: "terraform only ids",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{
{
OrganizationID: "foo",
ProjectID: "bar",
WorkspaceID: "baz",
},
},
},
},
},
wantErr: false,
},
{
desc: "terraform missing rules",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: JoinMethodTerraformCloud,
TerraformCloud: &ProvisionTokenSpecV2TerraformCloud{
Allow: []*ProvisionTokenSpecV2TerraformCloud_Rule{},
},
},
},
wantErr: true,
},
}

for _, tc := range testcases {
Expand Down
Loading