diff --git a/api/constants/constants.go b/api/constants/constants.go index 0a6748599462b..a504937659a5b 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -503,4 +503,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" ) diff --git a/docs/pages/reference/terraform-provider.mdx b/docs/pages/reference/terraform-provider.mdx index 7d7b908b9c29b..3ef34a8dbb978 100644 --- a/docs/pages/reference/terraform-provider.mdx +++ b/docs/pages/reference/terraform-provider.mdx @@ -138,6 +138,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`. diff --git a/integrations/lib/embeddedtbot/bot.go b/integrations/lib/embeddedtbot/bot.go index 9573ab52c99e0..e693b40793fe5 100644 --- a/integrations/lib/embeddedtbot/bot.go +++ b/integrations/lib/embeddedtbot/bot.go @@ -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() @@ -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) { diff --git a/integrations/lib/embeddedtbot/bot_test.go b/integrations/lib/embeddedtbot/bot_test.go index 18075e81cf945..ca4bc9480af43 100644 --- a/integrations/lib/embeddedtbot/bot_test.go +++ b/integrations/lib/embeddedtbot/bot_test.go @@ -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) diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index 40e662b623984..e5d4160088965 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -139,6 +139,8 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 // indirect github.com/emicklei/go-restful/v3 v3.11.3 // indirect + github.com/envoyproxy/go-control-plane v0.12.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect @@ -152,6 +154,7 @@ require ( github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -221,6 +224,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 @@ -234,6 +238,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailgun/holster/v3 v3.16.2 // indirect github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f // indirect github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47 // indirect @@ -272,6 +277,7 @@ require ( github.com/pkg/sftp v1.13.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/posener/complete v1.2.3 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/pquerna/otp v1.4.0 // indirect github.com/prometheus/client_golang v1.19.0 // indirect @@ -286,6 +292,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/progressbar/v3 v3.14.2 // indirect github.com/scim2/filter-parser/v2 v2.2.0 // indirect + github.com/shirou/gopsutil/v4 v4.24.6 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sijms/go-ora/v2 v2.8.10 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -294,6 +302,8 @@ require ( github.com/spiffe/go-spiffe/v2 v2.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/thales-e-security/pool v0.0.2 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -308,6 +318,7 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zclconf/go-cty v1.14.4 // indirect github.com/zeebo/errs v1.3.0 // indirect github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 45d48438228b8..c50d2f8e00932 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -1082,6 +1082,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -1507,6 +1509,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= @@ -1558,6 +1564,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -1730,6 +1738,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= @@ -1802,6 +1812,12 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckySQL64= +github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -1855,6 +1871,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -1909,6 +1929,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= @@ -2250,6 +2272,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2304,6 +2327,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -2433,6 +2457,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= diff --git a/integrations/terraform/provider/credentials.go b/integrations/terraform/provider/credentials.go index 9e04ed9639909..0e6a26804ad65 100644 --- a/integrations/terraform/provider/credentials.go +++ b/integrations/terraform/provider/credentials.go @@ -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{}, @@ -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) { diff --git a/integrations/terraform/provider/provider.go b/integrations/terraform/provider/provider.go index 19b6a4a4f2cc2..a3e54686f5e93 100644 --- a/integrations/terraform/provider/provider.go +++ b/integrations/terraform/provider/provider.go @@ -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 { @@ -95,6 +99,7 @@ type Provider struct { configured bool Client *client.Client RetryConfig RetryConfig + cancel context.CancelFunc } // providerData provider schema struct @@ -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 @@ -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 } @@ -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...) @@ -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 +} diff --git a/integrations/terraform/testlib/machineid_join_test.go b/integrations/terraform/testlib/machineid_join_test.go new file mode 100644 index 0000000000000..52299c3cf457e --- /dev/null +++ b/integrations/terraform/testlib/machineid_join_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2024 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testlib + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + + 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" + "github.com/gravitational/teleport/lib/kubernetestoken" + "github.com/gravitational/teleport/lib/services" + + "github.com/gravitational/teleport/integrations/terraform/provider" +) + +func TestTerraformJoin(t *testing.T) { + require.NoError(t, os.Setenv("TF_ACC", "true")) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + // Test setup: start a Telpeort auth server + authHelper := &integration.MinimalAuthHelper{} + clt := authHelper.StartServer(t) + + var err error + // Test setup: create the terraform role + tfRole := services.NewPresetTerraformProviderRole() + tfRole, err = clt.CreateRole(ctx, tfRole) + 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(ctx, token) + require.NoError(t, err) + + bot := &machineidv1.Bot{ + Metadata: &headerv1.Metadata{ + Name: testBotName, + }, + Spec: &machineidv1.BotSpec{ + Roles: []string{tfRole.GetName()}, + }, + } + _, err = clt.BotServiceClient().CreateBot(ctx, &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(ctx) + 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(kubernetestoken.EnvVarCustomKubernetesTokenPath, jwtPath)) + + // Test setup: craft a Terraform provider configuration + terraformConfig := fmt.Sprintf(` + provider "teleport" { + addr = %q + join_token = %q + join_method = %q + retry_base_duration = "900ms" + retry_cap_duration = "4s" + retry_max_tries = "12" + } + `, authHelper.ServerAddr(), 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"), + ), + }, + }, + }) +} diff --git a/integrations/terraform/testlib/main_test.go b/integrations/terraform/testlib/main_test.go index c714f234d2b33..97d7d35aa805c 100644 --- a/integrations/terraform/testlib/main_test.go +++ b/integrations/terraform/testlib/main_test.go @@ -205,9 +205,7 @@ func (s *TerraformBaseSuite) closeClient() { s.T().Helper() p, ok := s.terraformProvider.(*provider.Provider) require.True(s.T(), ok) - if p != nil && p.Client != nil { - require.NoError(s.T(), p.Client.Close()) - } + require.NoError(s.T(), p.Close()) } // getFixture loads fixture and returns it as string or if failed diff --git a/lib/kubernetestoken/token_source.go b/lib/kubernetestoken/token_source.go index 5d40296f9dd6b..55a506937cc89 100644 --- a/lib/kubernetestoken/token_source.go +++ b/lib/kubernetestoken/token_source.go @@ -24,7 +24,10 @@ import ( "github.com/gravitational/trace" ) -const kubernetesDefaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" +const ( + kubernetesDefaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + EnvVarCustomKubernetesTokenPath = "KUBERNETES_TOKEN_PATH" +) type getEnvFunc func(key string) string type readFileFunc func(name string) ([]byte, error) @@ -33,7 +36,7 @@ func GetIDToken(getEnv getEnvFunc, readFile readFileFunc) (string, error) { // We check if we should use a custom location instead of the default one. This env var is not standard. // This is useful when the operator wants to use a custom projected token, or another service account. path := kubernetesDefaultTokenPath - if customPath := getEnv("KUBERNETES_TOKEN_PATH"); customPath != "" { + if customPath := getEnv(EnvVarCustomKubernetesTokenPath); customPath != "" { path = customPath }