Simplifying Zero Trust Security for AWS with Teleport
Jan 23
Virtual
Register Now
Teleport logoTry For Free
Fork me on GitHub

Teleport

Run the Teleport Terraform Provider in CI or Cloud

This guides covers how to run the Teleport Terraform Provider from:

  • your CI/CD pipelines on:
    • GitHub Actions (we'll use the github join method)
    • GitlabCI (we'll use the gitlab join method)
    • CircleCI (we'll use the circleci join method)
  • a cloud VM on:
    • AWS (we'll use the aws join method)
    • GCP (we'll use the gcp join method)
Note

Running the Terraform provider with native MachineID is supported on Azure, inside a Kubernetes pod, and on servers with Trusted Platform Module (TPM). While those setups are not described in details in this guide, you can follow their regular MachineID guides and replace the "Configure tbot" step by passing the join method and token to the provider.

HCP Terraform (Terraform Cloud) and self-hosted Terraform Enterprise are supported but require special configuration, so refer to our dedicated guide.

This guide does not cover running Teleport locally, on a dedicated server or on certain platforms. See the following more specific guides for those cases:

How it works

This setup asks the runtime (the CI/CD system, cloud provider, container engine, ...) for an identity proof. This proof is then used directly by the Terraform provider to connect to Teleport and obtain credentials. In this setup, there is no tbot daemon involved as the Terraform provider can natively obtain the identity proof and join the Teleport cluster.

The setup only works for select runtimes which Teleport has a delegated join method for (e.g. GitHub Actions, GitLab CI, ...)

Prerequisites

You need either:

  • A GCP or AWS VM with terraform installed
  • A git repo able to run GitHub Actions, GitLab CI or CircleCI jobs

You also need:

  • A running Teleport cluster version 16.4.12 or above. If you want to get started with Teleport, sign up for a free trial or set up a demo environment.

  • The tctl admin tool and tsh client tool.

    Visit Installation for instructions on downloading tctl and tsh.

Step 1/4. Create the Terraform provider bot

In this step you will create a bot named terraform.

Create a file named terraform-bot.yaml:

kind: bot
version: v1
metadata:
  name: terraform
spec:
  # The terraform-provider is a default role shipped in Teleport granting access
  # to every resource supported by the terraform provider.
  roles: ["terraform-provider"]

Then apply it with tctl:

tctl create terraform-bot.yaml

This is an admin-level action and requires MFA to completeTap any security keyDetected security key tapbot "terraform" has been created

At this point, you should see your new bot:

tctl bots ls

Bot User Roles--------- ------------- ------------------terraform bot-terraform terraform-provider

Step 2/4. Create the bot join token

In this step you will create a token allowing a process to connect to Teleport as the terraform bot you created earlier.

The token type and configuration depends on where the Terraform provider is running. See the joining reference for more details about the joining process, the different join methods, types of tokens, and the fields they support.

To allow the Terraform Provider to join from GitHub Actions workflows in organization/repository, create the following terraform-bot-token.yaml:

kind: token
version: v2
metadata:
  name: terraform-bot
spec:
  roles: [Bot]
  join_method: github
  bot_name: terraform
  github:
    # allow specifies rules that control which GitHub Actions runs will be
    # granted access. Those not matching any allow rule will be denied.
    allow:
    - repository: organization/repository

To allow the Terraform Provider to join from GitLab CI pipelines in group/project on the gitlab.example.com GitLab instance, create the following terraform-bot-token.yaml:

kind: token
version: v2
metadata:
  name: terraform-bot
spec:
  roles: [Bot]
  join_method: gitlab
  bot_name: terraform
  gitlab:
    # domain should be the domain of your GitLab instance. If you are using
    # GitLab's cloud hosted offering, omit this field entirely.
    domain: gitlab.example.com
    # allow specifies rules that control which GitLab tokens will be accepted
    # by Teleport. Tokens not matching any allow rule will be denied.
    allow:
      - project_path: group/project

In order to configure the rules for which CircleCI workflows will be allowed to connect to your Teleport cluster, you must determine the ID of your CircleCI organization and create a CircleCI context.

Find your organization ID

Open CircleCI and navigate to "Organization settings" from the navbar. You should be presented with an interface titled "Overview" with a section called "Organization ID". Note this value down and substitute organization-id in configuration examples with this.

Create a context

CircleCI has an organization-level concept called contexts, which allow you to configure a series of secrets that should be exposed to a workflow job. You can configure CircleCI to control which actors are allowed to trigger jobs associated with a context.

The contexts that a workflow job has been assigned are also encoded in the identity token that CircleCI creates for the job. This makes them an ideal way for Teleport to determine which CircleCI jobs should be granted access to the Teleport cluster.

In this example, you will create a CircleCI context named teleport-access. You will then grant this context access to your Teleport cluster.

To create the CircleCI context, open up "Organization settings" in CircleCI and navigate to "Contexts". Click "Create Context" and provide teleport-access as the name of the context you wish to create. You may substitute this value for a string that makes more sense to your organization, but ensure in future steps of this guide that you replace teleport-access with your value.

Select the context you have just created. You will now be on a page that allows you to configure the context. To determine the ID of the context to use when configuring Teleport, locate the URL of the context settings page, which should have a format similar to the following:

https://app.circleci.com/settings/organization/github/gravitational/contexts/00000000-0000-0000-0000-000000000000

In this case, the context ID is: 00000000-0000-0000-0000-000000000000.

Note this value down and substitute context-id in configuration examples with this

Then, create the following terraform-bot-token.yaml:

kind: token
version: v2
metadata:
  name: terraform-bot
spec:
  roles: [Bot]
  join_method: circleci
  bot_name: terraform
  circleci:
    organization_id: organization-id
    # allow specifies the rules by which the Auth Server determines if `tbot`
    # should be allowed to join.
    allow:
    - context_id: context-id

Make sure you have:

  • An AWS IAM role that you wish to grant access to your Teleport cluster. This role must be granted sts:GetCallerIdentity. In this guide, this role will be named instance-iam-role.
  • An AWS EC2 virtual machine configured with the IAM role attached where you want to run the Terraform provider.
  • The AWS account ID: 111111111111

Then, create the following terraform-bot-token.yaml:

kind: token
version: v2
metadata:
  name: terraform-bot
spec:
  roles: [Bot]
  bot_name: terraform
  join_method: iam
  allow:
    - aws_account: "111111111111"
      aws_arn: "arn:aws:sts::organization-id:assumed-role/instance-iam-role/i-*"

Make sure you have:

  • A GCP Service Account (SA) attached to the VM you want to run the provider on. This cannot be the default compute SA. In this guide, this SA will be named my-service-account.
  • The GCP project ID: my-project-123456

Then, create the following terraform-bot-token.yaml:

kind: token
version: v2
metadata:
  name: terraform-bot
spec:
  roles: [Bot]
  bot_name: terraform
  join_method: gcp
  gcp:
    allow:
    - project_ids:
        - "my-project-123456"
      service_accounts:
        - "my-service-account@my-project-123456.iam.gserviceaccount.com"

Create the bot token described by the terraform-bot-token.yaml manifest:

tctl create -f terraform-bot-token.yaml

This is an admin-level action and requires MFA to completeTap any security keyDetected security key tapprovision_token "terraform-bot" has been created

Step 3/4. Configure your Terraform provider

In this step you will write a minimal Terraform code that configures the provider to connect to your Teleport cluster and join with the token you previously created.

To do this you need:

  • your Teleport cluster domain including the port: teleport.example.com:443. You can find this in the URL when accessing the Web UI.
  • the join method of the token you've created. This is the spec.join_method field in the terraform-bot-token.yaml: token-join-method.

Create this minimal main.tf file:

terraform {
  required_providers {
    teleport = {
      source  = "terraform.releases.teleport.dev/gravitational/teleport"
      version = "~> 16.0"
    }
  }
}

provider "teleport" {
  addr        = "teleport.example.com:443"
  join_method = "token-join-method"
  join_token  = "terraform-bot"
}

# We must create a test role, if we don't declare resources, Terraform won't try to
# connect to Teleport and we won't be able to validate the setup.
resource "teleport_role" "test" {
  version = "v7"
  metadata = {
    name        = "test"
    description = "Dummy role to validate Terraform Provider setup"
    labels = {
      test = "yes"
    }
  }

  spec = {}
}

Copy the main.tf file in the GitHub repo that runs GitHub Actions pipelines.

Copy the main.tf file in the GitLab repo that runs GitLab CI pipelines.

Copy the main.tf file in the Git repo that runs CircleCI pipelines.

Copy the main.tf file on the AWS VM you will run Terraform from.

Copy the main.tf file on the GCP VM you will run Terraform from.

Step 4/4. Run Terraform

This step shows minimal examples on how to run Terraform based on your environment. This code uses the default local backend which is not fit for production purposes. Especially in CI, you must use non-local Terraform backends so the Terraform state is persisted across CI pipelines.

In the repo containing your Terraform code, create a .github/workflows/teleport-terraform.yaml file with the following content:

name: Teleport Terraform Demo
# This is a basic workflow to help you get started.
# It will take the following action whenever a push is made to the "main" branch.
on:
  push:
    branches:
    - main
jobs:
  demo:
    permissions:
      # The "id-token: write" permission is required or Machine ID will not be
      # able to authenticate with the cluster.
      id-token: write
      contents: read
    name: terraform-plan
    runs-on: ubuntu-latest

    # You can find more advanced TF workflows at https://github.com/hashicorp/setup-terraform
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
    - name: Terraform Init
      id: init
      run: terraform init
    - name: Terraform Plan
      id: plan
      run: terraform plan -no-color

Commit the changes and push to the main branch to trigger the GitHub Actions workflow. You should see a successful Terraform plan in the workflow logs.

Recover your cluster name:

tctl status

Cluster teleport.example.comVersion 16.2.0

...

In the repo containing your Terraform code, create the .gitlab-ci.yaml file if it does exist, and add the following content:

stages:
  - plan
image:
  name: hashicorp/terraform:1.9
  entrypoint: [""]
terraform-job:
  stage: plan
  # id_tokens configures ID Tokens that GitLab will automatically inject into
  # the environment of your GitLab run.
  #
  # See https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html
  # for further explanation of the id_tokens configuration in GitLab.
  id_tokens:
    TBOT_GITLAB_JWT:
      # aud for TBOT_GITLAB_JWT must be configured with the name of your
      # Teleport cluster. This is not necessarily the address of your Teleport
      # cluster and will not include a port or scheme (http/https)
      #
      # This helps the Teleport Auth Server know that the token is intended for
      # it, and not a different service or Teleport cluster.
      aud: "teleport.example.com"
  script:
    - terraform init
    - terraform plan

Commit the changes and push to any branch to trigger a Gitlab CI pipeline. You should see a successful Terraform plan in the pipeline logs.

In the repo containing your Terraform code add the following CircleCI config in .circleci/config.yml:

version: '2.1'
orbs:
  terraform: circleci/[email protected]
workflows:
  deploy_infrastructure:
    jobs:
      - terraform/init:
          tag: 1.9.5
          checkout: true
          context: teleport-access
      - terraform/plan:
          tag: 1.9.5
          context: teleport-access
          requires:
            - terraform/init

Commit the changes and push to any branch to trigger a CircleCI pipeline. You should see a successful Terraform plan in the pipeline logs.

Run the Terraform commands on the EC2 instance:

terraform init
Initializing the backend...
Initializing provider plugins...- Finding terraform.releases.teleport.dev/gravitational/teleport versions matching ...
terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create
Terraform will perform the following actions:
# teleport_role.test will be created + resource "teleport_role" "test" { + id = (known after apply) + kind = (known after apply) + metadata = { + name = "test" + namespace = (known after apply) } + spec = {} + version = "v7" }
Plan: 1 to add, 0 to change, 0 to destroy.

Run the Terraform commands on the GCP VM:

terraform init
Initializing the backend...
Initializing provider plugins...- Finding terraform.releases.teleport.dev/gravitational/teleport versions matching ...
terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create
Terraform will perform the following actions:
# teleport_role.test will be created + resource "teleport_role" "test" { + id = (known after apply) + kind = (known after apply) + metadata = { + name = "test" + namespace = (known after apply) } + spec = {} + version = "v7" }
Plan: 1 to add, 0 to change, 0 to destroy.