Managing Scoped Resources using IaC
In this guide, you will run Infrastructure-As-Code (IaC) tools in scoped mode and use them to reconcile scoped resources.
See the Scoped RBAC page for more details about scopes.
Scopes are an actively developed feature of Teleport. Not every Teleport feature is supported within a scope yet, and breaking changes may still be introduced. Scoped resources and credentials created with one Teleport version may not work with another.
When operating in a scoped session, commands and APIs that have not yet been
updated to understand scopes will commonly fail with a scoped identities not supported error. This is expected for features outside the currently
supported set listed below.
How it works
Scopes are a hierarchical organization system for Teleport resources and
permissions. A scope is a path-like attribute (for example /staging/west or
/prod) attached to resources and permission grants. Permissions assigned at
a scope apply to resources within that scope and all of its descendant
scopes, but cannot reach across to orthogonal scopes or up to ancestor
scopes.
In this guide, you will grant access to the /test scope to the IaC tools.
The tool will be able to manage the content of the /test scope, but will
not be allowed to create resources in other scopes such as /example.
The Teleport Kubernetes operator is currently restricted to its own scope. It cannot write resources in nested scopes of its own.
For example: An operator in the /test scope cannot manage resources in the /test/sub scope.
It can still grant roles in nested scopes by specifying nested scopes as scoped_role_assignment targets.
Prerequisites
-
A running Teleport cluster accessible at a hostname with a valid TLS certificate. If you want to get started with Teleport, sign up for a free trial or set up a demo environment.
-
The
tctlandtshclients.Installing
tctlandtshclients-
Determine the version of your Teleport cluster. The
tctlandtshclients must be at most one major version behind your Teleport cluster version. Send a GET request to the Proxy Service at/v1/webapi/findand use a JSON query tool to obtain your cluster version. Replace teleport.example.com:443 with the web address of your Teleport Proxy Service:- Mac/Linux
- Windows - Powershell
TELEPORT_DOMAIN=teleport.example.com:443TELEPORT_VERSION="$(curl -s https://$TELEPORT_DOMAIN/v1/webapi/find | jq -r '.server_version')"$TELEPORT_DOMAIN = "teleport.example.com:443"$TELEPORT_VERSION = (Invoke-RestMethod -Uri "https://${TELEPORT_DOMAIN}/v1/webapi/find").server_version -
Follow the instructions for your platform to install
tctlandtshclients:- Mac
- Windows - Powershell
- Linux
Download the signed macOS .pkg installer for Teleport, which includes the
tctlandtshclients:curl -O https://cdn.teleport.dev/teleport-${TELEPORT_VERSION?}.pkgIn Finder double-click the
pkgfile to begin installation.dangerUsing Homebrew to install Teleport is not supported. The Teleport package in Homebrew is not maintained by Teleport and we can't guarantee its reliability or security.
curl.exe -O https://cdn.teleport.dev/teleport-v$TELEPORT_VERSION-windows-amd64-bin.zipUnzip the archive and move the `tctl` and `tsh` clients to your %PATH%
NOTE: Do not place the `tctl` and `tsh` clients in the System32 directory, as this can cause issues when using WinSCP.
Use %SystemRoot% (C:\Windows) or %USERPROFILE% (C:\Users\<username>) instead.
All of the Teleport binaries in Linux installations include the
tctlandtshclients. For more options (including RPM/DEB packages and downloads for i386/ARM/ARM64) see our installation page.curl -O https://cdn.teleport.dev/teleport-v${TELEPORT_VERSION?}-linux-amd64-bin.tar.gztar -xzf teleport-v${TELEPORT_VERSION?}-linux-amd64-bin.tar.gzcd teleportsudo ./installTeleport binaries have been copied to /usr/local/bin
Connecting with TLS routing disabled
This guide's commands assume your Teleport cluster uses TLS routing (
proxy_listener_mode: multiplex), where thetctlandtshclients reach every Teleport service through the Proxy Service's web address on port443. If you're not sure whether this applies to your cluster, check with whoever manages it.If your cluster uses separate listener ports instead, adjust ports as follows:
-
tshcommands (e.g.,tsh login --proxy=...): continue using the Proxy Service web address on port3080(or443if behind a load balancer). Do not change these to port3025. -
Direct
tctlor Auth Service API commands: use port3025for the Auth Service gRPC listener:tctl status --auth-server=teleport.example.com:3025
-
- Check that you can connect to your Teleport cluster and verify that you can run
tctlandtshcommands using your current credentials.-
Assign teleport.example.com to the domain name of the Teleport Proxy Service in your cluster and email@example.com to your Teleport username.
-
Authenticate to your Teleport cluster. This depends on whether your shell is interactive or not.
In an interactive shell: Run the following command. By default, this triggers a multi-factor authentication prompt:
tsh login --proxy=teleport.example.com --user=email@example.comtctl statusCluster teleport.example.com
Version 19.0.0-dev
CA pin sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678
On non-interactive environments: If you are running
tshandtctlas an AI agent, in a CI/CD environment, or similar, make sure theTELEPORT_IDENTITY_FILEenvironment variable is assigned to a valid file path with credentials for your cluster.tshandtctlread the file path from the environment variable and do not require a separate authentication step. If there is no identity file available, we recommend that you set up Machine ID to provision one automatically.When executing
tctlcommands with an identity file, you must pass the--auth-serverflag to provide the Teleport Auth Service address, which is not included in the identity file. If you provide the Proxy Service address,tctlconnects to the Proxy Service, which forwards traffic to and from the Teleport Auth Service. Update 443 to3025if you are contacting the Auth Service directly withtctl:tctl status --auth-server=teleport.example.com:443For
tshcommands that read an identity file, you must pass the--proxyflag, which pointstshto the address of the Teleport Proxy Service:tsh status --proxy=teleport.example.comEnsure client commands can access your identity file. Replace path/to/identity/file with the path to your identity file:
export TELEPORT_IDENTITY_FILE="${TELEPORT_IDENTITY_FILE:-path/to/identity/file}"Add the
--auth-serveror--proxyflags to all subsequenttctlandtshcommands.
tctl statuscommand, you can use your current credentials to run subsequenttctlcommands from your workstation. If you host your own Teleport cluster, you can also runtctlcommands on the computer that hosts the Teleport Auth Service for full permissions. -
- All Teleport instances (Auth Service, Proxy Service, and Agents) must be
running the same Teleport version, and must have the
TELEPORT_UNSTABLE_SCOPES=yesenvironment variable set. - The Auth Service must also have the
TELEPORT_UNSTABLE_AGENT_SCOPE_PIN=yesenvironment variable set before you use scoped tokens to join Agent roles other than SSH Services (node) and Bots. This includes scoped Kubernetes Services and Application Services.
- tctl
- Kubernetes Operator
- Terraform
Step 1/4. Grant scoped access to IaC tool
In this step, you will create the required Scoped RBAC and other Teleport resources so that the IaC tool can connect
to Teleport and managed scoped resources in the /test scope.
- tctl
- Kubernetes Operator
- Terraform
The user already has access to the / scope as per the prerequisites.
Retrieve the Kubernetes cluster OIDC URL:
kubectl get --raw /.well-known/openid-configuration | jq -r ".issuer"
https://oidc.example.com/
Write the scoped-operator.yaml manifests:
# Create the operator role (for the whole cluster)
version: v1
kind: scoped_role
metadata:
name: operator
scope: "/"
spec:
assignable_scopes: ["/**"]
rules:
- resources: ["scoped_role", "scoped_role_assignment", "bot", "access_list"]
verbs:
- list
- create
- read
- update
- delete
- resources: ["scoped_token"]
verbs:
- list
- create
- read
- update
- delete
- secrets
---
# Create the operator bot (for the `/test` scope).
version: v1
kind: bot
metadata:
name: operator
scope: "/test"
spec: {}
---
# Allow the `/test::operator` bot to manage the `/test` scope
kind: scoped_role_assignment
metadata:
name: operator
scope: /test
spec:
assignments:
- role: /::operator
scope: /test
bot: /test::operator
sub_kind: dynamic
version: v1
---
# Allow the operator pod in Kubernetes to join as the `/test::operator` bot.
version: v1
kind: scoped_token
metadata:
name: operator
scope: /test
spec:
bot: /test::operator
join_method: kubernetes
kubernetes:
allow:
- service_account_name: teleport-operator
service_account_namespace: teleport-iac
oidc:
# To get the issuer, run `kubectl get --raw /.well-known/openid-configuration | jq .issuer`
issuer: "https://oidc.example.com/"
type: oidc
roles:
- Bot
usage_mode: bot
version: v1
Apply the manifests:
tctl create -f scoped-operator.yaml
scoped_role "/::operator" has been upsertedBot "operator" has been createdscoped_role_assignment "/test::operator" has been upsertedscoped_token "operator" has been updated
Create the terraform-provider.yaml manifest to create the role used by the Terraform provider:
version: v1
kind: scoped_role
metadata:
name: terraform-provider
scope: "/"
spec:
assignable_scopes: ["/**"]
rules:
- resources: ["scoped_role", "scoped_role_assignment", "bot", "access_list"]
verbs:
- list
- create
- read
- update
- delete
- resources: ["scoped_token"]
verbs:
- list
- create
- read
- update
- delete
- secrets
Apply the manifest:
tctl create -f terraform-provider.yaml
scoped_role "/::terraform-provider" has been upserted
Step 2/4. Deploy the IaC tool
In this step, you will configure and deploy the IaC tool if applicable.
- tctl
- Kubernetes Operator
- Terraform
Validate that tctl works by running:
tctl status
Then, validate that you have permissions to list scoped resources:
tctl get scoped_role --format=texttctl get scoped_role_assignment --format=text
The commands should not return permission errors.
Recover the cluster name and Proxy Service address:
tsh status --format=json | jq '{"url": .active.profile_url, "name": .active.cluster}'
{ "url": "https://teleport.example.com:443", "name": "teleport.example.com"}
Configure the operator by writing the following values.yaml:
teleportAddress: teleport.example.com:443
teleportClusterName: teleport.example.com
token: /test::operator
scope: /test
serviceAccount:
name: teleport-operator
ownerEmail: your-name@example.com
Deploy the operator:
helm repo add teleport "https://charts.releases.teleport.dev"helm repo update
Hang tight while we grab the latest from your chart repositories......Successfully got an update from the "teleport" chart repositoryUpdate Complete. ⎈Happy Helming!⎈helm upgrade --install \ teleport-operator teleport/teleport-operator \ --namespace teleport-iac --create-namespace \ -f values.yaml \ --version 19.0.0-dev
Release "teleport-operator" does not exist. Installing it now.NAME: teleport-operatorLAST DEPLOYED: Mon Aug 3 18:00:35 2026NAMESPACE: teleport-iacSTATUS: deployedREVISION: 1DESCRIPTION: Install complete
Validate that the operator is healthy:
kubectl get pods -n teleport-iac
NAME READY STATUS RESTARTS AGEteleport-operator-75855944c-s1kk5 1/1 Running 0 22s
Run the following command to be able to use Terraform locally for an hour:
eval $(tctl terraform env --scope /test)
🔑 Detecting if MFA is required⚙️ Creating temporary bot "/test::tctl-terraform-env-3ba9a76f" and its token🤖 Using the temporary bot to obtain certificates🚀 Certificates obtained, you can now use Terraform in this terminal for 1h0m0s
Recover the cluster name and Proxy Service address:
tsh status --format=json | jq '{"url": .active.profile_url}'
{ "url": "https://teleport.example.com:443",}
Write the following teleport.tf file configuring the Teleport provider and connection to the cluster:
terraform {
required_providers {
teleport = {
source = "terraform.releases.teleport.dev/gravitational/teleport"
version = "13.3.7"
}
}
}
provider "teleport" {
addr = "teleport.example.com:443"
}
Finally, initialize the Terraform plugins:
terraform init
Initializing the backend...
Initializing provider plugins...- Finding terraform.releases.teleport.dev/gravitational/teleport versions matching "13.3.7"...- Installing terraform.releases.teleport.dev/gravitational/teleport v13.3.7...- Installed terraform.releases.teleport.dev/gravitational/teleport v13.3.7 (self-signed, key ID C87ED53A6282C411)
Terraform has been successfully initialized!
Step 3/4. Create the scoped resource using IaC
In this step, you will create scoped resources in the /test scope, allowing a /test::server-enroller fictive service
to create scoped tokens in /test/team-a and /test/team-b scopes.
You will configure 4 resources using IaC:
- a
scoped_role/test::token-creatorthat allows token creation and can be granted in/test/**nested scopes - a
bot/test::server-enrollerthat will be used by the fictive service to create tokens and enroll servers - a
scoped_role_assignment/test::server-enrollerthat grants the scoped role/test::token-creatorto the bot inside the/test/team-aand/test/team-b - a
scoped_token/test::server-enrollerthat allows the bot to join from a Kubernetes cluster
- tctl
- Kubernetes Operator
- Terraform
Create the following scoped-resources.yaml manifest:
kind: scoped_role
version: v1
metadata:
name: "token-creator"
scope: "/test"
spec:
assignable_scopes: ["/test/**"]
rules:
- resources: ["scoped_token"]
verbs : ["read", "list", "create", "update", "delete"]
---
kind: bot
version: v1
metadata:
name: "server-enroller"
scope: "/test"
spec: {}
---
kind: scoped_role_assignment
sub_kind: dynamic
version: v1
metadata:
name: "server-enroller"
scope: "/test"
spec:
assignments:
- role: "/test::token-creator"
scope: "/test/team-a"
- role: "/test::token-creator"
scope: "/test/team-b"
bot: "/test::server-enroller"
---
kind: scoped_token
version: v1
metadata:
name: "server-enroller"
scope: "/test"
spec:
bot: "/test::server-enroller"
join_method: "kubernetes"
kubernetes:
allow:
- service_account_name: "pod-sa"
service_account_namespace: "default"
oidc:
# fake OIDC issuer, where the bot would run
issuer: "https://kubernetes.example.com/cluster-a"
type: "oidc"
roles: ["Bot"]
usage_mode: "bot"
Apply the manifests using tctl:
tctl create -f scoped-resources.yaml
scoped_role "/test::token-creator" has been upsertedBot "server-enroller" has been createdscoped_role_assignment "/test::server-enroller" has been upsertedscoped_token "server-enroller" has been updated
Create the following scoped-custom-resources.yaml manifests:
kind: TeleportScopedRoleV1
apiVersion: "resources.teleport.dev/v1"
metadata:
name: "token-creator"
scope: "/test"
spec:
assignable_scopes: ["/test/**"]
rules:
- resources: ["scoped_token"]
verbs : ["read", "list", "create", "update", "delete"]
---
kind: TeleportBotV1
apiVersion: "resources.teleport.dev/v1"
metadata:
name: "server-enroller"
scope: "/test"
spec: {}
---
kind: TeleportScopedRoleAssignmentV1
apiVersion: "resources.teleport.dev/v1"
metadata:
name: "server-enroller"
scope: "/test"
spec:
assignments:
- role: "/test::token-creator"
scope: "/test/team-a"
- role: "/test::token-creator"
scope: "/test/team-b"
bot: "/test::server-enroller"
---
kind: TeleportScopedTokenV1
apiVersion: "resources.teleport.dev/v1"
metadata:
name: "server-enroller"
scope: "/test"
spec:
bot: "/test::server-enroller"
join_method: "kubernetes"
kubernetes:
allow:
- service_account_name: "pod-sa"
service_account_namespace: "default"
oidc:
# fake OIDC issuer, where the bot would run
issuer: "https://kubernetes.example.com/cluster-a"
type: "oidc"
roles: ["Bot"]
usage_mode: "bot"
Apply the manifests using kubectl in the operator namespace:
kubectl create -f scoped-custom-resources.yaml -n teleport-iac
teleportscopedrolev1.resources.teleport.dev/token-creator createdteleportbotv1.resources.teleport.dev/server-enroller createdteleportscopedroleassignmentv1.resources.teleport.dev/server-enroller createdteleportscopedtokenv1.resources.teleport.dev/server-enroller created
Create the following scoped-resources.tf file:
locals {
scope = "/test"
}
resource "teleport_scoped_role" "token-creator" {
version = "v1"
metadata = {
name = "token-creator"
description = "Role that can create tokens in select ${local.scope})/** scopes."
}
scope = local.scope
spec = {
assignable_scopes = ["${local.scope}/**"]
rules = [{
resources = ["scoped_token"]
verbs = ["read", "list", "create", "update", "delete"]
}]
}
}
resource "teleport_bot" "server-enroller" {
version = "v1"
metadata = {
name = "server-enroller"
}
scope = local.scope
spec = {}
}
resource "teleport_scoped_role_assignment" "server-enroller" {
version = "v1"
metadata = {
name = "server-enroller"
}
sub_kind = "dynamic"
scope = local.scope
spec = {
assignments = [
{role = "${local.scope}::token-creator", scope = "${local.scope}/team-a"},
{role = "${local.scope}::token-creator", scope = "${local.scope}/team-b"},
]
bot = "${local.scope}::server-enroller"
}
}
resource "teleport_scoped_token" "server-enroller" {
version = "v1"
metadata = {
name = "server-enroller"
}
scope = local.scope
spec = {
bot = "${local.scope}::server-enroller"
join_method = "kubernetes"
kubernetes = {
allow = [
{service_account_name = "pod-sa", service_account_namespace = "default"}
]
oidc = {
// fake OIDC issuer, where the bot would run
issuer = "https://kubernetes.example.com/cluster-a"
}
type = "oidc"
}
roles = ["Bot"]
usage_mode = "bot"
}
}
Plan the changes using Terraform:
terraform plan
Terraform will perform the following actions:
# teleport_bot.server-enroller will be created + resource "teleport_bot" "server-enroller" { ... }
# teleport_scoped_role.token-creator will be created + resource "teleport_scoped_role" "token-creator" { ... }
# teleport_scoped_role_assignment.server-enroller will be created + resource "teleport_scoped_role_assignment" "server-enroller" { ... }
# teleport_scoped_token.server-enroller will be created + resource "teleport_scoped_token" "server-enroller" { ... }
Plan: 4 to add, 0 to change, 0 to destroy.
Finally, apply the changes:
terraform apply -auto-approve
Plan: 4 to add, 0 to change, 0 to destroy.teleport_scoped_role_assignment.server-enroller: Creating...teleport_bot.server-enroller: Creating...teleport_scoped_role.token-creator: Creating...teleport_scoped_token.server-enroller: Creating...teleport_bot.server-enroller: Creation complete after 0s [id=/test::server-enroller]teleport_scoped_token.server-enroller: Creation complete after 0s [id=/test::server-enroller]teleport_scoped_role.token-creator: Creation complete after 1s [id=/test::token-creator]teleport_scoped_role_assignment.server-enroller: Creation complete after 1s [id=/test::server-enroller]
Step 4/4. Validate resources were created
At this point, the scoped resources should have been created in Teleport. Validate that they were by running:
tctl get scoped_role --format=text
ID------------------------------------/::operator/::terraform-provider/test::token-creatortctl get scoped_role_assignment --format=text
SubKind ID Assignee Assigns------- ------------------------ ----------------------------- --------------------------------------------------------------------------dynamic /test::operator bot: /test::operator /::operator -> /testdynamic /test::server-enroller bot: /test::server-enroller /test::token-creator -> /test/team-a, /test::token-creator -> /test/team-btctl get scoped_token --format=text
Token Type Assigns Scope Labels Expiry Time (UTC)---------------------- ---- -------------- ----------------------------------------------------------- ---------------------------------/test::operator Bot never/test::server-enroller Bot resources.teleport.dev/owner-email=your-name@example.com... nevertctl get bot --format=text
Name Roles------------------------ -----/test::operator/test::server-enroller