Skip to content

Commit

Permalink
Add Terraform Provider native MachineID support (#44306)
Browse files Browse the repository at this point in the history
* Add Terraform Provider native MachineID support

* Reject 'token' join method

* lint: fix imports

* re-render TF docs

* fix tests + add license

* lint
  • Loading branch information
hugoShaka committed Aug 2, 2024
1 parent 4efea65 commit 0c5c76f
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 13 deletions.
4 changes: 4 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,4 +500,8 @@ const (
EnvVarTerraformRetryMaxTries = "TF_TELEPORT_RETRY_MAX_TRIES"
// EnvVarTerraformDialTimeoutDuration is the environment variable configuring the Terraform provider dial timeout.
EnvVarTerraformDialTimeoutDuration = "TF_TELEPORT_DIAL_TIMEOUT_DURATION"
// EnvVarTerraformJoinMethod is the environment variable configuring the Terraform provider native MachineID join method.
EnvVarTerraformJoinMethod = "TF_TELEPORT_JOIN_METHOD"
// EnvVarTerraformJoinToken is the environment variable configuring the Terraform provider native MachineID join token.
EnvVarTerraformJoinToken = "TF_TELEPORT_JOIN_TOKEN"
)
2 changes: 2 additions & 0 deletions docs/pages/reference/terraform-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ 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`.
- `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`.
- `key_path` (String) Path to Teleport auth key file. This can also be set with the environment variable `TF_TELEPORT_KEY`.
- `profile_dir` (String) Teleport profile path. This can also be set with the environment variable `TF_TELEPORT_PROFILE_PATH`.
Expand Down
26 changes: 19 additions & 7 deletions integrations/lib/embeddedtbot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (b *EmbeddedBot) Start(ctx context.Context) error {
}
}

func (b *EmbeddedBot) waitForClient(ctx context.Context, deadline time.Duration) (*client.Client, error) {
func (b *EmbeddedBot) waitForCredentials(ctx context.Context, deadline time.Duration) (client.Credentials, error) {
waitCtx, cancel := context.WithTimeout(ctx, deadline)
defer cancel()

Expand All @@ -148,20 +148,32 @@ func (b *EmbeddedBot) waitForClient(ctx context.Context, deadline time.Duration)
log.Infof("credential ready")
}

c, err := b.buildClient(ctx)
return c, trace.Wrap(err)

return b.credential, nil
}

// StartAndWaitForClient starts the EmbeddedBot and waits for a client to be available.
// This is the proper way of starting the EmbeddedBot. It returns an error if the
// EmbeddedBot is not able to get a certificate before the deadline.
// It returns an error if the EmbeddedBot is not able to get a certificate before the deadline.
// If you need a client.Credentials instead, you can use StartAndWaitForCredentials.
func (b *EmbeddedBot) StartAndWaitForClient(ctx context.Context, deadline time.Duration) (*client.Client, error) {
b.start(ctx)
c, err := b.waitForClient(ctx, deadline)
_, err := b.waitForCredentials(ctx, deadline)
if err != nil {
return nil, trace.Wrap(err)
}

c, err := b.buildClient(ctx)
return c, trace.Wrap(err)
}

// StartAndWaitForCredentials starts the EmbeddedBot and waits for credentials to become ready.
// It returns an error if the EmbeddedBot is not able to get a certificate before the deadline.
// If you need a client.Client instead, you can use StartAndWaitForClient.
func (b *EmbeddedBot) StartAndWaitForCredentials(ctx context.Context, deadline time.Duration) (client.Credentials, error) {
b.start(ctx)
creds, err := b.waitForCredentials(ctx, deadline)
return creds, trace.Wrap(err)
}

// buildClient reads tbot's memory disttination, retrieves the certificates
// and builds a new Teleport client using those certs.
func (b *EmbeddedBot) buildClient(ctx context.Context) (*client.Client, error) {
Expand Down
2 changes: 1 addition & 1 deletion integrations/lib/embeddedtbot/bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestBotJoinAuth(t *testing.T) {
require.NoError(t, err)
require.Equal(t, clusterName, pong.ClusterName)

botClient, err := bot.waitForClient(ctx, 10*time.Second)
botClient, err := bot.StartAndWaitForClient(ctx, 10*time.Second)
require.NoError(t, err)
botPong, err := botClient.Ping(ctx)
require.NoError(t, err)
Expand Down
2 changes: 2 additions & 0 deletions integrations/terraform/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
Expand Down Expand Up @@ -290,6 +291,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
Expand Down
12 changes: 12 additions & 0 deletions integrations/terraform/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,8 @@ github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
Expand Down Expand Up @@ -1501,6 +1503,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -1827,6 +1833,8 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8=
github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
Expand Down Expand Up @@ -1918,6 +1926,8 @@ github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
Expand Down Expand Up @@ -2425,6 +2435,8 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
Expand Down
83 changes: 83 additions & 0 deletions integrations/terraform/provider/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ import (

"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/constants"
apitypes "github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/lib/embeddedtbot"
tbotconfig "github.com/gravitational/teleport/lib/tbot/config"
)

var supportedCredentialSources = CredentialSources{
CredentialsFromNativeMachineID{},
CredentialsFromKeyAndCertPath{},
CredentialsFromKeyAndCertBase64{},
CredentialsFromIdentityFilePath{},
Expand Down Expand Up @@ -453,6 +457,85 @@ func (CredentialsFromProfile) Credentials(ctx context.Context, config providerDa
return client.LoadProfile(profileDir, profileName), nil
}

// CredentialsFromNativeMachineID builds credentials by performing a MachineID join and
type CredentialsFromNativeMachineID struct{}

// Name implements CredentialSource and returns the source name.
func (CredentialsFromNativeMachineID) Name() string {
return "by performing native MachineID joining"
}

// IsActive implements CredentialSource and returns if the source is active and why.
func (CredentialsFromNativeMachineID) IsActive(config providerData) (bool, string) {
joinMethod := stringFromConfigOrEnv(config.JoinMethod, constants.EnvVarTerraformJoinMethod, "")
joinToken := stringFromConfigOrEnv(config.JoinToken, constants.EnvVarTerraformJoinToken, "")

// This method is active as soon as a token or a join method are set.
active := joinMethod != "" || joinToken != ""
return activeReason(
active,
attributeTerraformJoinMethod, attributeTerraformJoinToken,
constants.EnvVarTerraformJoinMethod, constants.EnvVarTerraformJoinToken,
)
}

// Credentials implements CredentialSource and returns a client.Credentials for the provider.
func (CredentialsFromNativeMachineID) Credentials(ctx context.Context, config providerData) (client.Credentials, error) {
joinMethod := stringFromConfigOrEnv(config.JoinMethod, constants.EnvVarTerraformJoinMethod, "")
joinToken := stringFromConfigOrEnv(config.JoinToken, constants.EnvVarTerraformJoinToken, "")
addr := stringFromConfigOrEnv(config.Addr, constants.EnvVarTerraformAddress, "")
caPath := stringFromConfigOrEnv(config.RootCaPath, constants.EnvVarTerraformRootCertificates, "")

if joinMethod == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformJoinMethod, constants.EnvVarTerraformJoinMethod)
}
if joinToken == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformJoinMethod, constants.EnvVarTerraformJoinMethod)
}
if addr == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformAddress, constants.EnvVarTerraformAddress)
}

if apitypes.JoinMethod(joinMethod) == apitypes.JoinMethodToken {
return nil, trace.BadParameter(`the secret token join method ('token') is not supported for native Machine ID joining.
Secret tokens are single use and the Terraform provider does not save the certificates it obtained, so the token join method can only be used once.
If you want to run the Terraform provider in the CI (GitHub Actions, GitlabCI, Circle CI) or in a supported runtime (AWS, GCP, Azure, Kubernetes, machine with a TPM)
you should use the join method specific to your environment.
If you want to use MachineID with secret tokens, the best approach is to run a local tbot on the server where the terraform provider runs.
See https://goteleport.com/docs/reference/join-methods for more details.`)
}

