Deploying tbot with any OIDC provider via Generic OIDC
While Teleport natively supports many cloud platforms and identity providers, it is still often necessary to join bots using identity providers that do not have a dedicated join method. Generic OIDC allows you to join bots from any platform or provider where workloads are issued OIDC compatible JWTs, and allows you to define custom rules to allow only the intended workloads to authenticate to Teleport.
In this guide, you will create a Machine & Workload Identity Bot and configure Teleport to allow it to join from a hypothetical provider that does not have a Teleport join method, and without using any long-lived credentials.
How it works
Instead of using shared secrets, the generic_oidc join method allows Teleport
Agents and Bots to authenticate to the Teleport Auth Service using a JWT issued
by any OIDC-compatible provider.
Many CI/CD platforms and cloud providers act as OIDC providers and can issue
short-lived JWTs to workloads that serve as proof of the workload's identity.
In fact, this is how many of Teleport's join methods work today, including
the github, kubernetes (in oidc mode), and gcp join methods, among
others.
The generic_oidc join method extends this support to any provider that can
issue compatible JWTs, allowing you to freely specify any compatible provider
and a set of free-form joining rules to select exactly which workloads are
allowed to join, without requiring a dedicated join method for the provider.
Be aware that due to the nature of generic_oidc, this guide is meaningfully
less directed than other deployment guides. The configuration shown here is only
useful as an example or reference, and you will need to thoroughly customize
and verify any configuration made before deploying it into a production
environment.
Be aware that not all OIDC providers are equally secure. When using
generic_oidc, you are responsible for both properly vetting the provider and
writing secure rules that will not allow unintended clients to join.
Where possible, one of Teleport's dedicated join
methods should be used instead of
generic_oidc, which benefit from dedicated documentation and vetted,
predefined rules.
Additionally, due to its generic nature, this document cannot fully guide you in
setting up the tbot client to run seamlessly on arbitrary platforms.
Teleport's dedicated join methods have guides and supporting documentation
tailored specifically for use with those platforms; with generic_oidc you will
need to determine the best deployment method yourself.
Prerequisites
-
A running Teleport (v18.11.0 or higher) 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 protected] 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 protected]tctl statusCluster teleport.example.com
Version 18.10.0
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. -
- Your user should have the privileges to create token resources.
- You must already have an OIDC-compliant identity provider that can issue JWTs to your bot environment
Security considerations and limitations
Before getting started with generic_oidc, it is important to consider the
potential security impacts associated with trusting arbitrary identity
providers, as not all providers are created equal.
When Teleport adds support for a new identity provider, care is taken to ensure they meet minimum security requirements and that joining rules ensure a minimum bar for security. This includes:
- Ensuring the provider is actually OIDC compliant and issues sane credentials to workloads
- Ensuring those issued credentials contain useful identifying claims, and that one or more of those claims are required
When using generic_oidc, you should ensure any providers whose tokens you
decide to trust provide at least the above properties.
As a hypothetical example, consider Google Cloud Platform. Note that Teleport
has a dedicated
gcp join method
which should be used instead of generic_oidc, but it makes for a useful
example. If you naively issue a token using the gcloud tool it contains very
few useful claims:
gcloud auth print-identity-token --audiences=example.teleport.sh | jwt decode -[...]{ "aud": "example.teleport.sh", "azp": "115001032080118155850", "exp": 1784168534, "iat": 1784164934, "iss": "https://accounts.google.com", "sub": "115001032080118155850"}
Note the following:
- There are no custom claims to match against
- Only
subcontains any identifying information at all - The issuer (
iss) is global (https://accounts.google.com) and applies to all GCP users - The audience (
aud) is determined by the client and is generally not a claim that should be used for client verification
Given this, if you configured a generic_oidc join token to trust these GCP
tokens, it would be trivially easy to write a rule that accidentally allowed
any GCP user to authenticate to your Teleport cluster if your rule for the
single useful claim (sub) was written incorrectly. As such, we would not
recommend using generic_oidc to allow clients to join with a token this
limited.
In GCP's case, you can pass an additional flag (--token-format=full) to the
gcloud tool to include a larger set of useful claims in the issued JWT, like
service account, instance, and project information - this is what Teleport's
dedicated gcp join method
ensures - and is why built-in join methods are generally preferred where
available.
Limitations
Be aware that not all possible rules can be configured at this time. The
generic_oidc join method has 3 different rule types, but it still may not
always possible to encode arbitrarily complex logic into the token to account
for especially unusual providers.
Limitations to be aware of include:
- Teleport's predicate language currently cannot compare floating point numbers,
so
expressionrules withinallow_anycannot validate numeric fields. Usemust_match_fieldsorconditionsif you need to compare a number field. Note that numbers encoded as strings in the JWT can be compared as strings without issue. - Teleport's predicate language currently has limited support for lists and
unary strings within its built-in functions, so string and list values must be
wrapped using the
set()helper to be used certain functions likeregexp.match(),contains_all(), orcontains_any(). - The
generic_oidcjoin method will refuse to compare integers encoded in number fields if they are larger than 2^53-1. This is because JSON numbers (64 bit IEEE floating point values) cannot uniquely represent numbers larger than 2^53-1, meaning multiple values would would match your expected number beyond this limit. If your token tries to compare a field with an integer value larger than this - on either side of the comparison - the join attempt will be rejected. - Teleport's Terraform provider currently does not support
must_match_fieldsand will drop any values you enter into the field. - Teleport's Kubernetes operator currently has limited support for
must_match_fieldsand only top-level claims fields (i.e. not nested fields) can be validated with this rule type.
Identity provider requirements
Additionally, there are a number of minimum security requirements that identity
providers must meet to work with generic_oidc:
- They must use a modern signature algorithm, including:
RS256or larger,PS256or larger,ES256or larger, orEdDSA - They must provide a
kidin the token header - They must issue JWTs with standard fields set, including
exp,iat,sub,aud, andiss - They must be able to serve OIDC discovery and JWKS resources within 10
seconds (unless
static_jwksis used) - They must serve OIDC discovery and JWKS resources via HTTPS (unless
insecure_allow_http_issueris set in the token) at the standard path:$ISSUER/.well-known/openid-configuration
Step 1/3. Create a Bot
Next, you need to create a Bot. A Bot is a Teleport identity for a machine or group of machines. Like users, bots have a set of roles and traits which define what they can access.
Create bot.yaml:
kind: bot
version: v1
metadata:
# name is a unique identifier for the Bot in the cluster.
name: example
spec:
# roles is a list of roles to grant to the Bot. Don't worry if you don't know
# what roles you need to specify here, the Access Guides will walk you through
# creating and assigning roles to the already created Bot.
roles: []
Make sure you replace example with a unique, descriptive name for your Bot.
Use tctl to apply this file:
tctl create bot.yaml
Step 2/3. Create a join token for Generic OIDC
In order to allow your workload to authenticate with your Teleport cluster, you'll need to create a join token. These tokens define criteria by which the Auth Service decides whether to allow a bot or node to join.
Retrieving a reference token
To create a generic_oidc join token, you'll first need to determine which OIDC
provider you're using and then find or generate a reference JWT to use as a
template for building your join token and rules.
Many providers provide documented token examples, for example:
Note that many of these providers (like GitHub) have dedicated Teleport join
methods which should be used instead of generic_oidc.
Alternatively, if you can fetch a JWT inside your existing workload, then you
can decode it with a tool like jwt-cli:
echo "$EXAMPLE_TOKEN" | jwt decode -
Different providers issue tokens to workloads in different ways, for example:
- Many CI/CD providers insert a token into a job's environment as an environment variable before the job starts. Note that many providers require a config parameter to enable this for a given step or workflow, or require a project-level setting to enable OIDC token issuance.
- Some platforms have a CLI tool that can fetch tokens and write them to stdout.
- Other platforms have an internal private HTTP API you can query to request a token.
Regardless of your provider, to continue you'll need to determine which of the
two options you'll use to configure the tbot client to use the token:
- An environment variable that contains a JWT
- A command to run that fetches a JWT
For this example, we'll use a hypothetical provider named ExampleCI. They
provide a command to run to fetch tokens with a given audience (aud) value,
and their tokens look like this:
example-ci issue-token --audience=example.teleport.sh/example-bot | jwt decode -{ "namespace_id": "123", "namespace_path": "acme-corp", "project_id": "456", "project_path": "acme-corp/example-project", "user_id": "1", "user_login": "alice", "user_email": "[email protected]", "job_id": "100", "ref": "feature-branch-1", "ref_type": "branch", "ref_path": "refs/heads/feature-branch-1", "ref_protected": "false", "runner": { "environment": "self-hosted", "protected": "false", "action": "start" }, "job_source": "push", "jti": "235b3a54-b797-45c7-ae9a-f72d7bc6ef5b", "iss": "https://example.com", "iat": 1681395193, "nbf": 1681395188, "exp": 1681398793, "sub": "project_path:acme-corp/example-project:ref_type:branch:ref:feature-branch-1", "aud": "example.teleport.sh/example-bot"}
Creating a join token
With this JWT template in mind, you will create a join token that grants access
to any CI/CD workflow runs within the acme-corp/example-project repository,
with some additional restrictions included for demonstration purposes. You can
find a full list of the available rules and syntax on the
join token reference page.
Create a file named bot-token.yaml:
kind: token
version: v2
metadata:
name: example-bot
spec:
# The Bot role indicates that this token grants access to a bot user, rather
# than allowing a node to join. This role is built in to Teleport.
roles: [Bot]
join_method: generic_oidc
# The bot_name indicates which bot user this token grants access to. This
# should match the name of the bot that you created in the previous step.
bot_name: example
generic_oidc:
# Issuer is the JWT issuer, and must match the `iss` value in the token
# template. Additionally, it must serve OIDC discovery and JWKS documents,
# unless `static_jwks` is configured as well.
issuer: https://example.com
# The token audience, or `aud` value. If your provider lets you set or
# request this value, it should uniquely identify the Teleport cluster and
# token, like the example below. You can use a random value like a UUID
# if you prefer not to use the token.
# If your provider does not allow you to specify the audience, enter the
# value they provide here directly. Additionally, some providers include an
# array of audiences; if so, the value specified here must be present in the
# array.
# The audience is always validated regardless of any custom rules you
# specify.
audience: example.teleport.sh/example-bot
# These "field"-style matchers are "global" and are applied to all join
# attempts using this token. They're useful for ensuring all `allow_any`
# rules still check important fields, like the organization or namespace,
# and ensure that even a mistake in an `allow_any` rule that might otherwise
# allow joins from an unintended party still check at least that field.
#
# These match against the structure of the incoming JWT, so you can freely
# specify even nested fields. Note that only strings, numbers, and booleans
# can be used here. To check
# against lists, you must use `allow_any` rules.
#
# This field is optional if you only want to use `allow_any` rules; at least
# one rule is required between both rule categories.
must_match_fields:
# This ensures all joins at least come from the "acme-corp" namespace. Be
# cautious of only matching against bare repository names: they may not be
# adequately namespaced, so attackers might be able to make a repository
# with the same name in a different namespace. A `must_match_fields` check
# ensures all `allow_any` rules that allow different repositories or other
# sub-resources at least must fall within this namespace, even if a given
# expression accidentally allows unintended clients.
namespace_path: acme-corp
# Note the datatype: a plain 123 in YAML would parse a number and would
# not match the datatype in the template JWT (a number inside a string).
# As a security tip, for providers that provide both forms, it may be
# helpful to match both a name and unique ID:
# - Unique IDs help prevent name-reuse attacks if a repository or
# namespace is deleted
# - Named identifiers help give a readable name to an arbitrary number or
# UUID
# Combining both can help keep tokens both secure and auditable.
namespace_id: "123"
# You can nest fields freely. If a parent field doesn't exist, the join
# attempt is rejected.
runner:
environment: self-hosted
# These rules are evaluated after `must_match_fields` (if any), and only one
# needs to match for the join to be allowed. This lets you allow joins from
# multiple different repositories within the same token, if desired.
#
# This field is optional if you are satisfied with the "AND"-only rules of
# `must_match_fields`; at least one rule is required between both rule
# categories.
allow_any:
# This rule uses a Teleport Predicate expression to allow joins where the
# user email matches a given suffix via regex.
# All claim fields are available under the `claims` variable. Note that
# the `set()` helper must be used to wrap any list or string values for
# functions that expect a Set, like `regexp.match()` or `contains_all()`.
# Note that the expression is wrapped in single quotes ('...') due to the
# double quotes inside the expression itself.
- expression: 'regexp.match(set(claims.user_email), "^[email protected]$")'
# This allows workflows from the `acme-corp/example-project` repo to join
# without further restrictions.
- conditions:
- attribute: project_path
eq:
value: acme-corp/example-project
# This allows workflows from `acme-corp/other-project`, but only if they
# are run against the `feature-branch-1` branch.
- conditions:
- attribute: project_path
eq:
value: acme-corp/other-project
- attribute: ref_type
eq:
value: branch
- attribute: ref
eq:
value: feature-branch-1
As these are explicitly demonstration values, make sure to replace all of these rules with values tailored to your provider and environment. Broadly, we recommend the following minimum rules:
-
Define some "global" requirements in
must_match_fields, like an organization name or ID. These are always required, and are cheap insurance to protect against anallow_anyrule that might accidentally let unintended clients in. At worst, these can limit the blast radius to within your organization. -
Define one or more
allow_anyrules to allow individual repositories, workloads, or other units that should be granted access, using eitherexpressionorconditions.-
The
conditionsrule allows you to specify a list of simple conditions (eq,not_eq,in,not_in) that claim attributes must match. -
The
expressionrule type uses Teleport's predicate language to evaluate claims based on complex logic.See the limitations section above for some specific compatibility notes when using some built-in predicate functions.
-
Additionally, be aware of the rule evaluation order:
- First, all
must_match_fieldschecks are executed. All values specified here must both exist and match the corresponding values on the incoming JWT. Ifmust_match_fieldsis empty, this is skipped. - Next, each rule listed in
allow_anyis evaluated sequentially and evaluation stops after the first matching rule (either anexpressionorconditions). - You may combine as many rules as you'd like between either
must_match_fieldsorallow_any, or skip either variant entirely if you prefer. At least one rule or value must be matched between both rule types.
Once you've written your token YAML, create it with tctl:
tctl create -f bot-token.yaml
Check that token example-bot has been created with the following
command:
tctl tokens lsToken Type Labels Expiry Time (UTC)----------- ---- ------ ----------------------------------------------example-bot Bot 01 Jan 00 00:00 UTC (2562047h47m16.854775807s)
Step 3/3. Configure tbot within your workflow
Due to the wide variation in potential providers or platforms you might use
generic_oidc on, we can only provide generalized advice for running tbot.
First, to install tbot, you can use your Teleport cluster's install.sh to
download and install the proper version:
curl "https://example.teleport.sh:443/scripts/install.sh" | bash
Next, tbot needs to be configured to connect to your Teleport cluster, and
must also to be configured to actually fetch the JWT. This can be done inside
tbot.yaml, but you might use a slightly different starting template depending
on the type of workload or provider in use.
Ephemeral workloads
For ephemeral workloads like CI/CD runs, there's no need to specify a long-lived
storage directory, so the memory type can be used. Start with this
tbot.yaml:
version: v2
proxy_server: example.teleport.sh:443
onboarding:
join_method: generic_oidc
token: example-bot
generic_oidc:
# Specify a command to run. The first value must be the executable to run,
# followed by arguments, one per list entry.
command: ["example-ci", "issue-token", "--audience=example.teleport.sh/example-bot"]
# By default, commands timeout after 1 minute. If your OIDC provider needs
# a different value, specify it in `timeout`
# timeout: 1m
# Alternatively, if the token can be found inside an environment variable,
# you can simply specify the name of the variable containing the token:
# env: "ENV_VAR_WITH_JWT"
storage:
# For ephemeral workloads, there is no need to store data persistently
type: memory
# Configure one or more services. Refer to the Access Guides to set up your
# desired services.
services: []
Long-lived workloads
For deployments where you expect a single tbot instance to last a long time,
and if it has a persistent filesystem, you might instead start with this
tbot.yaml:
version: v2
proxy_server: example.teleport.sh:443
onboarding:
join_method: generic_oidc
token: example-bot
generic_oidc:
# Specify a command to run. The first value must be the executable to run,
# followed by arguments, one per list entry.
command: ["example-ci", "issue-token", "--audience=example.teleport.sh/example-bot"]
# By default, commands timeout after 1 minute. If your OIDC provider needs
# a different value, specify it in `timeout`
# timeout: 1m
# Alternatively, if the token can be found inside an environment variable,
# you can simply specify the name of the variable containing the token:
# env: "ENV_VAR_WITH_JWT"
storage:
# This writes long-lived bot data to the configured directory. Note that the
# Unix user that runs `tbot` must have write access to this path.
type: directory
path: /var/lib/teleport/bot
# Configure one or more services. Refer to the Access Guides to set up your
# desired services.
services: []
Running tbot
Once configured, run the tbot client:
tbot start -c tbot.yaml
Note that the exact mechanics for running tbot, especially if you need it to
run in the background to serve proxies or other long-lived services, will vary
between different providers and platforms.
Next steps
- Follow the access guides to finish
configuring
tbotfor your environment by assigning roles and configuring services. - For more information about the
generic_oidcjoin method, read the join token reference page - More information about
anonymous-telemetry.