mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Add tctl terraform env scope support (#68739)
This commit is contained in:
@@ -2839,6 +2839,7 @@ Flags:
|
||||
|`--bot-ttl`|`1h`|Time-to-live of the Bot resource. The bot will be removed after this period. Defaults to \[1h\]|
|
||||
|`--resource-prefix`|`tctl-terraform-env-`|Resource prefix to use when creating the Terraform role and bots. Defaults to \[tctl-terraform-env-\]|
|
||||
|`--role`|*no default*|Role used by Terraform. The role must already exist in Teleport. When not specified, uses the default role "terraform-provider"|
|
||||
|`--scope`|*no default*|Scope of the bot used by the Terraform provider. When empty, TF provider runs unscoped.|
|
||||
|
||||
## tctl tokens add
|
||||
|
||||
|
||||
@@ -35,16 +35,24 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/gravitational/teleport/api/breaker"
|
||||
"github.com/gravitational/teleport/api/client"
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
|
||||
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/wait"
|
||||
"github.com/gravitational/teleport/integration/helpers"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/reversetunnelclient"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
||||
"github.com/gravitational/teleport/lib/service/servicecfg"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
||||
"github.com/gravitational/teleport/tool/tctl/common"
|
||||
@@ -200,6 +208,141 @@ func TestTCTLTerraformCommand_AuthJoin(t *testing.T) {
|
||||
connectWithCredentialsFromVars(t, vars, clt)
|
||||
}
|
||||
|
||||
// TestTCTLTerraformCommand_ScopedProxyJoin validates that the command `tctl terraform env`
|
||||
// can run from an unscoped admin against a Teleport Proxy service and generates valid
|
||||
// scoped credentials Terraform can use to connect to Teleport.
|
||||
func TestTCTLTerraformCommand_ScopedProxyJoin(t *testing.T) {
|
||||
// test is not Parallel because of the metrics black hole
|
||||
testDir := t.TempDir()
|
||||
prometheus.DefaultRegisterer = metricRegistryBlackHole{}
|
||||
|
||||
// Test setup: creating a teleport instance running auth and proxy
|
||||
clusterName := "root.example.com"
|
||||
cfg := helpers.InstanceConfig{
|
||||
ClusterName: clusterName,
|
||||
HostID: uuid.New().String(),
|
||||
NodeName: helpers.Loopback,
|
||||
Logger: logtest.NewLogger(),
|
||||
}
|
||||
cfg.Listeners = helpers.SingleProxyPortSetup(t, &cfg.Fds)
|
||||
rc := helpers.NewInstance(t, cfg)
|
||||
|
||||
rcConf := servicecfg.MakeDefaultConfig()
|
||||
rcConf.DataDir = filepath.Join(testDir, "data")
|
||||
rcConf.Auth.Enabled = true
|
||||
rcConf.Proxy.Enabled = true
|
||||
rcConf.SSH.Enabled = false
|
||||
rcConf.Proxy.DisableWebInterface = true
|
||||
rcConf.Version = "v3"
|
||||
rcConf.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
|
||||
rcConf.ScopesFeatures = scopes.Features{Enabled: true}
|
||||
|
||||
testUsername := "test-user"
|
||||
createTCTLTerraformCrossScopeAdmin(t, testUsername, rc)
|
||||
|
||||
// Test setup: starting the Teleport instance
|
||||
err := rc.CreateEx(t, nil, rcConf)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = rc.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, rc.StopAll())
|
||||
})
|
||||
|
||||
// Test setup: obtaining and authclient connected via the proxy
|
||||
clt := getAuthClientForProxy(t, rc, testUsername, time.Hour)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
_, err = clt.Ping(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
createDefaultTerraformScopedRole(t, ctx, clt)
|
||||
|
||||
addr, err := rc.Process.ProxyWebAddr()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test execution, running the tctl command
|
||||
tctlCfg := &servicecfg.Config{}
|
||||
err = tctlCfg.SetAuthServerAddresses([]utils.NetAddr{*addr})
|
||||
require.NoError(t, err)
|
||||
tctlCommand := common.TerraformCommand{}
|
||||
|
||||
const testScope = "/test"
|
||||
app := kingpin.New("test", "test")
|
||||
tctlCommand.Initialize(app, nil, tctlCfg)
|
||||
_, err = app.Parse([]string{"terraform", "env", "--scope", testScope})
|
||||
require.NoError(t, err)
|
||||
// Create io buffer writer
|
||||
stdout := &bytes.Buffer{}
|
||||
|
||||
err = tctlCommand.RunEnvCommand(ctx, clt, stdout, os.Stderr)
|
||||
require.NoError(t, err)
|
||||
|
||||
vars := parseExportedEnvVars(t, stdout)
|
||||
require.Contains(t, vars, constants.EnvVarTerraformAddress)
|
||||
require.Contains(t, vars, constants.EnvVarTerraformIdentityFileBase64)
|
||||
|
||||
// Test validation: connect with the credentials in env vars and do a ping
|
||||
require.Equal(t, addr.String(), vars[constants.EnvVarTerraformAddress])
|
||||
|
||||
connectWithCredentialsFromVars(t, vars, clt)
|
||||
|
||||
// Test validation: make sure no role assignment is left. We don't know the assignment's name so we have to list them all initially.
|
||||
resp, err := clt.ScopedAccessServiceClient().ListScopedRoleAssignments(ctx, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
|
||||
ScopeFilter: scopesv1.Filter_builder{
|
||||
Scope: testScope,
|
||||
Mode: scopesv1.Mode_MODE_EXACT,
|
||||
}.Build(),
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
switch len(resp.GetAssignments()) {
|
||||
case 0:
|
||||
// assignment already deleted
|
||||
return
|
||||
case 1:
|
||||
// Assignment still exists, this might be a race against the cache.
|
||||
// Will retry a few times.
|
||||
_, err = wait.UntilNotFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
|
||||
return clt.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
||||
Name: resp.GetAssignments()[0].GetMetadata().GetName(),
|
||||
SubKind: resp.GetAssignments()[0].GetSubKind(),
|
||||
Scope: resp.GetAssignments()[0].GetScope(),
|
||||
}.Build())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Fail(t, "unexpected number of role assignments: %v", len(resp.GetAssignments()))
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultTerraformScopedRole(t *testing.T, ctx context.Context, clt *authclient.Client) {
|
||||
reader, err := utils.OpenFileNoUnsafeLinks("../integrations/terraform/examples/provider/scoped-provider-role.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
decoder := kyaml.NewYAMLOrJSONDecoder(reader, defaults.LookaheadBufSize)
|
||||
var raw services.UnknownResource
|
||||
err = decoder.Decode(&raw)
|
||||
require.NoError(t, err)
|
||||
r, err := services.UnmarshalProtoResource[*scopedaccessv1.ScopedRole](raw.Raw, services.DisallowUnknown())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = clt.ScopedAccessServiceClient().CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
||||
Role: r,
|
||||
}.Build())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the role to enter the cache, there is a race. If the test wins, tctl will fail to find the role.
|
||||
_, err = wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleResponse, error) {
|
||||
return clt.ScopedAccessServiceClient().GetScopedRole(ctx,
|
||||
scopedaccessv1.GetScopedRoleRequest_builder{
|
||||
Name: r.GetMetadata().GetName(),
|
||||
Scope: r.GetScope(),
|
||||
}.Build())
|
||||
}, wait.WithMaxTries(7))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createTCTLTerraformUserAndRole(t *testing.T, username string, instance *helpers.TeleInstance) {
|
||||
// Test setup: creating a test user and its role
|
||||
role, err := types.NewRole("test-role", types.RoleSpecV6{
|
||||
@@ -218,6 +361,32 @@ func createTCTLTerraformUserAndRole(t *testing.T, username string, instance *hel
|
||||
instance.AddUserWithRole(username, role)
|
||||
}
|
||||
|
||||
func createTCTLTerraformCrossScopeAdmin(t *testing.T, username string, instance *helpers.TeleInstance) {
|
||||
// Test setup: creating a test user and its role
|
||||
role, err := types.NewRole("test-role-full-admin", types.RoleSpecV6{
|
||||
Options: types.RoleOptions{},
|
||||
Allow: types.RoleConditions{
|
||||
Rules: []types.Rule{
|
||||
{
|
||||
Resources: []string{types.KindScopedToken},
|
||||
Verbs: []string{types.VerbRead, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
|
||||
},
|
||||
{
|
||||
Resources: []string{scopedaccess.KindScopedRole, scopedaccess.KindScopedRoleAssignment},
|
||||
Verbs: []string{types.VerbReadNoSecrets, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
|
||||
},
|
||||
{
|
||||
Resources: []string{types.KindBot},
|
||||
Verbs: []string{types.VerbRead, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
instance.AddUserWithRole(username, role)
|
||||
}
|
||||
|
||||
// getAuthCLientForProxy builds an authclient.CLient connecting to the auth through the proxy
|
||||
// (with a web client resolver hitting /v1/wenapi/ping and a tunnel auth dialer reaching the auth through the proxy).
|
||||
// For the tests, the client is configured to trust the proxy TLS certs on first connection.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
version: v1
|
||||
kind: scoped_role
|
||||
metadata:
|
||||
name: terraform-provider
|
||||
scope: "/"
|
||||
spec:
|
||||
assignable_scopes: ["/**"]
|
||||
rules:
|
||||
- resources: ["scoped_role", "scoped_role_assignment", "bot"]
|
||||
verbs:
|
||||
- list
|
||||
- create
|
||||
- readnosecrets
|
||||
- update
|
||||
- delete
|
||||
- resources: ["scoped_token"]
|
||||
verbs:
|
||||
- list
|
||||
- create
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
@@ -38,6 +37,7 @@ import (
|
||||
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
|
||||
scopedjoiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/wait"
|
||||
"github.com/gravitational/teleport/integration/helpers"
|
||||
"github.com/gravitational/teleport/integrations/lib/testing/integration"
|
||||
"github.com/gravitational/teleport/lib/oidc/fakeissuer"
|
||||
@@ -460,8 +460,8 @@ func TestTerraformJoinScoped(t *testing.T) {
|
||||
|
||||
// Test setup: Wait for the role assignment to enter the auth server cache
|
||||
// else the bot will race against the cache, joining will fail and the test wil be flaky.
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
resp, err := adminClient.ScopedAccessServiceClient().GetScopedRoleAssignment(
|
||||
resp, err := wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
|
||||
return adminClient.ScopedAccessServiceClient().GetScopedRoleAssignment(
|
||||
ctx,
|
||||
scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
||||
Name: testBotRole,
|
||||
@@ -469,10 +469,10 @@ func TestTerraformJoinScoped(t *testing.T) {
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
}.Build(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.GetAssignment())
|
||||
}, 5*time.Second, 100*time.Millisecond, "expected role assignment to enter the auth cache")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.GetAssignment())
|
||||
|
||||
// Test setup: Create a fake Kubernetes JWKS signer that will be used by the bot to join the cluster.
|
||||
const (
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
@@ -36,12 +37,18 @@ import (
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
|
||||
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
|
||||
scopedjoiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
|
||||
"github.com/gravitational/teleport/api/identityfile"
|
||||
"github.com/gravitational/teleport/api/mfa"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/common"
|
||||
"github.com/gravitational/teleport/api/utils/wait"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
||||
scopedjoining "github.com/gravitational/teleport/lib/scopes/joining"
|
||||
"github.com/gravitational/teleport/lib/service/servicecfg"
|
||||
"github.com/gravitational/teleport/lib/tbot/bot"
|
||||
"github.com/gravitational/teleport/lib/tbot/bot/connection"
|
||||
@@ -57,6 +64,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
terraformScopedRoleDefault = "/::" + teleport.PresetTerraformProviderRoleName
|
||||
terraformHelperDefaultResourcePrefix = "tctl-terraform-env-"
|
||||
terraformHelperDefaultTTL = "1h"
|
||||
|
||||
@@ -75,6 +83,7 @@ type TerraformCommand struct {
|
||||
resourcePrefix string
|
||||
existingRole string
|
||||
botTTL time.Duration
|
||||
scope string
|
||||
|
||||
cfg *servicecfg.Config
|
||||
|
||||
@@ -105,6 +114,10 @@ func (c *TerraformCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Globa
|
||||
"role",
|
||||
fmt.Sprintf("Role used by Terraform. The role must already exist in Teleport. When not specified, uses the default role %q", teleport.PresetTerraformProviderRoleName),
|
||||
).StringVar(&c.existingRole)
|
||||
c.envCmd.Flag(
|
||||
"scope",
|
||||
"Scope of the bot used by the Terraform provider. When empty, TF provider runs unscoped.",
|
||||
).StringVar(&c.scope)
|
||||
|
||||
// Save a pointer to the config to be able to recover the Debug config later
|
||||
c.cfg = cfg
|
||||
@@ -163,46 +176,93 @@ func (c *TerraformCommand) RunEnvCommand(ctx context.Context, client *authclient
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Checking Terraform role
|
||||
roleName, err := c.checkIfRoleExists(ctx, client)
|
||||
if err != nil {
|
||||
switch {
|
||||
case trace.IsNotFound(err) && c.existingRole == "":
|
||||
return trace.Wrap(err, `The Terraform role %q does not exist in your Teleport cluster.
|
||||
var onboard onboarding.Config
|
||||
scoped := c.scope != ""
|
||||
|
||||
if scoped {
|
||||
// We are running in a scope.
|
||||
roleSQN, err := c.checkIfScopedRoleExists(ctx, client.ScopedAccessServiceClient())
|
||||
if err != nil {
|
||||
if !trace.IsAccessDenied(err) {
|
||||
return trace.Wrap(err, "checking if scoped role exists")
|
||||
}
|
||||
// We currently have no way for a scoped user to look into the assignable roles from parent scopes.
|
||||
// So this check is likely to fail, even if the role exists.
|
||||
// This should be revisited once we have an API to list such assignable parent roles.
|
||||
}
|
||||
|
||||
token, assignment, err := c.createTransientScopedBotAndToken(ctx, client, roleSQN)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "creating transient scoped bot, token, and assigning role")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if _, err := client.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, scopedaccessv1.DeleteScopedRoleAssignmentRequest_builder{
|
||||
Name: assignment.GetMetadata().GetName(),
|
||||
Revision: assignment.GetMetadata().GetRevision(),
|
||||
SubKind: assignment.GetSubKind(),
|
||||
Scope: assignment.GetScope(),
|
||||
}.Build()); err != nil {
|
||||
c.showProgress(fmt.Sprintf("🚨 Failed to cleanup scoped role assignment %q", assignment.GetMetadata().GetName()))
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
|
||||
onboard = onboarding.Config{
|
||||
TokenValue: token.GetMetadata().GetName(),
|
||||
JoinMethod: types.JoinMethodBoundKeypair,
|
||||
BoundKeypair: onboarding.BoundKeypairOnboardingConfig{
|
||||
RegistrationSecretValue: token.GetStatus().GetUsage().GetBoundKeypair().GetRegistrationSecret(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// We are running unscoped.
|
||||
// Checking Terraform role
|
||||
roleName, err := c.checkIfRoleExists(ctx, client)
|
||||
if err != nil {
|
||||
switch {
|
||||
case trace.IsNotFound(err) && c.existingRole == "":
|
||||
return trace.Wrap(err, `The Terraform role %q does not exist in your Teleport cluster.
|
||||
This default role is included in Teleport clusters whose version is higher than v16.1 or v17.
|
||||
If you want to use "tctl terraform env" against an older Teleport cluster, you must create the Terraform role
|
||||
yourself and set the flag --role <your-terraform-role-name>.`, roleName)
|
||||
case trace.IsNotFound(err) && c.existingRole != "":
|
||||
return trace.Wrap(err, `The Terraform role %q specified with --role does not exist in your Teleport cluster.
|
||||
case trace.IsNotFound(err) && c.existingRole != "":
|
||||
return trace.Wrap(err, `The Terraform role %q specified with --role does not exist in your Teleport cluster.
|
||||
Please check that the role exists in the cluster.`, roleName)
|
||||
case trace.IsAccessDenied(err):
|
||||
return trace.Wrap(err, `Failed to validate if the role %q exists.
|
||||
case trace.IsAccessDenied(err):
|
||||
return trace.Wrap(err, `Failed to validate if the role %q exists.
|
||||
To use the "tctl terraform env" command you must have rights to list and read Teleport roles.
|
||||
If you got a role granted recently, you might have to run "tsh logout" and login again.`, roleName)
|
||||
default:
|
||||
return trace.Wrap(err, "Unexpected error while trying to validate if the role %q exists.", roleName)
|
||||
default:
|
||||
return trace.Wrap(err, "Unexpected error while trying to validate if the role %q exists.", roleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create temporary bot and token
|
||||
tokenName, err := c.createTransientBotAndToken(ctx, client, roleName)
|
||||
if trace.IsAccessDenied(err) {
|
||||
return trace.Wrap(err, `Failed to create the temporary Terraform bot or its token.
|
||||
// Create temporary bot and token
|
||||
tokenName, err := c.createTransientBotAndToken(ctx, client, roleName)
|
||||
if trace.IsAccessDenied(err) {
|
||||
return trace.Wrap(err, `Failed to create the temporary Terraform bot or its token.
|
||||
To use the "tctl terraform env" command you must have rights to create Teleport bot and token resources.
|
||||
If you got a role granted recently, you might have to run "tsh logout" and login again.`)
|
||||
}
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "bootstrapping bot")
|
||||
}
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "bootstrapping bot")
|
||||
}
|
||||
onboard = onboarding.Config{
|
||||
TokenValue: tokenName,
|
||||
JoinMethod: types.JoinMethodToken,
|
||||
}
|
||||
}
|
||||
|
||||
// Now run tbot
|
||||
c.showProgress("🤖 Using the temporary bot to obtain certificates")
|
||||
id, sshHostCACerts, err := c.useBotToObtainIdentity(ctx, addr, tokenName, client)
|
||||
id, sshHostCACerts, err := c.useBotToObtainIdentity(ctx, addr, onboard, client, scoped)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "The temporary bot failed to connect to Teleport.")
|
||||
}
|
||||
|
||||
envVars, err := identityToTerraformEnvVars(addr.String(), id, sshHostCACerts)
|
||||
envVars, err := identityToTerraformEnvVars(addr.String(), id, sshHostCACerts, scoped)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "exporting identity into environment variables")
|
||||
}
|
||||
@@ -274,6 +334,104 @@ func (c *TerraformCommand) createTransientBotAndToken(ctx context.Context, clien
|
||||
return tokenName, nil
|
||||
}
|
||||
|
||||
// createTransientBotAndToken creates a Bot resource and a secret Token.
|
||||
// The token is single use (secret tokens are consumed on MachineID join)
|
||||
// and the bot expires after the given TTL.
|
||||
func (c *TerraformCommand) createTransientScopedBotAndToken(ctx context.Context, client *authclient.Client, roleName scopes.QualifiedName) (*scopedjoiningv1.ScopedToken, *scopedaccessv1.ScopedRoleAssignment, error) {
|
||||
// Create token and bot name
|
||||
suffix, err := utils.CryptoRandomHex(4)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
botName := c.resourcePrefix + suffix
|
||||
c.showProgress(fmt.Sprintf("⚙️ Creating temporary bot %q and its token", botName))
|
||||
|
||||
token := scopedjoiningv1.ScopedToken_builder{
|
||||
Kind: scopedaccess.KindScopedToken,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: botName,
|
||||
Labels: terraformEnvCommandLabels,
|
||||
Expires: timestamppb.New(time.Now().Add(c.botTTL)),
|
||||
}.Build(),
|
||||
Scope: c.scope,
|
||||
Spec: scopedjoiningv1.ScopedTokenSpec_builder{
|
||||
JoinMethod: string(types.JoinMethodBoundKeypair),
|
||||
UsageMode: scopedjoining.TokenUsageModeBot,
|
||||
Bot: scopes.QualifiedName{Scope: c.scope, Name: botName}.String(),
|
||||
Roles: []string{string(types.RoleBot)},
|
||||
BoundKeypair: scopedjoiningv1.BoundKeypairSpec_builder{Onboarding: scopedjoiningv1.BoundKeypairSpec_OnboardingSpec_builder{}.Build()}.Build(),
|
||||
}.Build(),
|
||||
}.Build()
|
||||
|
||||
token, err = client.CreateScopedToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err, "creating scoped token")
|
||||
}
|
||||
|
||||
// Create bot
|
||||
bot := machineidv1pb.Bot_builder{
|
||||
Kind: types.KindBot,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: botName,
|
||||
Expires: timestamppb.New(time.Now().Add(c.botTTL)),
|
||||
Labels: terraformEnvCommandLabels,
|
||||
}.Build(),
|
||||
Scope: c.scope,
|
||||
Spec: machineidv1pb.BotSpec_builder{}.Build(),
|
||||
}.Build()
|
||||
|
||||
_, err = client.BotServiceClient().CreateBot(ctx, machineidv1pb.CreateBotRequest_builder{
|
||||
Bot: bot,
|
||||
}.Build())
|
||||
if err != nil {
|
||||
return token, nil, trace.Wrap(err, "creating bot")
|
||||
}
|
||||
|
||||
// Assign role to bot
|
||||
roleAssignment := scopedaccessv1.ScopedRoleAssignment_builder{
|
||||
Kind: scopedaccess.KindScopedRoleAssignment,
|
||||
SubKind: scopedaccess.SubKindDynamic,
|
||||
Version: types.V1,
|
||||
Metadata: headerv1.Metadata_builder{
|
||||
Name: botName,
|
||||
}.Build(),
|
||||
Scope: c.scope,
|
||||
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
||||
Assignments: []*scopedaccessv1.Assignment{
|
||||
scopedaccessv1.Assignment_builder{
|
||||
Role: roleName.String(),
|
||||
Scope: c.scope,
|
||||
}.Build(),
|
||||
},
|
||||
Bot: scopes.QualifiedName{Scope: c.scope, Name: botName}.String(),
|
||||
}.Build(),
|
||||
}.Build()
|
||||
assignmentResponse, err := client.ScopedAccessServiceClient().CreateScopedRoleAssignment(
|
||||
ctx,
|
||||
scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
||||
Assignment: roleAssignment,
|
||||
}.Build())
|
||||
if err != nil {
|
||||
return token, nil, trace.Wrap(err, "creating scoped role assignment")
|
||||
}
|
||||
|
||||
_, err = wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
|
||||
return client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx,
|
||||
scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
||||
Name: assignmentResponse.GetAssignment().GetMetadata().GetName(),
|
||||
Scope: assignmentResponse.GetAssignment().GetScope(),
|
||||
SubKind: assignmentResponse.GetAssignment().GetSubKind(),
|
||||
}.Build())
|
||||
})
|
||||
if err != nil {
|
||||
return token, nil, trace.Wrap(err, "waiting for role assignment to be created")
|
||||
}
|
||||
return token, assignmentResponse.GetAssignment(), nil
|
||||
}
|
||||
|
||||
// roleClient describes the minimal set of operations that the helper uses to
|
||||
// create the Terraform provider role.
|
||||
type roleClient interface {
|
||||
@@ -294,13 +452,40 @@ func (c *TerraformCommand) checkIfRoleExists(ctx context.Context, client roleCli
|
||||
return roleName, trace.Wrap(err)
|
||||
}
|
||||
|
||||
type scopedRoleClient interface {
|
||||
GetScopedRole(ctx context.Context, req *scopedaccessv1.GetScopedRoleRequest) (*scopedaccessv1.GetScopedRoleResponse, error)
|
||||
}
|
||||
|
||||
// createRoleIfNeeded checks if the terraform role exists.
|
||||
// Returns the Terraform role name even in case of error, so this can be used to craft nice error messages.
|
||||
func (c *TerraformCommand) checkIfScopedRoleExists(ctx context.Context, client scopedRoleClient) (scopes.QualifiedName, error) {
|
||||
roleSQN := c.existingRole
|
||||
|
||||
if roleSQN == "" {
|
||||
roleSQN = terraformScopedRoleDefault
|
||||
}
|
||||
|
||||
sqn, err := scopes.ParseQualifiedName(roleSQN)
|
||||
if err != nil {
|
||||
return scopes.QualifiedName{}, trace.Wrap(err, "parsing role SQN %q", roleSQN)
|
||||
}
|
||||
|
||||
req := scopedaccessv1.GetScopedRoleRequest_builder{
|
||||
Name: sqn.Name,
|
||||
Scope: sqn.Scope,
|
||||
}.Build()
|
||||
_, err = client.GetScopedRole(ctx, req)
|
||||
|
||||
return sqn, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// useBotToObtainIdentity takes secret bot token and runs a one-shot in-process tbot to trade the token
|
||||
// against valid certificates. Those certs are then serialized into an identity file.
|
||||
// The output is a set of environment variables, one of them including the base64-encoded identity file.
|
||||
// Later, the Terraform provider will read those environment variables to build its Teleport client.
|
||||
// Note: the function also returns the SSH Host CA cert encoded in the known host format.
|
||||
// The identity.Identity uses a different format (authorized keys).
|
||||
func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr utils.NetAddr, token string, clt *authclient.Client) (*identity.Identity, [][]byte, error) {
|
||||
func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr utils.NetAddr, onboard onboarding.Config, clt *authclient.Client, scoped bool) (*identity.Identity, [][]byte, error) {
|
||||
credential := &clientcredentials.UnstableConfig{}
|
||||
credentialLifetime := bot.CredentialLifetime{
|
||||
TTL: c.botTTL,
|
||||
@@ -318,16 +503,14 @@ func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr util
|
||||
// This does not truly disable TLS validation, only trusts the certificate on first connection.
|
||||
Insecure: clt.Config().InsecureSkipVerify,
|
||||
},
|
||||
Onboarding: onboarding.Config{
|
||||
TokenValue: token,
|
||||
JoinMethod: types.JoinMethodToken,
|
||||
},
|
||||
Onboarding: onboard,
|
||||
InternalStorage: destination.NewMemory(),
|
||||
CredentialLifetime: credentialLifetime,
|
||||
Services: []bot.ServiceBuilder{
|
||||
clientcredentials.ServiceBuilder(credential, credentialLifetime),
|
||||
},
|
||||
Logger: c.log,
|
||||
Scoped: scoped,
|
||||
}
|
||||
|
||||
// Insecure joining is not compatible with CA pinning
|
||||
@@ -390,7 +573,7 @@ func (c *TerraformCommand) showProgress(update string) {
|
||||
// identityToTerraformEnvVars takes an identity and builds environment variables
|
||||
// configuring the Terraform provider to use this identity.
|
||||
// The sshHostCACerts must be in the "known hosts" format.
|
||||
func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACerts [][]byte) (map[string]string, error) {
|
||||
func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACerts [][]byte, scoped bool) (map[string]string, error) {
|
||||
idFile := &identityfile.IdentityFile{
|
||||
PrivateKey: id.PrivateKeyBytes,
|
||||
Certs: identityfile.Certs{
|
||||
@@ -407,8 +590,14 @@ func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACer
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
idBase64 := base64.StdEncoding.EncodeToString(idBytes)
|
||||
return map[string]string{
|
||||
|
||||
envVars := map[string]string{
|
||||
constants.EnvVarTerraformAddress: addr,
|
||||
constants.EnvVarTerraformIdentityFileBase64: idBase64,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if scoped {
|
||||
envVars[constants.EnvVarTerraformScoped] = strconv.FormatBool(scoped)
|
||||
}
|
||||
return envVars, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user