Skip to main content

Deploying tbot with any OIDC provider via Generic OIDC

Report an Issue

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.

warning

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 tctl and tsh clients.

    Installing tctl and tsh clients
    1. Determine the version of your Teleport cluster. The tctl and tsh clients must be at most one major version behind your Teleport cluster version. Send a GET request to the Proxy Service at /v1/webapi/find and use a JSON query tool to obtain your cluster version. Replace teleport.example.com:443 with the web address of your Teleport Proxy Service:

      TELEPORT_DOMAIN=teleport.example.com:443
      TELEPORT_VERSION="$(curl -s https://$TELEPORT_DOMAIN/v1/webapi/find | jq -r '.server_version')"
    2. Follow the instructions for your platform to install tctl and tsh clients:

      Download the signed macOS .pkg installer for Teleport, which includes the tctl and tsh clients:

      curl -O https://cdn.teleport.dev/teleport-${TELEPORT_VERSION?}.pkg

      In Finder double-click the pkg file to begin installation.

      danger

      Using 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.

    Connecting with TLS routing disabled

    This guide's commands assume your Teleport cluster uses TLS routing (proxy_listener_mode: multiplex), where the tctl and tsh clients reach every Teleport service through the Proxy Service's web address on port 443. 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:

    • tsh commands (e.g., tsh login --proxy=...): continue using the Proxy Service web address on port 3080 (or 443 if behind a load balancer). Do not change these to port 3025.

    • Direct tctl or Auth Service API commands: use port 3025 for 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 tctl and tsh commands using your current credentials.
    1. Assign teleport.example.com to the domain name of the Teleport Proxy Service in your cluster and [email protected] to your Teleport username.

    2. 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 status

      Cluster teleport.example.com

      Version 18.10.0

      CA pin sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678

      On non-interactive environments: If you are running tsh and tctl as an AI agent, in a CI/CD environment, or similar, make sure the TELEPORT_IDENTITY_FILE environment variable is assigned to a valid file path with credentials for your cluster. tsh and tctl read 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 tctl commands with an identity file, you must pass the --auth-server flag to provide the Teleport Auth Service address, which is not included in the identity file. If you provide the Proxy Service address, tctl connects to the Proxy Service, which forwards traffic to and from the Teleport Auth Service. Update 443 to 3025 if you are contacting the Auth Service directly with tctl:

      tctl status --auth-server=teleport.example.com:443

      For tsh commands that read an identity file, you must pass the --proxy flag, which points tsh to the address of the Teleport Proxy Service:

      tsh status --proxy=teleport.example.com

      Ensure 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-server or --proxy flags to all subsequent tctl and tsh commands.

    If you can connect to the cluster and run the tctl status command, you can use your current credentials to run subsequent tctl commands from your workstation. If you host your own Teleport cluster, you can also run tctl commands 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 sub contains 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 expression rules within allow_any cannot validate numeric fields. Use must_match_fields or conditions if 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 like regexp.match(), contains_all(), or contains_any().
  • The generic_oidc join 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_fields and will drop any values you enter into the field.
  • Teleport's Kubernetes operator currently has limited support for must_match_fields and 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:

  1. They must use a modern signature algorithm, including: RS256 or larger, PS256 or larger, ES256 or larger, or EdDSA
  2. They must provide a kid in the token header
  3. They must issue JWTs with standard fields set, including exp, iat, sub, aud, and iss
  4. They must be able to serve OIDC discovery and JWKS resources within 10 seconds (unless static_jwks is used)
  5. They must serve OIDC discovery and JWKS resources via HTTPS (unless insecure_allow_http_issuer is 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:

  1. An environment variable that contains a JWT
  2. 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 an allow_any rule that might accidentally let unintended clients in. At worst, these can limit the blast radius to within your organization.

  • Define one or more allow_any rules to allow individual repositories, workloads, or other units that should be granted access, using either expression or conditions.

    • The conditions rule allows you to specify a list of simple conditions (eq, not_eq, in, not_in) that claim attributes must match.

    • The expression rule 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_fields checks are executed. All values specified here must both exist and match the corresponding values on the incoming JWT. If must_match_fields is empty, this is skipped.
  • Next, each rule listed in allow_any is evaluated sequentially and evaluation stops after the first matching rule (either an expression or conditions).
  • You may combine as many rules as you'd like between either must_match_fields or allow_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 ls
Token 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