diff --git a/api/gen/proto/go/teleport/workloadcluster/v1/workloadcluster.pb.go b/api/gen/proto/go/teleport/workloadcluster/v1/workloadcluster.pb.go index 632f1de67fe..11719c3d61e 100644 --- a/api/gen/proto/go/teleport/workloadcluster/v1/workloadcluster.pb.go +++ b/api/gen/proto/go/teleport/workloadcluster/v1/workloadcluster.pb.go @@ -231,7 +231,7 @@ func (x *Region) GetName() string { // Bot configures a bot for the child Teleport Cloud cluster type Bot struct { state protoimpl.MessageState `protogen:"open.v1"` - // name is the name of the bot, token, and role that will be created + // name of the bot, token, and role that will be created Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/api/proto/teleport/workloadcluster/v1/workloadcluster.proto b/api/proto/teleport/workloadcluster/v1/workloadcluster.proto index 1799fb36769..757fa94abf8 100644 --- a/api/proto/teleport/workloadcluster/v1/workloadcluster.proto +++ b/api/proto/teleport/workloadcluster/v1/workloadcluster.proto @@ -46,7 +46,7 @@ message Region { // Bot configures a bot for the child Teleport Cloud cluster message Bot { - // name is the name of the bot, token, and role that will be created + // name of the bot, token, and role that will be created string name = 1; } diff --git a/build.assets/tooling/cmd/resource-ref-generator/config.yaml b/build.assets/tooling/cmd/resource-ref-generator/config.yaml index 26cccc7e3c2..9d4f2659d27 100644 --- a/build.assets/tooling/cmd/resource-ref-generator/config.yaml +++ b/build.assets/tooling/cmd/resource-ref-generator/config.yaml @@ -133,6 +133,10 @@ resources: package: github.com/gravitational/teleport/api/types yaml_kind: node yaml_version: v2 + - type: WorkloadCluster + package: github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadcluster/v1 + yaml_kind: workload_cluster + yaml_version: v1 camel_case_exceptions: - AWS diff --git a/build.assets/tooling/cmd/resource-ref-generator/resource_examples/workload_cluster.yaml b/build.assets/tooling/cmd/resource-ref-generator/resource_examples/workload_cluster.yaml new file mode 100644 index 00000000000..a1e9e6bced2 --- /dev/null +++ b/build.assets/tooling/cmd/resource-ref-generator/resource_examples/workload_cluster.yaml @@ -0,0 +1,23 @@ +kind: workload_cluster +version: v1 +metadata: + name: company-organization +spec: + regions: + - name: us-west-2 + bot: + # Teleport Cloud creates a bot, token, and role with this name + # in the child cluster. + name: example-iam + token: + # Allow tbot to join the child cluster using the below IAM assumed role. + join_method: iam + allow: + - aws_account: "123456789012" + aws_arn: "arn:aws:sts::123456789012:assumed-role/example-tbot-role/session-name" +# Status will be set by Teleport with status from Teleport Cloud. +status: + # The Teleport Proxy address of the child cluster. + domain: company-organization.teleport.sh + # The current state of the child cluster in Teleport Cloud. + state: active diff --git a/docs/cspell.json b/docs/cspell.json index 33e32c50fc5..31b06e3d337 100644 --- a/docs/cspell.json +++ b/docs/cspell.json @@ -525,6 +525,7 @@ "enzos", "errcode", "errgroup", + "errorf", "etcdctl", "evtx", "exadata", @@ -595,6 +596,7 @@ "gworkspace", "hashfile", "hashicorp", + "headerv", "healthcheck", "healthchecks", "healthz", @@ -837,6 +839,7 @@ "pagerduty", "pagerdutyapikey", "pagerdutyapikeyfromsecret", + "panicf", "paramiko", "parquetlog", "pastable", @@ -1057,6 +1060,7 @@ "timechart", "timedatectl", "timekey", + "timeoutctx", "timesearch", "timesync", "timesyncd", @@ -1136,6 +1140,7 @@ "winscp", "winserver", "workgroups", + "workloadcluster", "wtmp", "xample", "xauth", diff --git a/docs/pages/reference/infrastructure-as-code/teleport-resources/teleport-resources.mdx b/docs/pages/reference/infrastructure-as-code/teleport-resources/teleport-resources.mdx index 9ac2b1534c4..4e2463caf2b 100644 --- a/docs/pages/reference/infrastructure-as-code/teleport-resources/teleport-resources.mdx +++ b/docs/pages/reference/infrastructure-as-code/teleport-resources/teleport-resources.mdx @@ -70,3 +70,4 @@ Here's the list of resources currently exposed via [`tctl`](../../cli/tctl.mdx): | [inference_secret](inference-secret.mdx) | Session summarization AI provider secret (API key) | | [inference_policy](inference-policy.mdx)| Matches sessions to inference models using session kind and other metadata | | [discovery_config](discovery-config.mdx)| Auto discovery configurations | +| [workload_cluster](workload-cluster.mdx) | A child Teleport Cloud cluster managed from a parent Teleport Cloud cluster. | diff --git a/docs/pages/reference/infrastructure-as-code/teleport-resources/workload-cluster.mdx b/docs/pages/reference/infrastructure-as-code/teleport-resources/workload-cluster.mdx new file mode 100644 index 00000000000..ce6b57680da --- /dev/null +++ b/docs/pages/reference/infrastructure-as-code/teleport-resources/workload-cluster.mdx @@ -0,0 +1,198 @@ +--- +title: Workload Cluster Reference +description: Provides a reference of fields within the Workload Cluster resource, which you can manage with tctl. +sidebar_label: Workload Cluster +--- +{/* vale 3rd-party-products.former-names = NO */} +{/* vale messaging.capitalization = NO */} +{/* Automatically generated from: gen/proto/go/teleport/workloadcluster/v1/workloadcluster.pb.go */} +{/* DO NOT EDIT */} + +**Kind**: `workload_cluster`
+**Version**: `v1` + +Represents a child Teleport Cloud cluster + +Example definition: +```yaml +kind: workload_cluster +version: v1 +metadata: + name: company-organization +spec: + regions: + - name: us-west-2 + bot: + # Teleport Cloud creates a bot, token, and role with this name + # in the child cluster. + name: example-iam + token: + # Allow tbot to join the child cluster using the below IAM assumed role. + join_method: iam + allow: + - aws_account: "123456789012" + aws_arn: "arn:aws:sts::123456789012:assumed-role/example-tbot-role/session-name" +# Status will be set by Teleport with status from Teleport Cloud. +status: + # The Teleport Proxy address of the child cluster. + domain: company-organization.teleport.sh + # The current state of the child cluster in Teleport Cloud. + state: active + +``` + +## Top-level fields + +Example: + +```yaml +kind: "string" +sub_kind: "string" +version: "string" +metadata: # [...] +spec: # [...] +status: # [...] +``` +|Field Name|Description|Type| +|---|---|---| +|kind||string| +|metadata||[Metadata](#metadata)| +|spec||[Workload Cluster Spec](#workload-cluster-spec)| +|status||[Workload Cluster Status](#workload-cluster-status)| +|sub_kind||string| +|version||string| + +## Allow + +Configures rules for using the corresponding join method + + +Example: + +```yaml +aws_account: "string" +aws_arn: "string" +``` + +|Field Name|Description|Type| +|---|---|---| +|aws_account||string| +|aws_arn||string| + +## Bot + +Configures a bot for the child Teleport Cloud cluster + + +Example: + +```yaml +name: "string" +``` + +|Field Name|Description|Type| +|---|---|---| +|name|Name of the bot, token, and role that will be created|string| + +## Metadata + +Resource metadata. + + +Example: + +```yaml +name: "string" +namespace: "string" +description: "string" +labels: + "string": "string" + "string": "string" + "string": "string" +expires: # See description +revision: "string" +``` + +|Field Name|Description|Type| +|---|---|---| +|description|Description is object description.|string| +|expires|Expires is a global expiry time header can be set on any resource in the system.|| +|labels|Labels is a set of labels.|map[string]string| +|name|Name is an object name.|string| +|namespace|Namespace is object namespace. The field should be called "namespace" when it returns in Teleport 2.4.|string| +|revision|Revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.|string| + +## Region + +Configures a region for deploying Teleport services + + +Example: + +```yaml +name: "string" +``` + +|Field Name|Description|Type| +|---|---|---| +|name|Name is the region's name|string| + +## Token + +Configures a token for a bot + + +Example: + +```yaml +join_method: "string" +allow: + - # [...] + - # [...] + - # [...] +``` + +|Field Name|Description|Type| +|---|---|---| +|allow|Allow defines the AWS Accounts and ARNs to allow joining from|[][Allow](#allow)| +|join_method|Join_method is type of join method to allow for the token|string| + +## Workload Cluster Spec + +Configures where a child Teleport Cloud cluster should be created and the initial bot + + +Example: + +```yaml +regions: + - # [...] + - # [...] + - # [...] +bot: # [...] +token: # [...] +``` + +|Field Name|Description|Type| +|---|---|---| +|bot||[Bot](#bot)| +|regions||[][Region](#region)| +|token||[Token](#token)| + +## Workload Cluster Status + +Populated with status from Teleport Cloud about the child Teleport Cloud cluster + + +Example: + +```yaml +state: "string" +domain: "string" +``` + +|Field Name|Description|Type| +|---|---|---| +|domain|Domain is the Teleport Proxy address of the child Teleport Cloud cluster|string| +|state|State is the status of the child cluster running in Teleport Cloud|string| + diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/workload_cluster.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/workload_cluster.mdx index b03b08887cf..a1605399081 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/workload_cluster.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/workload_cluster.mdx @@ -57,7 +57,7 @@ Optional: Optional: -- `name` (String) name is the name of the bot, token, and role that will be created +- `name` (String) name of the bot, token, and role that will be created ### Nested Schema for `spec.regions` diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/workload_cluster.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/workload_cluster.mdx index b4cbaa2f942..37aa649afe1 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/workload_cluster.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/workload_cluster.mdx @@ -88,7 +88,7 @@ Optional: Optional: -- `name` (String) name is the name of the bot, token, and role that will be created +- `name` (String) name of the bot, token, and role that will be created ### Nested Schema for `spec.regions` diff --git a/docs/pages/zero-trust-access/management/workload-clusters.mdx b/docs/pages/zero-trust-access/management/workload-clusters.mdx new file mode 100644 index 00000000000..99369d5b8b2 --- /dev/null +++ b/docs/pages/zero-trust-access/management/workload-clusters.mdx @@ -0,0 +1,342 @@ +--- +title: Workload Clusters +sidebar_label: Workload Clusters +description: How to create and manage Teleport Cloud workload clusters using tctl and Machine & Workload Identity +tags: + - how-to + - mwi + - infrastructure-identity +--- + +`workload_cluster` resources can be used to automatically provision and +deprovision Teleport Cloud clusters. When a workload cluster is created, +Teleport Cloud also creates a Bot, token, and role in the provisioned Teleport +Cloud cluster so that automation can create, read, and update users and roles +in that cluster. + +This guide covers how to: + +- Create a `workload_cluster` resource in a parent Teleport Cloud cluster. +- Wait for the child Teleport Cloud cluster to become active. +- Configure `tbot` to join the child Teleport Cloud cluster. +- Use the identity generated by `tbot` to create, read, and update users and + roles in the child Teleport Cloud cluster with `tctl`. + +## How it works + +Creating a `workload_cluster` resource in a parent Teleport Cloud cluster tells +Teleport Cloud to provision a child Teleport Cloud cluster. + +A child Teleport Cloud cluster is a separate Teleport Cloud cluster whose +lifecycle is managed from a parent Teleport Cloud cluster. It is not the same +thing as a trusted cluster. + +Changes to the parent Teleport Cloud cluster do not affect the child Teleport +Cloud cluster. The parent Teleport Cloud cluster can only provision and +deprovision child Teleport Cloud clusters. + +When the child Teleport Cloud cluster is provisioned, Teleport Cloud also +creates: + +- A Bot in the child Teleport Cloud cluster +- A join token for that Bot +- A role for that Bot + +That Bot can then join the child Teleport Cloud cluster using IAM joining via +`tbot`. The identity generated by `tbot` can be used to create, read, and +update users and roles in the child Teleport Cloud cluster. + +## Prerequisites + +This guide uses AWS IAM joining for the child Teleport Cloud cluster Bot because +`workload_cluster` currently supports IAM joining only. Learn more +about IAM joining in the +[join methods reference](../../reference/deployment/join-methods.mdx#aws-iam-role-iam). + +- A Teleport Cloud cluster to use as the parent cluster. + + +Teleport Cloud customers must contact support at +support@goteleport.com to enable +the workload cluster feature. + + +- `tctl` and `tbot` installed. For installation and deployment guidance, see + the [Machine & Workload Identity deployment guides](../../machine-workload-identity/deployment/deployment.mdx). +- An AWS identity that matches the IAM join token rules configured for the + child Teleport Cloud cluster Bot. + +## Step 1/5. Configure RBAC in the parent cluster + +To create and delete `workload_cluster` resources, the Teleport user in the +parent cluster needs permission to manage that resource type. + +If the user already has access to manage `workload_cluster` resources, skip +this step. + +Create a file called `workload-cluster-editor.yaml`: + + ```yaml + kind: role + version: v8 + metadata: + name: workload-cluster-editor + spec: + allow: + rules: + - resources: + - workload_cluster + verbs: + - create + - delete + - list + - read + - update + ``` + +Use `tctl` to create the role: + + ```code + $ tctl create -f workload-cluster-editor.yaml + ``` + +Assign the role to a user in the parent cluster. Replace `alice` with the +Teleport username and include any roles that user already needs: + + ```code + $ tctl users update --set-roles editor,workload-cluster-editor alice + ``` + +## Step 2/5. Create a workload cluster + +Create a file called `workload-cluster.yaml`: + + ```yaml + kind: workload_cluster + version: v1 + metadata: + name: company-organization + spec: + regions: + - name: us-west-2 + bot: + name: example-iam + token: + join_method: iam + allow: + - aws_account: "123456789012" + aws_arn: "arn:aws:sts::123456789012:assumed-role/example-tbot-role/session-name" + ``` + +Replace: + +- `company-organization` with the name of the Teleport Cloud cluster that Teleport Cloud + should create. +- `us-west-2` with the Teleport Cloud region for the child Teleport Cloud + cluster. See [Teleport Cloud architecture](../../reference/architecture/teleport-cloud-architecture.mdx#auth-service) + for available auth regions. +- `example-iam` with the Bot and token name that `tbot` will use. +- `123456789012` and `arn:aws:sts::123456789012:assumed-role/example-tbot-role/session-name` + with values that match the AWS identity from which `tbot` will join. + +Use `tctl` to create the resource in the parent cluster: + + ```code + $ tctl --auth-server parent.teleport.sh:443 create -f workload-cluster.yaml + ``` + + +After creation, workload cluster configuration cannot be modified. + + +The Teleport Cloud cluster domain is derived from the resource name. In this +example, the child Teleport Cloud cluster will use the domain +`company-organization.teleport.sh`. + +## Step 3/5. Wait for the child Teleport Cloud cluster to become active + +Retrieve the resource until the `status.state` field is `active`: + + ```code + $ tctl --auth-server parent.teleport.sh:443 get workload_cluster/company-organization + ``` + +Example output: + + ```yaml + kind: workload_cluster + version: v1 + metadata: + name: company-organization + spec: + regions: + - name: us-west-2 + bot: + name: example-iam + token: + join_method: iam + allow: + - aws_account: "123456789012" + aws_arn: "arn:aws:sts::123456789012:assumed-role/example-tbot-role/session-name" + status: + domain: company-organization.teleport.sh + state: active + ``` + +Once the resource is active, use the `status.domain` value when connecting +`tbot` and `tctl` to the child Teleport Cloud cluster. + +## Step 4/5. Use `tbot` to join the child Teleport Cloud cluster + +Create a file called `tbot.yaml`: + + ```yaml + version: v2 + oneshot: true + proxy_server: company-organization.teleport.sh:443 + onboarding: + join_method: iam + token: example-iam + storage: + type: memory + services: + - type: identity + destination: + type: directory + path: ./tbot-output + ``` + +Replace: + +- `company-organization.teleport.sh:443` with the child Teleport Cloud cluster's proxy + address from + `status.domain`. +- `example-iam` with the Bot name from `spec.bot.name`. + +Before running `tbot`, authenticate to AWS using an identity that satisfies the +token's IAM join rules. + +Then run `tbot`: + + ```code + $ tbot start -c ./tbot.yaml + ``` + +This writes an identity file to `./tbot-output/identity`. Use that +identity with `tctl` to create, read, and update users and roles in the child +Teleport Cloud cluster. + +## Step 5/5. Manage users and roles in the child Teleport Cloud cluster + +Create a role definition for the child Teleport Cloud cluster in +`child-role.yaml`: + + ```yaml + kind: role + version: v8 + metadata: + name: child-auditor + spec: + allow: + rules: + - resources: + - role + - user + verbs: + - list + - read + ``` + +Create the role in the child Teleport Cloud cluster using the identity +produced by `tbot`: + + ```code + $ tctl --auth-server company-organization.teleport.sh:443 -i ./tbot-output/identity create -f child-role.yaml + ``` + +Now create a user in the child Teleport Cloud cluster and assign the new role: + + ```code + $ tctl --auth-server company-organization.teleport.sh:443 -i ./tbot-output/identity users add --roles child-auditor example-user + ``` + +The same identity can also inspect roles in the child Teleport Cloud cluster: + + ```code + $ tctl --auth-server company-organization.teleport.sh:443 -i ./tbot-output/identity get role/child-auditor + ``` + +## Delete the workload cluster + +When the child Teleport Cloud cluster is no longer needed, remove the +`workload_cluster` resource from the parent cluster: + + ```code + $ tctl --auth-server parent.teleport.sh:443 rm workload_cluster/company-organization + ``` + +After the `workload_cluster` resource is deleted, Teleport Cloud keeps the +child Teleport Cloud cluster in a 14-day grace period. During that period, +the cluster name cannot be reused. Contact support at +support@goteleport.com to restore +access to the cluster or to request an expedited deletion. + +## Use the Go SDK instead of `tctl` + +Teleport's Go SDK can be used to automate this workflow instead of relying on +client tools such as `tctl`. + +For general Teleport API guidance, see +[Using the Teleport API](../api/api.mdx) and +[Getting Started with the Teleport API](../api/getting-started.mdx). + +Teleport's repository includes an example using the Go SDK that does the following: + +- Creates a Teleport API client connected to the parent Teleport Cloud cluster. +- Creates a `workload_cluster` resource in the parent Teleport Cloud cluster. +- Waits for the `workload_cluster` resource to report being `active`. +- Uses the `tbot` binary to retrieve an identity file for the child Teleport + Cloud cluster. +- Creates a role and user in the child Teleport Cloud cluster. +- Creates an invite link for the new user. +- Deletes the `workload_cluster` resource in the parent Teleport Cloud cluster. + +Please follow the below steps to run the workload cluster Go example. + +1. Log in to the parent Teleport Cloud cluster (replace `parent.teleport.sh` with the Teleport Cloud cluster's address): + + ```code + $ tsh login --proxy parent.teleport.sh + ``` + +2. Clone the Teleport repository: + + ```code + $ git clone --depth=1 https://github.com/gravitational/teleport + ``` + +3. Navigate to the workload clusters example: + + ```code + $ cd ./teleport/examples/workload-clusters/ + ``` + +4. Replace `parent.teleport.sh`, `company-organization`, `account`, and `arn` +with the parent Teleport Cloud cluster address, desired workload cluster name, a valid AWS account ID, and +a valid AWS ARN that `tbot` can use for IAM joining in `main.go`. + +5. Authenticate to AWS using the identity referenced in the +`workload_cluster` resource, then run the Go program: + + ```code + $ go run ./ + ``` + +## Next steps + +- Read the [Workload Cluster resource reference](../../reference/infrastructure-as-code/teleport-resources/workload-cluster.mdx) + for the complete schema. +- Read the [Machine & Workload Identity configuration reference](../../reference/machine-workload-identity/configuration.mdx) + for more `tbot` options. +- Review the [role resource reference](../../reference/infrastructure-as-code/teleport-resources/role.mdx) + to define more precise RBAC in parent and child Teleport Cloud clusters. diff --git a/examples/workload-clusters/go.mod b/examples/workload-clusters/go.mod new file mode 100644 index 00000000000..ca69add6096 --- /dev/null +++ b/examples/workload-clusters/go.mod @@ -0,0 +1,48 @@ +module workload-clusters + +go 1.25.9 + +require github.com/gravitational/teleport/api v0.0.0-20260428152919-24b2608bf3f5 + +require ( + github.com/beevik/etree v1.6.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charlievieth/strcase v0.0.5 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-piv/piv-go/v2 v2.5.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gravitational/trace v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/russellhaering/gosaml2 v0.11.0 // indirect + github.com/russellhaering/goxmldsig v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/examples/workload-clusters/go.sum b/examples/workload-clusters/go.sum new file mode 100644 index 00000000000..9353c511f76 --- /dev/null +++ b/examples/workload-clusters/go.sum @@ -0,0 +1,144 @@ +github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= +github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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-piv/piv-go/v2 v2.5.0 h1:w4KZ3GytEGZt8zm+S7olcIHZk0giL23xVqCa2HgwuqA= +github.com/go-piv/piv-go/v2 v2.5.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gravitational/teleport/api v0.0.0-20260428152919-24b2608bf3f5 h1:RRp06ZBvr/dVI3p3l+6hnToZhQdHzGA8uG4R+1qwr50= +github.com/gravitational/teleport/api v0.0.0-20260428152919-24b2608bf3f5/go.mod h1:wC4lfXzs0yzFoKu2PJ6E6EDQr+nV3uJ3MZ6KhHG8yMk= +github.com/gravitational/trace v1.5.3 h1:qP/BP7xFJBr83D5ctTClNiJUVQKlg6Q4qVPviK7sMfo= +github.com/gravitational/trace v1.5.3/go.mod h1:/uCbC3ukVU8Pdrh8+3vNLyyE1aGheBWpGGrMnwIK80E= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russellhaering/gosaml2 v0.11.0 h1:wlWm7dWMrpJBzh0xEOZof70nVen4f/2BEF8ZXaidJ9o= +github.com/russellhaering/gosaml2 v0.11.0/go.mod h1:GmL5LeCP7PBYzSkkFxtmHuRzC2eUZ/6JSLYQd5fzKK4= +github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= +github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/workload-clusters/main.go b/examples/workload-clusters/main.go new file mode 100644 index 00000000000..ee30abf2e1d --- /dev/null +++ b/examples/workload-clusters/main.go @@ -0,0 +1,367 @@ +/* +Copyright 2026 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 main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/api/client/proto" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + workloadclusterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadcluster/v1" + "github.com/gravitational/teleport/api/types" +) + +var ( + // awsAccount is the AWS account to allow tbot to use for joining + awsAccount = "account" + // awsARN is the AWS ARN to allow tbot to use for joining + awsARN = "arn" + // parentClusterProxyAddress is the parent Teleport Cloud cluster's proxy address + parentClusterProxyAddress = "parent.teleport.sh" + // workloadClusterName is the desired name for the new child Teleport Cloud cluster + workloadClusterName = "company-organization" +) + +// TbotConfig defines a configuration for running tbot. +type TbotConfig struct { + // Version is the configuration version. + Version string `json:"version"` + // Oneshot determines if tbot runs as a service. + Oneshot bool `json:"oneshot"` + // ProxyServer is the Teleport Proxy to run tbot against. + ProxyServer string `json:"proxy_server"` + // Onboarding defines how tbot should attempt to join the Teleport cluster. + Onboarding Onboarding `json:"onboarding"` + // Storage instructs tbot where to save its internal certificates. + Storage Storage `json:"storage"` + // Services defines which services for tbot to run. + Services []Service `json:"services"` +} + +// Onboarding defines how tbot should attempt to join the Teleport cluster. +type Onboarding struct { + // JoinMethod is how to join, such as iam. + JoinMethod string `json:"join_method"` + // Token is which token in the Teleport cluster to use. + Token string `json:"token"` +} + +// Storage instructs tbot where to save its internal certificates. +type Storage struct { + // Type is the storage type, such as "memory" for in-memory storage. + Type string `json:"type"` +} + +// Service defines which services for tbot to run. +type Service struct { + // Type is the service type, such as "identity". + Type string `json:"type"` + // Destination is used by the identity service to save retrieved identity files and certs. + Destination Destination `json:"destination"` +} + +// Destination is used by the identity service to save retrieved identity files and certs. +type Destination struct { + // Type is the type of storage, such as "path". + Type string `json:"type"` + // Path is the filepath to use. + Path string `json:"path"` +} + +func main() { + if err := run(); err != nil { + log.Fatalf("Failed running workload_cluster lifecycle: %v", err) + } +} + +func run() (err error) { + ctx := context.Background() + + /********************************************** + * Create a Teleport workload_cluster resource * + **********************************************/ + + // parentClient is a Teleport client connected to the parent cluster, which + // assumes the identity used has access for creating, reading, and deleting + // workload_cluster resources. + parentClient, err := client.New(ctx, client.Config{ + Addrs: []string{ + parentClusterProxyAddress, + // Note: port is optional. + }, + Credentials: []client.Credentials{ + // this loads the credential from tsh + client.LoadProfile("", ""), + }, + }) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + defer parentClient.Close() + + // wc defines a workload_cluster to create in us-west-2 with a bot named + // example-iam and configuration for a token to use IAM joining. + // The created child Teleport Cloud cluster will have a bot, role, and token + // each named example-iam. The bot will have access to create, read, and + // update users and roles. + wc := &workloadclusterv1.WorkloadCluster{ + Kind: types.KindWorkloadCluster, + Version: "v1", + Metadata: &headerv1.Metadata{ + Name: workloadClusterName, + }, + Spec: &workloadclusterv1.WorkloadClusterSpec{ + Regions: []*workloadclusterv1.Region{ + { + Name: "us-west-2", + }, + }, + Bot: &workloadclusterv1.Bot{ + Name: "example-iam", + }, + Token: &workloadclusterv1.Token{ + JoinMethod: "iam", + Allow: []*workloadclusterv1.Allow{ + { + AwsAccount: awsAccount, + AwsArn: awsARN, + }, + }, + }, + }, + } + + // Create a workload_cluster resource within the parent Teleport Cloud cluster. + if _, err := parentClient.CreateWorkloadCluster(ctx, wc); err != nil { + return fmt.Errorf("failed to create workload cluster: %w", err) + } + + defer func() { + /**************************** + * Delete a workload cluster * + ****************************/ + + // Delete the workload cluster if any error is encountered or if whole lifecycle + // completes successfully. + + // Clean up the previously created workload_cluster resource in the parent + // Teleport Cloud cluster. + if deleteErr := parentClient.DeleteWorkloadCluster(ctx, wc.Metadata.Name); deleteErr != nil { + err = errors.Join(err, fmt.Errorf("error deleting workload cluster: %w", deleteErr)) + } + }() + + // Wait for the created workload cluster to reach an active state. + timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + wc, err = waitForActiveWorkloadCluster(timeoutCtx, parentClient, wc.Metadata.Name, 30*time.Second) + if err != nil { + return fmt.Errorf("failed waiting for workload cluster to be active: %w", err) + } + + /************************************************ + * Run tbot against child Teleport Cloud cluster * + ************************************************/ + + // Create a directory that will be used for tbot's configuration and saving a + // retrieved identity file for interacting with the child Teleport Cloud cluster. + tbotDir, err := os.MkdirTemp("", "") + if err != nil { + return fmt.Errorf("error creating directory for tbot: %w", err) + } + defer func() { + if err := os.RemoveAll(tbotDir); err != nil { + log.Printf("Error removing tbot directory %s: %v", tbotDir, err) + } + }() + + // Create the tbot configuration. + tbotConfig := TbotConfig{ + // Version must be v2. + Version: "v2", + // Oneshot should be true to avoid running tbot as a daemon. + Oneshot: true, + // ProxyServer should be the Proxy Server including the port 443 for the + // new child Teleport Cloud cluster. + ProxyServer: fmt.Sprintf("%s:443", wc.Status.Domain), + Onboarding: Onboarding{ + // Only iam join method will be supported in the short term for workload_clusters. + JoinMethod: "iam", + // Token must match the same name provided in the workload_cluster's + // Spec.Bot.Name. + Token: "example-iam", + }, + Storage: Storage{ + // Configure tbot to use in-memory storage. + Type: "memory", + }, + Services: []Service{ + { + Type: "identity", + Destination: Destination{ + Type: "directory", + // A file named identity will be created in the provided path. + // This identity file may be provided to tctl or Teleport clients + // for interacting with a Teleport cluster. + Path: tbotDir, + }, + }, + }, + } + + // Write the tbot configuration to a `tbot.json` file. + tbotConfigContent, err := json.Marshal(tbotConfig) + if err != nil { + return fmt.Errorf("error marshalling tbot configuration: %w", err) + } + tbotConfigPath := filepath.Join(tbotDir, "tbot.json") + if err := os.WriteFile(tbotConfigPath, tbotConfigContent, 0600); err != nil { + return fmt.Errorf("error writing tbot configuration: %w", err) + } + + // Run the tbot binary. Teleport does not expose programmatic access to + // tbot, so the binary must be used. + // Once tbot start has successfully completed then an identity file + // will be populated at the provided path in the tbot configuration. + var bufErr bytes.Buffer + tbotCmd := exec.Command("tbot", "start", "-c", tbotConfigPath) + tbotCmd.Stderr = &bufErr + if err := tbotCmd.Run(); err != nil { + return fmt.Errorf("error running tbot: %w\n\n%s", err, bufErr.String()) + } + + /************************************************************* + * Manage roles and users in the child Teleport Cloud cluster * + *************************************************************/ + + // Create a new Teleport client to interact with the child Teleport Cloud cluster. + // This client will use the identify file retrieved by tbot. + childClient, err := client.New(ctx, client.Config{ + Addrs: []string{ + // This is the child Teleport Cloud cluster's proxy address: + wc.Status.Domain, + // Note: port is optional. + }, + Credentials: []client.Credentials{ + // This uses an identity file instead of using a credential from tsh. + client.LoadIdentityFile(filepath.Join(tbotDir, "identity")), + }, + }) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + defer childClient.Close() + + // The following section includes examples of creating and deleting + // a role and a user. + + // Create a new role named example in the child Teleport Cloud cluster. + newRole := types.RoleV6{ + Metadata: types.Metadata{ + Name: "example", + }, + Spec: types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + { + Resources: []string{ + "workload_cluster", + }, + Verbs: []string{ + "read", + }, + }, + }, + }, + }, + } + if _, err := childClient.CreateRole(ctx, &newRole); err != nil { + return fmt.Errorf("error creating role: %w", err) + } + + // Create user named "example" that has the new "example" role assigned. + newUser := types.UserV2{ + Metadata: types.Metadata{ + Name: "example", + }, + Spec: types.UserSpecV2{ + Roles: []string{ + "example", + }, + }, + } + if _, err := childClient.CreateUser(ctx, &newUser); err != nil { + return fmt.Errorf("error creating user: %w", err) + } + + // create an invite URL for user to activate account and setup MFA + resetPasswordToken := proto.CreateResetPasswordTokenRequest{ + Name: newUser.Metadata.Name, + TTL: proto.Duration(2 * time.Hour), + Type: "invite", + } + resetToken, err := childClient.CreateResetPasswordToken(ctx, &resetPasswordToken) + if err != nil { + return fmt.Errorf("error creating reset token: %w", err) + } + + ttl := resetToken.Expiry().Sub(time.Now().UTC()) + fmt.Printf("User %q has been created but requires a password. Share this URL with the user to complete user setup, link is valid for %v:\n%v\n\n", newUser.Metadata.Name, ttl, resetToken.GetURL()) + + /**************************** + * Delete a workload cluster * + ****************************/ + // Deferred function above will execute and delete the workload_cluster resource in the parent + // Teleport Cloud cluster. + + return nil +} + +func waitForActiveWorkloadCluster(ctx context.Context, client *client.Client, workloadClusterName string, pollingInterval time.Duration) (*workloadclusterv1.WorkloadCluster, error) { + ticker := time.NewTicker(pollingInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + wc, err := client.GetWorkloadCluster(ctx, workloadClusterName) + if err != nil { + return nil, fmt.Errorf("error getting workload cluster: %w", err) + } + + if wc.Status == nil { + continue + } + + if wc.Status.State == "active" { + return wc, nil + } + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} diff --git a/integrations/terraform/tfschema/workloadcluster/v1/workloadcluster_terraform.go b/integrations/terraform/tfschema/workloadcluster/v1/workloadcluster_terraform.go index b0ef25cff75..28661381d5a 100644 --- a/integrations/terraform/tfschema/workloadcluster/v1/workloadcluster_terraform.go +++ b/integrations/terraform/tfschema/workloadcluster/v1/workloadcluster_terraform.go @@ -104,7 +104,7 @@ func GenSchemaWorkloadCluster(ctx context.Context) (github_com_hashicorp_terrafo Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ "bot": { Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"name": { - Description: "name is the name of the bot, token, and role that will be created", + Description: "name of the bot, token, and role that will be created", Optional: true, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, }}),