Skip to main content

Joining Services 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 Teleport services using identity providers that do not have a dedicated join method. Generic OIDC allows you to join Teleport services from any platform or provider where workloads are issued OIDC compatible JWTs, and allows you to define custom rules to allow only the intended hosts or workloads to authenticate to Teleport.

This guide will explain how to use the Generic OIDC join method to configure Teleport services to join your Teleport cluster by establishing trust with an OIDC-compatible issuer of your choice.

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
  • An OIDC-compatible issuer of your choice (see below for specific requirements)
  • An environment or workload that can fetch JWTs from your OIDC-compatible issuer
  • One of the following client tools for managing Teleport resources:

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/2. Create a Generic OIDC join token

To allow your new agent to authenticate with Teleport, you'll need to create a join token. Join tokens define the criteria by which the Teleport Auth Service decides whether a join attempt will be allowed or rejected. In this step, you'll create a generic_oidc-type join token.

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, GitLab, and Google Cloud) 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 providers that provide an HTTP endpoint, you may need to provide your own script that fetches the token, for example using curl.

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=teleport.example.com/example-agent | 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": "alice@example.com", "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": "teleport.example.com/example-agent"}

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.

To do so, decide how you wish to create the token - directly via tctl, via Teleport's Terraform provider, or via Teleport's Kubernetes operator - and refer to the matching token template to use as a starting point:

Create token.yaml with this initial content:

kind: token
version: v2
metadata:
  # name identifies the token. When configuring a bot or node to join using this
  # token, this name should be specified.
  name: generic-oidc-token
spec:
  # For Machine & Workload Identity bots, roles will always be "Bot". For
  # standard Teleport agents, roles may include "Node", "Kube", "App", "db", or
  # similar, depending on the desired services.
  roles: [Node]

  # This field is always "generic_oidc" for Generic OIDC joining.
  join_method: generic_oidc

  # bot_name specifies the name of the bot that this token will grant access to
  # when it is used. If joining a standard Teleport agent, this field should be
  # omitted. It is required for bots.
  # bot_name: generic-oidc-demo

  generic_oidc:
    # The OIDC issuer. Must exactly match the `iss` value in incoming OIDC
    # tokens. This value is always required.
    #
    # Unless `static_jwks` is configured, this issuer must be accessible over
    # HTTPS to the Teleport cluster and must serve valid OIDC metadata,
    # including discovery configuration and JWKS keys.
    issuer: https://example.com

    # If set, this flag allows the use of HTTP-only issuers. Not recommended for
    # production use.
    # insecure_allow_http_issuer: false

    # The audience (`aud`) value to require in the incoming JWTs. This value is
    # always required.
    #
    # Note that not all issuers allow you to configure or request an audience
    # value of your choosing. For issuers that require use of prescribed `aud`
    # values, that value should be entered here exactly.
    #
    # Otherwise, for providers that do allow you to configure or request a
    # value, we recommend making the value unique to this cluster and join
    # token. You can include your Teleport cluster name (teleport.example.com)
    # and the name of the token, as shown below, or use a random UUID if you
    # prefer.
    audience: teleport.example.com/generic-oidc-token

    # The TLS CA certificate or certificates, if your OIDC provider's TLS
    # certificates are not trusted by the standard system trust store. If set,
    # this value replaces the system CA store outright, so you can include
    # multiple concatenated certificates if necessary.
    #
    # The CAs specified here are only used to validate requests using this token
    # and will not be used to verify any other join attempts or TLS connections
    # for other Teleport features. Be aware that if the issuer's TLS certificate
    # is rotated such that it is no longer trusted by the CA certs specified
    # here, join attempts will be rejected and this token will need to be
    # updated to include the updated CA certificates.
    #
    # Most users will not need to configure this value.
    #
    # tls_ca: |
    #   -----BEGIN CERTIFICATE-----
    #   ...
    #   -----END CERTIFICATE-----

    # These fields perform simple equality matches against the incoming JWT
    # using "AND" semantics. Rules are written by mirroring the structure of the
    # JWT, and each written value must be equal to the value in the JWT
    # presented by the client.
    #
    # These field rules can be used as simple "global" matchers that apply to
    # all join attempts using this token. For example, you can use this to
    # ensure all `allow_any` rules also check `organization_name` to ensure they
    # can never accidentally skip an important check.
    #
    # If any rules are specified here, all must match. If any rules are
    # specified in `allow_any`, these rules are evaluated first, if any exist,
    # and `allow_any` is only evaluated if all of these checks pass. If no rules
    # are specified here, only `allow_any` rules are evaluated. At least one
    # rule must exist between `must_match_fields` and `allow_any`.
    #
    # Note the following limitations for field rules:
    # - This field currently cannot be configured via the Teleport Terraform
    #   provider and `allow_any` must be used instead.
    # - Strings, numbers, booleans, and nested objects are supported.
    # - List values are currently not supported.
    # - Integer comparisons must be less than 2^53-1, as integers larger than
    #   this cannot be accurately represented in JSON's 64-bit floating point
    #   number type. If either a rule or incoming value is larger than this, the
    #   join attempt will be rejected.
    #
    # NOTE: must_match_fields is currently NOT SUPPORTED in Teleport's Terraform
    # provider, and any rules you specify here will be SILENTLY IGNORED. If
    # using the Terraform provider, you must rely exclusively on `allow_any`
    # rules instead.
    #
    # NOTE: must_match_fields currently has limited support if using Teleport's
    # Kubernetes operator to provision resources. Nested fields are currently
    # not supported and will result in an error at creation time. Top-level
    # fields (direct children of `must_match_fields`) will work as expected.
    must_match_fields:
      organization_name: "acme-corp"

      # Fields can be nested freely to match the structure of the incoming JWT.
      example:
        project_id: example-1234567

        # Numeric comparisons are supported, but be mindful of the datatype as
        # determined by YAML parsing rules.
        # This will be parsed as a number, and the datatype of the equivalent
        # field on the incoming JWT must also be a number. To compare as a
        # string, it must be quoted, e.g.: "1234"
        number_field: 1234

        # Boolean values are supported, but as above, this also requires that
        # the equivalent field on the incoming JWT is a boolean. To compare it
        # as a string, the value must be quoted, e.g.: "true"
        boolean_field: true

    # If specified, at least one of these rules must match. These are evaluated
    # with "OR" semantics, after all `must_match_fields` rules have been
    # evaluated successfully (if any were written). At least one rule must be
    # written between `must_match_fields` and `allow_any`.
    #
    # Note that for a given entry, at most one of `expression` or `conditions`
    # can be configured.
    allow_any:
      # A Teleport predicate expression. Claims are available under the `claims`
      # object.
      # Note the following limitations with Teleport's predicate language and
      # claim evaluation:
      # - Numbers cannot be compared; use `conditions` or `must_match_fields`
      #   for numeric comparisons instead.
      # - Lists must be wrapped in `set()` for use with most predicate
      #   functions.
      - expression: 'claims.organization_name == "acme-corp"'

      # A list of simple field conditions. Unlike expressions, attributes are
      # not prefixed with "claims" and instead are a dot-separated list of
      # fields starting from the top level of the JWT.
      #
      # Valid operators include: eq, not_eq, in, not_in
      - conditions:
        - attribute: email_verified
          eq:
            value: "true"

        # Nested fields are supported. This denies join requests where the
        # project named "invalid-project" is present in the JWT.
        - attribute: example.project_name
          not_eq:
            value: "invalid-project"

        # `in` can be used to accept one from a set of values, for example this
        # allows join attempts where `project_name` is any of "foo", "bar", or
        # "baz".
        - attribute: example.project_name
          in:
            values: [foo, bar, baz]

        # `not_in` can be used to exclude a set of values, for example this
        # denies join attempts where `zone` is `us-central1-a|b|c`
        - attribute: zone
          not_in:
            values: ["us-central1-a", "us-central1-b", "us-central1-c"]

