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
2 changes: 2 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,8 @@ const (
EnvVarTerraformIdentityFile = "TF_TELEPORT_IDENTITY_FILE"
// EnvVarTerraformIdentityFileBase64 is the environment variable containing the base64-encoded identity file used by the Terraform provider.
EnvVarTerraformIdentityFileBase64 = "TF_TELEPORT_IDENTITY_FILE_BASE64"
// EnvVarTerraformInsecure is the environment variable used to control whether the Terraform provider will skip verifying the proxy server's TLS certificate.
EnvVarTerraformInsecure = "TF_TELEPORT_INSECURE"
// EnvVarTerraformRetryBaseDuration is the environment variable configuring the base duration between two Terraform provider retries.
EnvVarTerraformRetryBaseDuration = "TF_TELEPORT_RETRY_BASE_DURATION"
// EnvVarTerraformRetryCapDuration is the environment variable configuring the maximum duration between two Terraform provider retries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ This auth method has the following limitations:
- `identity_file` (String, Sensitive) Teleport identity file content. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE`.
- `identity_file_base64` (String, Sensitive) Teleport identity file content base64 encoded. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE_BASE64`.
- `identity_file_path` (String) Teleport identity file path. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE_PATH`.
- `insecure` (Boolean) Skip proxy certificate verification when joining the Teleport cluster. This is not recommended for production use. This can also be set with the environment variable `TF_TELEPORT_INSECURE`.
- `join_method` (String) Enables the native Terraform MachineID support. When set, Terraform uses MachineID to securely join the Teleport cluster and obtain credentials. See [the join method reference](../join-methods.mdx) for possible values. You must use [a delegated join method](../join-methods.mdx#secret-vs-delegated). This can also be set with the environment variable `TF_TELEPORT_JOIN_METHOD`.
- `join_token` (String) Name of the token used for the native MachineID joining. This value is not sensitive for [delegated join methods](../join-methods.mdx#secret-vs-delegated). This can also be set with the environment variable `TF_TELEPORT_JOIN_TOKEN`.
- `key_base64` (String, Sensitive) Base64 encoded TLS auth key. This can also be set with the environment variable `TF_TELEPORT_KEY_BASE64`.
Expand Down
93 changes: 91 additions & 2 deletions integrations/terraform/go.mod

Large diffs are not rendered by default.

263 changes: 250 additions & 13 deletions integrations/terraform/go.sum

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions integrations/terraform/provider/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ func (CredentialsFromNativeMachineID) Credentials(ctx context.Context, config pr
addr := stringFromConfigOrEnv(config.Addr, constants.EnvVarTerraformAddress, "")
caPath := stringFromConfigOrEnv(config.RootCaPath, constants.EnvVarTerraformRootCertificates, "")
gitlabIDTokenEnvVar := stringFromConfigOrEnv(config.GitlabIDTokenEnvVar, constants.EnvVarGitlabIDTokenEnvVar, "")
insecure := boolFromConfigOrEnv(config.Insecure, constants.EnvVarTerraformInsecure)

if joinMethod == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformJoinMethod, constants.EnvVarTerraformJoinMethod)
Expand Down Expand Up @@ -531,6 +532,7 @@ See https://goteleport.com/docs/reference/join-methods for more details.`)
TTL: time.Hour,
RenewalInterval: 20 * time.Minute,
},
Insecure: insecure,
}
// slog default logger has been configured during the provider init.
bot, err := embeddedtbot.New(botConfig, slog.Default())
Expand Down
21 changes: 21 additions & 0 deletions integrations/terraform/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const (
attributeTerraformIdentityFile = "identity_file"
// attributeTerraformIdentityFileBase64 is the attribute containing the base64-encoded identity file used by the Terraform provider.
attributeTerraformIdentityFileBase64 = "identity_file_base64"
// attributeTerraformInsecure is the attribute used to control whether the Terraform provider will skip verifying the proxy server's TLS certificate.
attributeTerraformInsecure = "insecure"
// attributeTerraformRetryBaseDuration is the attribute configuring the base duration between two Terraform provider retries.
attributeTerraformRetryBaseDuration = "retry_base_duration"
// attributeTerraformRetryCapDuration is the attribute configuring the maximum duration between two Terraform provider retries.
Expand Down Expand Up @@ -135,6 +137,8 @@ type providerData struct {
IdentityFile types.String `tfsdk:"identity_file"`
// IdentityFile identity file content encoded in base64
IdentityFileBase64 types.String `tfsdk:"identity_file_base64"`
// Insecure means the provider will skip verifying the proxy server's TLS certificate.
Insecure types.Bool `tfsdk:"insecure"`
// RetryBaseDuration is used to setup the retry algorithm when the API returns 'not found'
RetryBaseDuration types.String `tfsdk:"retry_base_duration"`
// RetryCapDuration is used to setup the retry algorithm when the API returns 'not found'
Expand Down Expand Up @@ -228,6 +232,11 @@ func (p *Provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics)
Optional: true,
Description: fmt.Sprintf("Teleport identity file content base64 encoded. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformIdentityFileBase64),
},
attributeTerraformInsecure: {
Type: types.BoolType,
Optional: true,
Description: fmt.Sprintf("Skip proxy certificate verification when joining the Teleport cluster. This is not recommended for production use. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformInsecure),
},
attributeTerraformRetryBaseDuration: {
Type: types.StringType,
Sensitive: false,
Expand Down Expand Up @@ -315,6 +324,7 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq
retryCapDurationStr := stringFromConfigOrEnv(config.RetryCapDuration, constants.EnvVarTerraformRetryCapDuration, "5s")
maxTriesStr := stringFromConfigOrEnv(config.RetryMaxTries, constants.EnvVarTerraformRetryMaxTries, "10")
dialTimeoutDurationStr := stringFromConfigOrEnv(config.DialTimeoutDuration, constants.EnvVarTerraformDialTimeoutDuration, "30s")
insecure := boolFromConfigOrEnv(config.Insecure, constants.EnvVarTerraformInsecure)

if !p.validateAddr(addr, resp) {
return
Expand Down Expand Up @@ -349,6 +359,7 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq
grpc.WaitForReady(true),
),
},
InsecureAddressDiscovery: insecure,
}

clt, diags := activeSources.BuildClient(ctx, clientConfig, config)
Expand Down Expand Up @@ -449,6 +460,16 @@ func stringFromConfigOrEnv(value types.String, env string, def string) string {
return configValue
}

func boolFromConfigOrEnv(value types.Bool, env string) bool {
envVar := os.Getenv(env)
if envVar != "" && (value.Unknown || value.Null) {
if b, err := strconv.ParseBool(envVar); err == nil {
return b
}
}
return value.Value
}

// validateAddr validates passed addr
func (p *Provider) validateAddr(addr string, resp *tfsdk.ConfigureProviderResponse) bool {
if addr == "" {
Expand Down
127 changes: 127 additions & 0 deletions integrations/terraform/testlib/machineid_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import (
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/lib/testing/fakejoin"
"github.com/gravitational/teleport/integrations/lib/testing/integration"
kubetoken "github.com/gravitational/teleport/lib/kube/token"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/tool/teleport/testenv"

"github.com/gravitational/teleport/integrations/terraform/provider"
)
Expand Down Expand Up @@ -164,3 +166,128 @@ func TestTerraformJoin(t *testing.T) {
},
})
}

func TestTerraformJoinViaProxy(t *testing.T) {
require.NoError(t, os.Setenv("TF_ACC", "true"))

// Test setup: start a full Teleport process including a proxy.
process := testenv.MakeTestServer(t)
clt := testenv.MakeDefaultAuthClient(t, process)

var err error
// Test setup: get the terraform role
tfRole, err := clt.GetRole(t.Context(), teleport.PresetTerraformProviderRoleName)
require.NoError(t, err)

// Test setup: create a fake Kubernetes signer that will allow us to use the kubernetes/jwks join method
clock := clockwork.NewRealClock()
signer, err := fakejoin.NewKubernetesSigner(clock)
require.NoError(t, err)

jwks, err := signer.GetMarshaledJWKS()
require.NoError(t, err)

// Test setup: create a token and a bot that can join the cluster with JWT signed by our fake Kubernetes signer
testBotName := "testBot"
testTokenName := "testToken"
fakeNamespace := "test-namespace"
fakeServiceAccount := "test-service-account"
token, err := types.NewProvisionTokenFromSpec(
testTokenName,
clock.Now().Add(time.Hour),
types.ProvisionTokenSpecV2{
Roles: types.SystemRoles{types.RoleBot},
JoinMethod: types.JoinMethodKubernetes,
BotName: testBotName,
Kubernetes: &types.ProvisionTokenSpecV2Kubernetes{
Allow: []*types.ProvisionTokenSpecV2Kubernetes_Rule{
{
ServiceAccount: fmt.Sprintf("%s:%s", fakeNamespace, fakeServiceAccount),
},
},
Type: types.KubernetesJoinTypeStaticJWKS,
StaticJWKS: &types.ProvisionTokenSpecV2Kubernetes_StaticJWKSConfig{
JWKS: jwks,
},
},
})
require.NoError(t, err)
err = clt.CreateToken(t.Context(), token)
require.NoError(t, err)

bot := &machineidv1.Bot{
Kind: types.KindBot,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: testBotName,
},
Spec: &machineidv1.BotSpec{
Roles: []string{tfRole.GetName()},
},
}
_, err = clt.BotServiceClient().CreateBot(t.Context(), &machineidv1.CreateBotRequest{Bot: bot})
require.NoError(t, err)

// Test setup: sign a Kube JWT for our bot to join the cluster
// We sign the token, write it to a temporary file, and point the embedded tbot to it
// with an environment variable.
pong, err := clt.Ping(t.Context())
require.NoError(t, err)
clusterName := pong.ClusterName
jwt, err := signer.SignServiceAccountJWT("pod-name-doesnt-matter", fakeNamespace, fakeServiceAccount, clusterName)
require.NoError(t, err)

tempDir := t.TempDir()
jwtPath := filepath.Join(tempDir, "token")
require.NoError(t, os.WriteFile(jwtPath, []byte(jwt), 0600))
require.NoError(t, os.Setenv(kubetoken.EnvVarCustomKubernetesTokenPath, jwtPath))

// Test setup: craft a Terraform provider configuration
proxyAddr, err := process.ProxyTunnelAddr()
require.NoError(t, err)

terraformConfig := fmt.Sprintf(`
provider "teleport" {
addr = %q
join_token = %q
join_method = %q
insecure = true
retry_base_duration = "900ms"
retry_cap_duration = "4s"
retry_max_tries = "12"
}
`, proxyAddr, testTokenName, types.JoinMethodKubernetes)

terraformProvider := provider.New()
terraformProviders := make(map[string]func() (tfprotov6.ProviderServer, error))
terraformProviders["teleport"] = func() (tfprotov6.ProviderServer, error) {
// Terraform configures provider on every test step, but does not clean up previous one, which produces
// to "too many open files" at some point.
//
// With this statement we try to forcefully close previously opened client, which stays cached in
// the provider variable.
p, ok := terraformProvider.(*provider.Provider)
require.True(t, ok)
require.NoError(t, p.Close())
return providerserver.NewProtocol6(terraformProvider)(), nil
}

// Test execution: apply a TF resource with the provider joining via MachineID
dummyResource, err := fixtures.ReadFile(filepath.Join("fixtures", "app_0_create.tf"))
require.NoError(t, err)
testConfig := terraformConfig + "\n" + string(dummyResource)
name := "teleport_app.test"

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: terraformProviders,
Steps: []resource.TestStep{
{
Config: testConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "kind", "app"),
resource.TestCheckResourceAttr(name, "spec.uri", "localhost:3000"),
),
},
},
})
}
Loading