if err := apitypes.ValidateJoinMethod(apitypes.JoinMethod(joinMethod)); err != nil {
return nil, trace.Wrap(err, "Invalid Join Method")
}
botConfig := &embeddedtbot.BotConfig{
AuthServer: addr,
Onboarding: tbotconfig.OnboardingConfig{
TokenValue: joinToken,
CAPath: caPath,
JoinMethod: apitypes.JoinMethod(joinMethod),
},
CertificateTTL: time.Hour,
RenewalInterval: 20 * time.Minute,
}
bot, err := embeddedtbot.New(botConfig)
if err != nil {
return nil, trace.Wrap(err, "Failed to create bot configuration, this is a provider bug, please open a GitHub issue.")
}

preflightCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
_, err = bot.Preflight(preflightCtx)
if err != nil {
return nil, trace.Wrap(err, "Failed to preflight bot configuration")
}

creds, err := bot.StartAndWaitForCredentials(ctx, 20*time.Second /* deadline */)
return creds, trace.Wrap(err, "Waiting for bot to obtain credentials")
}

// activeReason renders a user-friendly active reason message describing if the credentials source is active
// and which parameters are controlling its activity.
func activeReason(active bool, params ...string) (bool, string) {
Expand Down
41 changes: 41 additions & 0 deletions integrations/terraform/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const (
attributeTerraformRetryMaxTries = "retry_max_tries"
// attributeTerraformDialTimeoutDuration is the attribute configuring the Terraform provider dial timeout.
attributeTerraformDialTimeoutDuration = "dial_timeout_duration"
// attributeTerraformJoinMethod is the attribute configuring the Terraform provider native MachineID join method.
attributeTerraformJoinMethod = "join_method"
// attributeTerraformJoinToken is the attribute configuring the Terraform provider native MachineID join token.
attributeTerraformJoinToken = "join_token"
)

type RetryConfig struct {
Expand All @@ -95,6 +99,7 @@ type Provider struct {
configured bool
Client *client.Client
RetryConfig RetryConfig
cancel context.CancelFunc
}

// providerData provider schema struct
Expand Down Expand Up @@ -131,6 +136,10 @@ type providerData struct {
RetryMaxTries types.String `tfsdk:"retry_max_tries"`
// DialTimeout sets timeout when trying to connect to the server.
DialTimeoutDuration types.String `tfsdk:"dial_timeout_duration"`
// JoinMethod is the MachineID join method.
JoinMethod types.String `tfsdk:"join_method"`
// JoinMethod is the MachineID join token.
JoinToken types.String `tfsdk:"join_token"`
}

// New returns an empty provider struct
Expand Down Expand Up @@ -229,6 +238,18 @@ func (p *Provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics)
Optional: true,
Description: fmt.Sprintf("DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformDialTimeoutDuration),
},
attributeTerraformJoinMethod: {
Type: types.StringType,
Sensitive: false,
Optional: true,
Description: fmt.Sprintf("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 `%s`.", constants.EnvVarTerraformJoinMethod),
},
attributeTerraformJoinToken: {
Type: types.StringType,
Sensitive: false,
Optional: true,
Description: fmt.Sprintf("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 `%s`.", constants.EnvVarTerraformJoinToken),
},
},
}, nil
}
Expand All @@ -249,6 +270,13 @@ func (p *Provider) IsConfigured(diags diag.Diagnostics) bool {
func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
p.configureLog()

// We wrap the provider's context into a cancellable one.
// This allows us to cancel the context and properly close the client and any background task potentially running
// (e.g. MachineID bot renewing creds). This is required during the tests as the provider is run multiple times.
// You can cancel the context by calling Provider.Close()
ctx, cancel := context.WithCancel(ctx)
p.cancel = cancel

var config providerData
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
Expand Down Expand Up @@ -486,3 +514,16 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc
"teleport_access_list": dataSourceTeleportAccessListType{},
}, nil
}

// Close closes the provider's client and cancels its context.
// This is needed in the tests to avoid accumulating clients and running out of file descriptors.
func (p *Provider) Close() error {
var err error
if p.Client != nil {
err = p.Client.Close()
}
if p.cancel != nil {
p.cancel()
}
return err
}
Loading

0 comments on commit 0c5c76f

Please sign in to comment.