diff --git a/docs/pages/admin-guides/management/security/revoking-access.mdx b/docs/pages/admin-guides/management/security/revoking-access.mdx
index 42472b7802693..91de6f5dd9506 100644
--- a/docs/pages/admin-guides/management/security/revoking-access.mdx
+++ b/docs/pages/admin-guides/management/security/revoking-access.mdx
@@ -24,9 +24,11 @@ how to execute the procedure.
Teleport locks allow you to permanently or temporarily revoke access to a number
of different "targets". Supported lock targets include: specific users, roles,
-servers, desktops, or MFA devices. After you create a lock, all existing
-sessions where the lock applies are terminated and new sessions are rejected
-while the lock remains in force.
+servers, desktops, or MFA devices. For Machine & Workload Identity bots, this
+additionally includes the join token name (for delegated join methods) and the
+bot instance UUID. After you create a lock, all existing sessions where the loc
+applies are terminated and new sessions are rejected while the lock remains in
+force.
For more information, read our
[Session and Identity Locking Guide](../../../identity-governance/locking.mdx).
diff --git a/docs/pages/identity-governance/locking.mdx b/docs/pages/identity-governance/locking.mdx
index 434ab6b774449..f2eed1318ab63 100644
--- a/docs/pages/identity-governance/locking.mdx
+++ b/docs/pages/identity-governance/locking.mdx
@@ -22,6 +22,8 @@ A lock can target the following objects or attributes:
cluster)
- a Windows desktop by the desktop's name
- an [Access Request](access-requests/access-requests.mdx) by UUID
+- a bot instance ID (for Machine & Workload Identity bots)
+- a join token name (for Machine & Workload Identity bots using a [delegated join method](../reference/join-methods.mdx#delegated-join-methods))
## Prerequisites
@@ -83,6 +85,29 @@ with one of the following options:
# Created a lock with name "dc7cee9d-fe5e-4534-a90d-db770f0234a1".
```
+
+ The most appropriate locking target for a Machine & Workload Identity bot
+ depends on its join method.
+
+ For [delegated join methods](../reference/join-methods.mdx#secret-vs-delegated),
+ it's best to target the specific join token the bot is using to join:
+ ```code
+ $ tctl lock --join-token=example-token-name
+ ```
+
+ The join token name cannot be targeted for bots joined using the `token` join
+ method, so it's best to use the
+ [bot instance ID](../reference/architecture/machine-id-architecture.mdx#bot-instances):
+ ```code
+ $ tctl lock --bot-instance-id aabbccdd-1234-5678-0000-3b04d7d03acc
+ ```
+
+ In all cases, you may also target the bot user, which will lock all instances
+ of a bot that share the same underlying user:
+ ```code
+ $ tctl lock --user bot-example
+ ```
+
@@ -209,7 +234,7 @@ auth_service:
Restart or redeploy the Auth Service for the change to take effect.
-If not, edit your cluster authentication preference resource:
+If not, edit your cluster authentication preference resource:
```code
$ tctl edit cap
diff --git a/docs/pages/includes/provision-token/bound-keypair-spec.mdx b/docs/pages/includes/provision-token/bound-keypair-spec.mdx
new file mode 100644
index 0000000000000..2e4d38b383d54
--- /dev/null
+++ b/docs/pages/includes/provision-token/bound-keypair-spec.mdx
@@ -0,0 +1,65 @@
+```yaml
+kind: token
+version: v2
+metadata:
+ name: example-token
+spec:
+ roles: [Bot]
+ join_method: bound_keypair
+ bot_name: example
+
+ # Fields related to the bound keypair joining process.
+ bound_keypair:
+ # Fields related to the initial join attempt.
+ onboarding:
+ # If set to a public key in SSH authorized_keys format, the
+ # joining client must have the corresponding private key to join. This
+ # keypair may be created using `tbot keypair create`. If set,
+ # `registration_secret` and `must_register_before` are ignored.
+ initial_public_key: ""
+
+ # If set to a secret string value, a client may use this secret to perform
+ # the first join without pre-registering a public key in
+ # `initial_public_key`. If unset and no `initial_public_key` is provided,
+ # a random value will be generated automatically into
+ # `.status.bound_keypair.registration_secret`.
+ registration_secret: ""
+
+ # If set to an RFC 3339 timestamp, attempts to register via
+ # `registration_secret` will be denied once the timestamp has elapsed. If
+ # more time is needed, this field can be edited to extend the registration
+ # period.
+ must_register_before: ""
+
+ # Fields related to recovery after certificates have expired.
+ recovery:
+ # The maximum number of allowed recovery attempts. This value may
+ # be raised or lowered after creation to allow additional recovery
+ # attempts should the initial limit be exhausted. If `mode` is set to
+ # `standard`, recovery attempts will only be allowed if
+ # `.status.bound_keypair.recovery_count` is less than this limit. This
+ # limit is not enforced if `mode` is set to `relaxed` or `insecure`. This
+ # value must be at least 1 to allow for the initial join during
+ # onboarding, which counts as a recovery.
+ limit: 1
+
+ # The recovery rule enforcement mode. Valid values:
+ # - standard (or unset): all configured rules enforced. The recovery limit
+ # and client join state are required and verified. This is the most
+ # secure recovery mode.
+ # - relaxed: recovery limit is not enforced, but client join state is
+ # still required. This effectively allows unlimited recovery attempts,
+ # but client join state still helps mitigate stolen credentials.
+ # - insecure: neither the recovery limit nor client join state are
+ # enforced. This allows any client with the private key to join freely.
+ # This is less secure, but can be useful in certain situations, like in
+ # otherwise unsupported CI/CD providers. This mode should be used with
+ # care, and RBAC rules should be configured to heavily restrict which
+ # resources this identity can access.
+ mode: "standard"
+
+ # If set to an RFC 3339 timestamp, once elapsed, a keypair rotation will be
+ # forced on next join if it has not already been rotated. The most recent
+ # rotation is recorded in `.status.bound_keypair.last_rotated_at`.
+ rotate_after: ""
+```
\ No newline at end of file
diff --git a/docs/pages/machine-workload-identity/machine-id/deployment/bound-keypair.mdx b/docs/pages/machine-workload-identity/machine-id/deployment/bound-keypair.mdx
new file mode 100644
index 0000000000000..fa012d0cf61e3
--- /dev/null
+++ b/docs/pages/machine-workload-identity/machine-id/deployment/bound-keypair.mdx
@@ -0,0 +1,196 @@
+---
+title: Deploying Machine ID with Bound Keypair Joining
+description: "How to install and configure Machine ID with Bound Keypair Joining"
+---
+
+In this guide, you will install Machine & Workload Identity's agent, `tbot`, on
+an arbitrary host using Bound Keypair Joining. This host could be a bare-metal
+machine, a VM, a container, or any other host - the only requirement is that the
+host has persistent storage.
+
+Bound Keypair Joining is an improved alternative to
+[secret-based join methods][secret] and can function as a drop-in replacement.
+It is more secure than static token joining, and is more flexible than ephemeral
+token joining with renewable certificates: when its certificates expire, it can
+perform an automated recovery to ensure the bot can rejoin even after an
+extended outage.
+
+Note that platform-specific join methods may be available that are better suited
+to your environment; refer to the [deployment guides](./deployment.mdx) for a
+full list of options.
+
+## How it works
+
+With Bound Keypair Joining, Machine & Workload Identity bots generate a unique
+keypair which is persistently stored in their internal data directory. Teleport
+is then configured to trust this public key for future joining attempts.
+
+Later, when the bot attempts to join the cluster, Teleport issues it a challenge
+that can only be completed using its private key. The bot returns the solved
+challenge, attesting to its own identity, and is conditionally allowed to join
+the cluster. This process is repeated for every join attempt, but if the bot has
+been offline long enough for its certificates to expire, it is additionally
+forced to perform an automatic recovery to join again.
+
+As self attestation is inherently less secure than the external verification
+that would be provided by a cloud provider like AWS or a dedicated TPM, Bound
+Keypair Joining enforces a number of additional checks to prevent abuse,
+including:
+- Join state verification to ensure the keypair cannot be usefully shared or
+ duplicated
+- Certificate generation counter checks to ensure regular bot certificates
+ cannot be usefully shared or duplicated
+- Configurable limits on how often - if at all - bots may be allowed to
+ automatically recover using this keypair
+
+An important benefit to Bound Keypair Joining is that all joining restrictions
+can be reconfigured at any time, and bots that expire or go offline can be
+recovered by making a server-side exemption without any client-side
+intervention.
+
+Refer to the [admin guide][guide] for further details on how this join method
+works.
+
+## Prerequisites
+
+{/* note: consider edition-prereqs-tabs.mdx include for v19; it is misleading due to the minor launch release */}
+
+- A running Teleport cluster version 18.1.0 or above.
+- The `tsh` and `tctl` clients.
+- (!docs/pages/includes/tctl.mdx!)
+- This guide assumes the bot host has mutable persistent storage for internal
+ bot data. While it is possible to use Bound Keypair Joining can on immutable
+ hosts (like CI runs), doing so will reduce security guarantees; see the
+ [admin guide][guide] for further information.
+
+## Step 1/5. Install `tbot`
+
+**This step is completed on the bot host.**
+
+First, `tbot` needs to be installed on the host that you wish to use Machine ID
+on.
+
+Download and install the appropriate Teleport package for your platform:
+
+(!docs/pages/includes/install-linux.mdx!)
+
+## Step 2/5. Create a Bot
+
+**This step is completed on your local machine.**
+
+(!docs/pages/includes/machine-id/create-a-bot.mdx!)
+
+## Step 3/5. Create a join token
+
+**This step is completed on your local machine.**
+
+In this guide, we'll demonstrate joining a bot using a registration secret: this
+is a one-time use secret the bot can provide to Teleport to authenticate its
+first join. Once authenticated, the bot automatically generates a keypair and
+registers its public key with Teleport for use in all future join attempts.
+
+Create `token-example.yaml`:
+
+```yaml
+kind: token
+version: v2
+metadata:
+ # This name will be used in tbot's `onboarding.token` field.
+ name: example
+spec:
+ roles: [Bot]
+ # bot_name should match the name of the bot created earlier in this guide.
+ bot_name: example
+ join_method: bound_keypair
+ bound_keypair:
+ recovery:
+ mode: standard
+ limit: 1
+```
+
+Replace `example` in `spec.bot_name` with the name of the bot you created in the
+second step.
+
+For this example, we don't need to set any additional options for the bound
+keypair token. We've allowed a single recovery attempt, which will be used to
+allow the bot's initial join, and Teleport will generate a registration secret
+automatically when the token is created as we have not preregistered a public
+key to use.
+
+
+This example makesĀ use of registration secrets to authenticate the initial join.
+If desired, it is also possible to generate a key on the bot host first and
+register it with Teleport out-of-band, avoiding the need to copy secrets between
+hosts.
+
+To learn more about preregistering public keys and Bound Keypair Joining's other
+onboarding and recovery options, refer to the
+[Reference and Admin Guide][guide].
+
+
+Use `tctl` to apply this file:
+
+```code
+$ tctl create -f token-example.yaml
+```
+
+Next, retrieve the generated registration secret, which will be needed for the
+next step:
+```code
+$ tctl get token/example --format=json | jq -r '.[0].status.bound_keypair.registration_secret'
+```
+
+This assumes `jq` is installed. If not, run `tctl get token/example` and inspect
+the `.status.bound_keypair.registration_secret` field.
+
+## Step 4/5. Configure `tbot`
+
+**This step is completed on the bot host.**
+
+Create `/etc/tbot.yaml`:
+
+```yaml
+version: v2
+proxy_server: example.teleport.sh:443
+onboarding:
+ join_method: bound_keypair
+ token: example
+ bound_keypair:
+ registration_secret: SECRET
+storage:
+ type: directory
+ path: /var/lib/teleport/bot
+# outputs will be filled in during the completion of an access guide.
+outputs: []
+```
+
+Replace the following:
+- `example.teleport.sh:443` with the address of your Teleport Proxy.
+- `example` with the name of the token created in the previous step, if you
+ changed it from `example`.
+- `SECRET` with the registration secret retrieved in the previous step.
+
+(!docs/pages/includes/machine-id/daemon-or-oneshot.mdx!)
+
+## Step 5/5. Configure outputs
+
+(!docs/pages/includes/machine-id/configure-outputs.mdx!)
+
+## Next steps
+
+- Read the [Bound Keypair Joining Reference andĀ Admin Guide][guide]
+ for more details about the join method and the available configuration options.
+- Follow the [access guides](../access-guides/access-guides.mdx) to finish configuring `tbot` for
+ your environment.
+- Read the [configuration reference](../../../reference/machine-id/configuration.mdx) to explore
+ all the available configuration options.
+- [More information about `TELEPORT_ANONYMOUS_TELEMETRY`.](../../../reference/machine-id/telemetry.mdx)
+
+{/*
+TODO: guide link above is a placeholder, link to the real guide once merged in
+follow-up PR.
+[guide]: ../../../reference/machine-id/bound-keypair.mdx
+*/}
+
+[secret]: ../../../reference/join-methods.mdx#secret-vs-delegated
+[guide]: ../../../reference/machine-id/machine-id.mdx
diff --git a/docs/pages/machine-workload-identity/machine-id/deployment/deployment.mdx b/docs/pages/machine-workload-identity/machine-id/deployment/deployment.mdx
index 96ea2de48f299..007456b504327 100644
--- a/docs/pages/machine-workload-identity/machine-id/deployment/deployment.mdx
+++ b/docs/pages/machine-workload-identity/machine-id/deployment/deployment.mdx
@@ -52,14 +52,15 @@ and [Architecture](../../../reference/architecture/machine-id-architecture.mdx)
Read the following guides for how to deploy Machine ID on your cloud platform or
on-prem infrastructure.
-| Platform | Installation method | Join method |
-|-------------------------------------------|-------------------------------------------------|-----------------------------------------------------|
-| [Linux](linux.mdx) | Package manager or TAR archive | Static join token |
-| [Linux (TPM)](linux-tpm.mdx) | Package manager or TAR archive | Attestation from TPM 2.0 |
-| [GCP](gcp.mdx) | Package manager, TAR archive, or Kubernetes pod | Identity document signed by GCP |
-| [AWS](aws.mdx) | Package manager, TAR archive, or Kubernetes pod | Identity document signed by AWS |
-| [Azure](azure.mdx) | Package manager or TAR archive | Identity document signed by Azure |
-| [Kubernetes](kubernetes.mdx) | Kubernetes pod | Identity document signed by your Kubernetes cluster |
+| Platform | Installation method | Join method |
+|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|
+| [Linux](linux.mdx) | Package manager or TAR archive | Static join token |
+| [Linux (TPM)](linux-tpm.mdx) | Package manager or TAR archive | Attestation from TPM 2.0 |
+| [Linux (Bound Keypair)](bound-keypair.mdx) | Package manager or TAR archive | Bound Keypair |
+| [GCP](gcp.mdx) | Package manager, TAR archive, or Kubernetes pod | Identity document signed by GCP |
+| [AWS](aws.mdx) | Package manager, TAR archive, or Kubernetes pod | Identity document signed by AWS |
+| [Azure](azure.mdx) | Package manager or TAR archive | Identity document signed by Azure |
+| [Kubernetes](kubernetes.mdx) | Kubernetes pod | Identity document signed by your Kubernetes cluster |
### CI/CD
diff --git a/docs/pages/reference/cli/tbot.mdx b/docs/pages/reference/cli/tbot.mdx
index 6b74a335e9d81..362727ec41b0c 100644
--- a/docs/pages/reference/cli/tbot.mdx
+++ b/docs/pages/reference/cli/tbot.mdx
@@ -269,21 +269,22 @@ These flags are available to all `tbot start` commands. Note that
`tbot start legacy` supports slightly different options, so refer to its
specific section for details when using a YAML config file or legacy output.
-| Flag | Description |
-|----------------------|-------------|
-| `-d/--debug` | Enable verbose logging to stderr. |
-| `--[no-]fips` | Whether to run tbot in FIPS compliance mode. This requires the FIPS `tbot` binary. |
-| `--log-format` | Controls the format of output logs. Can be `json` or `text`. Defaults to `text`. |
-| `-a/--auth-server` | Address of the Teleport Auth Service. Prefer using `--proxy-server` where possible. |
-| `--proxy-server` | Address of the Teleport Proxy Server. |
-| `--token` | A bot join token or path to file with token value, if attempting to onboard a new bot; used on first connect. |
-| `--ca-pin` | CA pin to validate the Teleport Auth Service; used on first connect. |
-| `--certificate-ttl` | TTL of short-lived machine certificates. |
-| `--renewal-interval` | Interval at which short-lived certificates are renewed; must be less than the certificate TTL. |
-| `--join-method` | Method to use to join the cluster. One of: `azure`, `circleci`, `gcp`, `github`, `gitlab`, `iam`, `kubernetes`, `spacelift`, `token`, `tpm`, `terraform_cloud` |
-| `--[no-]oneshot` | If set, quit after the first renewal. |
-| `--diag-addr` | If set and the bot is in debug mode, a diagnostics service will listen on specified address. |
-| `--storage` | A destination URI for tbot's internal storage, e.g. `file:///foo/bar`. See [Destination URIs](#destination-uris) for more info. |
+| Flag | Description |
+|-------------------------|-------------|
+| `-d/--debug` | Enable verbose logging to stderr. |
+| `--[no-]fips` | Whether to run tbot in FIPS compliance mode. This requires the FIPS `tbot` binary. |
+| `--log-format` | Controls the format of output logs. Can be `json` or `text`. Defaults to `text`. |
+| `-a/--auth-server` | Address of the Teleport Auth Service. Prefer using `--proxy-server` where possible. |
+| `--proxy-server` | Address of the Teleport Proxy Service. |
+| `--token` | A bot join token or path to file with token value, if attempting to onboard a new bot; used on first connect. |
+| `--ca-pin` | CA pin to validate the Teleport Auth Service; used on first connect. |
+| `--certificate-ttl` | TTL of short-lived machine certificates. |
+| `--renewal-interval` | Interval at which short-lived certificates are renewed; must be less than the certificate TTL. |
+| `--join-method` | Method to use to join the cluster. One of: `azure`, `circleci`, `gcp`, `github`, `gitlab`, `iam`, `kubernetes`, `spacelift`, `token`, `tpm`, `terraform_cloud` |
+| `--[no-]oneshot` | If set, quit after the first renewal. |
+| `--diag-addr` | If set and the bot is in debug mode, a diagnostics service will listen on specified address. |
+| `--storage` | A destination URI for tbot's internal storage, e.g. `file:///foo/bar`. See [Destination URIs](#destination-uris) for more info. |
+| `--registration-secret` | An optional joining secret to use on first join with the `bound_keypair` join method. |
## tbot start legacy
@@ -710,6 +711,53 @@ $ tbot install systemd \
--write
```
+## tbot keypair create
+
+Generates a keypair for use with `bound_keypair` joining and stores it in the
+specified bot internal storage directory. If a key already exists in the storage
+directory, no new key will be generated and the existing public key will be
+printed to the console; use the `--overwrite` flag to force the generation of a
+new keypair.
+
+### Flags
+
+| Flag | Description |
+|------|-------------|
+| `--storage` | A destination URI to be used for bot internal storage. Required. |
+| `--proxy-server` | A Teleport Proxy Service address. Required. |
+| `--overwrite` | If set, always generate a new key. If unset, the existing public key will be printed if one already exists in the destination specified by `--storage` |
+
+### Examples
+
+First, ensure the desired internal storage directory exists:
+```code
+$ mkdir -p /var/lib/teleport/bot
+```
+
+Next, generate a keypair:
+```code
+$ tbot keypair create --proxy-server example.teleport.sh:443 --storage /var/lib/teleport/bot
+2025-07-09T00:00:00.000-00:00 INFO [TBOT] keypair has been written to storage storage:directory: /var/lib/teleport/bot tbot/keypair.go:135
+
+To register the keypair with Teleport, include this public key in the token's
+`spec.bound_keypair.onboarding.initial_public_key`:
+
+ ssh-ed25519
+```
+
+This public key, including the algorithm identifier (`ssh-ed25519`, but may vary
+depending on your cluster configuration) can then be copied into a Bound Keypair
+join token to be used as a preregistered key.
+
+{/*
+TODO: Replace with a link into the admin guide once the follow up PR has merged.
+[preregistered key](../machine-id/bound-keypair.mdx#preregistered-key-example).
+*/}
+
+Note that the Teleport Proxy Service address is required to fetch the currently
+enabled [signature suite](../signature-algorithms.mdx). No authentication takes
+place at this time.
+
## Destination URIs
Many `tbot start` subcommands accept destination URIs via the `--storage` and
diff --git a/docs/pages/reference/join-methods.mdx b/docs/pages/reference/join-methods.mdx
index e9e9df0d349d2..f66dd9b0ea0b1 100644
--- a/docs/pages/reference/join-methods.mdx
+++ b/docs/pages/reference/join-methods.mdx
@@ -117,6 +117,7 @@ Delegated join methods are:
- [`azure`](#azure-managed-identity-azure)
- [`azure_devops`](#azure-devops-azure_devops)
- [`bitbucket`](#bitbucket-pipelines-bitbucket)
+- [`bound_keypair`](#bound-keypair-bound_keypair)
- [`circleci`](#circleci-circleci)
- [`ec2`](#aws-ec2-identity-document-ec2)
- [`gcp`](#gcp-service-account-gcp)
@@ -143,6 +144,7 @@ Renewable join-methods are:
- [ephemeral `token`](#ephemeral-tokens)
- [static `token`](#static-tokens)
- [`ec2`](#aws-ec2-identity-document-ec2)
+- [`bound_keypair`](#bound-keypair-bound_keypair)
Nodes with non-renewable certificates must join again in order to get a new
certificate before expiry. The instance will have to prove again that it is legitimate.
@@ -257,13 +259,41 @@ Or as Teleport resources:
(!docs/pages/includes/provision-token/ephemeral-spec.mdx!)
-When a MachineID bot uses an ephemeral join token, the token is deleted.
+When a Machine ID bot uses an ephemeral join token, the token is deleted.
+
+
+New Machine & Workload Identity bot deployments should consider upgrading to the
+[`bound_keypair` join method](#bound-keypair-bound_keypair).
+
- How to [Join Services with a Secure Token](../enroll-resources/agents/join-token.mdx).
- [Deploying Machine ID on Linux](../machine-workload-identity/machine-id/deployment/linux.mdx)
+### Bound Keypair: `bound_keypair`
+
+Bound Keypair tokens are an alternative to
+[secret-based join methods](#secret-based-join-methods) that improve security
+and flexibility. They are best used on platforms with persistent storage, but
+can be configured for use in any environment.
+
+This join method is recommended for on-prem environments
+[without TPMs](#trusted-platform-module-tpm) or cloud platforms
+without a specialized [delegated join method](#delegated-join-methods).
+
+(!docs/pages/includes/provision-token/bound-keypair-spec.mdx!)
+
+
+- [Deploying Machine ID with Bound Keypair joining](../machine-workload-identity/machine-id/deployment/bound-keypair.mdx)
+
+{/*
+TODO: Uncomment after follow-up PR with admin guide has merged.
+- [Bound Keypair Reference and Admin Guide](./machine-id/bound-keypair.mdx)
+*/}
+
+
+
### AWS IAM role: `iam`
The IAM join method is available to any Teleport process running anywhere with access to IAM credentials,
diff --git a/docs/pages/reference/machine-id/configuration.mdx b/docs/pages/reference/machine-id/configuration.mdx
index 20349a7e599a7..3fc743005c5b4 100644
--- a/docs/pages/reference/machine-id/configuration.mdx
+++ b/docs/pages/reference/machine-id/configuration.mdx
@@ -121,6 +121,11 @@ onboarding:
# multiple Teleport clusters from a single GitLab CI job.
token_env_var_name: "MY_GITLAB_ID_TOKEN"
+ # bound_keypair holds parameters specific to the "bound_keypair" join method
+ bound_keypair:
+ # registration_secret is an optional secret to use on first join in lieu of
+ # a preregistered keypair.
+ registration_secret: "secret"
# storage specifies the destination that `tbot` should use to store its
# internal state. This state is sensitive, and you should ensure that the
@@ -216,7 +221,7 @@ output used in context.
# always be `identity`.
type: identity
# ssh_config controls whether the identity output will attempt to generate an
-# OpenSSH configuration file. This requires that `tbot` can connect to the
+# OpenSSH configuration file. This requires that `tbot` can connect to the
# Teleport Proxy Service. Must be "on" or "off". If unspecified, this defaults to
# "on".
ssh_config: on