Skip to content

Commit

Permalink
[v16] RFD 173 implementation: Terraform provider UX improvements (#44690
Browse files Browse the repository at this point in the history
)

* Introduce the `tctl terraform env` command (#43664)

* Introduce the `tctl terrafor env` command

* fix tests

* address marco's feedback + use correct b64 lib

* add license

* add created-by label as specified in the RFD

* Update tool/tctl/common/terraform_command.go

Co-authored-by: Roman Tkachenko <[email protected]>

* Apply suggestions from code review

Co-authored-by: Roman Tkachenko <[email protected]>

* Have telpeort create the Terraform default role

* rename use-existing-role -> role, and stop hijacking identity.SSHCACertBytes

* Make the terraform provider role a real preset, rename to 'terraform-provider'

* lint

* Fix tbot's invocation after rebase

---------

Co-authored-by: Roman Tkachenko <[email protected]>

* Refactor Terraform credential loading (#44037)

* Refactor Terraform credential loading

* Warn about expiry

* kip expired credentials

* fixup! kip expired credentials

* Use constants everywhere + add godocs

* fixup! Use constants everywhere + add godocs

* Address marco's feedback

* fixup! Address marco's feedback

* tidy go mod

* lint

* re-render TF docs

* Update v16 version in error message

* Add Terraform Provider native MachineID support (#44306)

* Add Terraform Provider native MachineID support

* Reject 'token' join method

* lint: fix imports

* re-render TF docs

* fix tests + add license

* lint

* tidy go mod

* use v16 client.Expiry() function

---------

Co-authored-by: Roman Tkachenko <[email protected]>
  • Loading branch information
hugoShaka and r0mant authored Aug 26, 2024
1 parent f4ce209 commit 9d61809
Show file tree
Hide file tree
Showing 24 changed files with 2,148 additions and 258 deletions.
59 changes: 59 additions & 0 deletions api/client/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,62 @@ func (d *DynamicIdentityFileCreds) Expiry() (time.Time, bool) {

return x509Cert.NotAfter, true
}

// KeyPair returns a Credential give a TLS key, certificate and CA certificates PEM-encoded.
// It behaves live LoadKeyPair except it doesn't read the TLS material from a file.
// This is useful when key and certs are not on the disk (e.g. environment variables).
// This should be preferred over manually building a tls.Config and calling LoadTLS
// as Credentials returned by KeyPair can report their expiry, which allows to warn
// the user in case of expired certificates.
func KeyPair(certPEM, keyPEM, caPEM []byte) (Credentials, error) {
if len(certPEM) == 0 {
return nil, trace.BadParameter("missing certificate PEM data")
}
if len(keyPEM) == 0 {
return nil, trace.BadParameter("missing private key PEM data")
}
return &staticKeypairCreds{
certPEM: certPEM,
keyPEM: keyPEM,
caPEM: caPEM,
}, nil
}

// staticKeypairCreds uses keypair certificates to provide client credentials.
type staticKeypairCreds struct {
certPEM []byte
keyPEM []byte
caPEM []byte
}

// TLSConfig returns TLS configuration.
func (c *staticKeypairCreds) TLSConfig() (*tls.Config, error) {
cert, err := keys.X509KeyPair(c.certPEM, c.keyPEM)
if err != nil {
return nil, trace.Wrap(err)
}

pool := x509.NewCertPool()
if ok := pool.AppendCertsFromPEM(c.caPEM); !ok {
return nil, trace.BadParameter("invalid TLS CA cert PEM")
}

return configureTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: pool,
}), nil
}

// SSHClientConfig returns SSH configuration.
func (c *staticKeypairCreds) SSHClientConfig() (*ssh.ClientConfig, error) {
return nil, trace.NotImplemented("no ssh config")
}

// Expiry returns the credential expiry.
func (c *staticKeypairCreds) Expiry() (time.Time, bool) {
cert, _, err := keys.X509Certificate(c.certPEM)
if err != nil {
return time.Time{}, false
}
return cert.NotAfter, true
}
26 changes: 26 additions & 0 deletions api/client/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,32 @@ func TestLoadKeyPair(t *testing.T) {
require.False(t, ok, "expiry should be unknown on a broken credential")
}

func TestKeyPair(t *testing.T) {
t.Parallel()

// Load expected tls.Config.
expectedTLSConfig := getExpectedTLSConfig(t)

// Load key pair from disk.
creds, err := KeyPair(tlsCert, keyPEM, tlsCACert)
require.NoError(t, err)

// Build tls.Config and compare to expected tls.Config.
tlsConfig, err := creds.TLSConfig()
require.NoError(t, err)
requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig)

// Load invalid keypairs.
invalidIdentityCreds, err := KeyPair([]byte("invalid_cert"), []byte("invalid_key"), []byte("invalid_ca_cert"))
require.NoError(t, err)
_, err = invalidIdentityCreds.TLSConfig()
require.Error(t, err)

// Load missing keypairs
_, err = KeyPair(nil, nil, nil)
require.Error(t, err)
}