Customize it as necessary, and when ready, create the token:

tctl create token.yaml

Finally, validate the token was created:

tctl get token/generic-oidc-token

kind: tokenmetadata: expires: "3000-01-01T00:00:00Z" name: generic-oidc-tokenspec: join_method: generic_oidc roles: - Nodeversion: v2

Be aware that the examples shown above are for demonstrative purposes. Make sure to replace all rules with values tailored to your provider and environment, using the reference token you fetched as a guide.

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.

    In the example JWT shown above, namespace_id and namespace_path might be good global rules to configure for all joining clients.

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

    With the example JWT in mind, project_id and project_path might be useful fields to perform an equality check on, to ensure join attempts are coming from an authorized project.

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

Step 2/2. Set up Teleport on your provider

The Generic OIDC join method can be used for Teleport processes running the SSH, Proxy, Kubernetes, Application, Database, or Desktop Service.

  1. Install Teleport on your target environment (VM or physical machine)

    To install Teleport binaries on your Linux server, the recommended installation method is the cluster install script. This script is served by your Teleport cluster's Proxy Service and automatically selects the correct version, edition, and installation mode to match your cluster.

    1. Remove any existing Teleport binaries on your system:

      sudo rm -f /usr/local/bin/{tsh,teleport,tctl,tbot,fdpass-teleport,teleport-update}
    2. Assign teleport.example.com:443 to your Teleport cluster hostname and port, but not the scheme (https://).

    3. Run your cluster's install script:

      curl "https://teleport.example.com:443/scripts/install.sh" | sudo bash
  2. Configure your Teleport process with a custom teleport.yaml file. Use the join_params section with token_name matching your token created in Step 1 and method: generic_oidc as shown in the following example config:

    # /etc/teleport.yaml
    version: v3
    teleport:
      join_params:
        token_name: generic-oidc-token
        method: generic_oidc
        generic_oidc:
          # Specify a command to run. The first value must be the executable to run,
          # followed by arguments, one per list entry. This example calls
          # `example-provider issue-token ...` with a client-specified audience
          # request. Your provider will differ.
          command: ["example-ci", "issue-token", "--audience=teleport.example.com/example-agent"]
    
          # 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. This cannot be combined with `command`.
          # env: "ENV_VAR_WITH_JWT"
      proxy_server: teleport.example.com:443
    ssh_service:
      enabled: true
    auth_service:
      enabled: false
    proxy_service:
      enabled: false
    
  3. Start Teleport:

    Start your Teleport instance. The instructions depend on how you installed your Teleport instance and whether your system supports systemd:

    Configure your Teleport instance to start automatically when the host boots up by creating a systemd service for it. On the host where you will run your Teleport instance, enable and start Teleport:

    sudo systemctl enable teleport
    sudo systemctl start teleport

    You can check the status of your Teleport instance with systemctl status teleport and view its logs with journalctl -fu teleport.

  4. Confirm that your Teleport process is able to connect to and join your cluster. You're all set!