func TestLoadProfile(t *testing.T) {
t.Parallel()
profileName := "proxy.example.com"
Expand Down
47 changes: 47 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,50 @@ const (
// Multiple decisions can be sent for the same request if the policy requires it.
FileTransferDecision string = "[email protected]"
)

// Terraform provider environment variable names.
// This is mainly used by the Terraform provider and the `tctl terraform` command.
const (
// EnvVarTerraformAddress is the environment variable configuring the Teleport address the Terraform provider connects to.
EnvVarTerraformAddress = "TF_TELEPORT_ADDR"
// EnvVarTerraformCertificates is the environment variable configuring the path the Terraform provider loads its
// client certificates from. This only works for direct auth joining.
EnvVarTerraformCertificates = "TF_TELEPORT_CERT"
// EnvVarTerraformCertificatesBase64 is the environment variable configuring the client certificates used by the
// Terraform provider. This only works for direct auth joining.
EnvVarTerraformCertificatesBase64 = "TF_TELEPORT_CERT_BASE64"
// EnvVarTerraformKey is the environment variable configuring the path the Terraform provider loads its
// client key from. This only works for direct auth joining.
EnvVarTerraformKey = "TF_TELEPORT_KEY"
// EnvVarTerraformKeyBase64 is the environment variable configuring the client key used by the
// Terraform provider. This only works for direct auth joining.
EnvVarTerraformKeyBase64 = "TF_TELEPORT_KEY_BASE64"
// EnvVarTerraformRootCertificates is the environment variable configuring the path the Terraform provider loads its
// trusted CA certificates from. This only works for direct auth joining.
EnvVarTerraformRootCertificates = "TF_TELEPORT_ROOT_CA"
// EnvVarTerraformRootCertificatesBase64 is the environment variable configuring the CA certificates trusted by the
// Terraform provider. This only works for direct auth joining.
EnvVarTerraformRootCertificatesBase64 = "TF_TELEPORT_CA_BASE64"
// EnvVarTerraformProfileName is the environment variable containing name of the profile used by the Terraform provider.
EnvVarTerraformProfileName = "TF_TELEPORT_PROFILE_NAME"
// EnvVarTerraformProfilePath is the environment variable containing the profile directory used by the Terraform provider.
EnvVarTerraformProfilePath = "TF_TELEPORT_PROFILE_PATH"
// EnvVarTerraformIdentityFilePath is the environment variable containing the path to the identity file used by the provider.
EnvVarTerraformIdentityFilePath = "TF_TELEPORT_IDENTITY_FILE_PATH"
// EnvVarTerraformIdentityFile is the environment variable containing the identity file used by the Terraform provider.
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"
// 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.
EnvVarTerraformRetryCapDuration = "TF_TELEPORT_RETRY_CAP_DURATION"
// EnvVarTerraformRetryMaxTries is the environment variable configuring the maximum number of Terraform provider retries.
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"
)
4 changes: 4 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ const (
// resources.
PresetRequireTrustedDeviceRoleName = "require-trusted-device"

// PresetTerraformProviderRoleName is a name of a default role that allows the Terraform provider
// to configure all its supported Teleport resources.
PresetTerraformProviderRoleName = "terraform-provider"

// SystemAutomaticAccessApprovalRoleName names a preset role that may
// automatically approve any Role Access Request
SystemAutomaticAccessApprovalRoleName = "@teleport-access-approver"
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/reference/terraform-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ This auth method has the following limitations:

### Optional

- `addr` (String) host:port where Teleport Auth Service is running. This can also be set with the environment variable `TF_TELEPORT_ADDR`.
- `addr` (String) host:port of the Teleport address. This can be the Teleport Proxy Service address (port 443 or 4080) or the Teleport Auth Service address (port 3025). This can also be set with the environment variable `TF_TELEPORT_ADDR`.
- `cert_base64` (String) Base64 encoded TLS auth certificate. This can also be set with the environment variable `TF_TELEPORT_CERT_BASE64`.
- `cert_path` (String) Path to Teleport auth certificate file. This can also be set with the environment variable `TF_TELEPORT_CERT`.
- `dial_timeout_duration` (String) DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `TF_TELEPORT_DIAL_TIMEOUT_DURATION`.
- `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
9 changes: 7 additions & 2 deletions integration/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func CloseAgent(teleAgent *teleagent.AgentServer, socketDirPath string) error {
return nil
}

func MustCreateUserIdentityFile(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) string {
func MustCreateUserKey(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) *client.Key {
key, err := client.GenerateRSAKey()
require.NoError(t, err)
key.ClusterName = tc.Secrets.SiteName
Expand All @@ -209,9 +209,14 @@ func MustCreateUserIdentityFile(t *testing.T, tc *TeleInstance, username string,
hostCAs, err := tc.Process.GetAuthServer().GetCertAuthorities(context.Background(), types.HostCA, false)
require.NoError(t, err)
key.TrustedCerts = authclient.AuthoritiesToTrustedCerts(hostCAs)
return key
}

func MustCreateUserIdentityFile(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) string {
key := MustCreateUserKey(t, tc, username, ttl)

idPath := filepath.Join(t.TempDir(), "user_identity")
_, err = identityfile.Write(context.Background(), identityfile.WriteConfig{
_, err := identityfile.Write(context.Background(), identityfile.WriteConfig{
OutputPath: idPath,
Key: key,
Format: identityfile.FormatFile,
Expand Down
Loading

0 comments on commit 9d61809

Please sign in to comment.