Secretless TLS client certificates in GitHub Actions

Secretless TLS client certificates in GitHub Actions

Updated on: June 8, 2023

TL;DR With GitHub Actions OIDC tokens and Smallstep Certificate Manager, you can access protected internal resources like cloud services, databases, websites, or Kubernetes clusters from inside GitHub Actions, using short-lived TLS certificates. Best of all, this approach requires no hard-coded secrets, and just a few lines of YAML.

Introduction

What are GitHub Actions?

First, a quick refresher on GitHub Actions. GitHub’s Actions lets GitHub maintainers define and automate various workflows that act on GitHub repositories. Actions are hosted inside repositories and defined in YAML files like this one. After launching in 2018, GitHub Actions have become very popular for Continuous Integration (CI) tests and other build and deployment pipelines—especially Continuous Deployment (CD) to the cloud. GitHub Actions has become very popular in recent years, eclipsing third-party tools like Jenkins or Travis CI that used to handle these tasks.

At Smallstep, we use GitHub Actions heavily, to run tests, scan for security vulnerabilities, build binaries for step-ca and step-cli, sign binaries, and to distribute our releases to places like Docker Hub, Homebrew, and other software repositories.

👾 Here’s an extreme use case of GitHub Actions GitHub Actions have a huge variety of use cases, only limited by the imagination. Simon Willison put together a GitHub Action project (that popped on Hacker News last week) which performs automatic AI transcription of YouTube videos. You create a GitHub Issue on his repo with a video link, and the action runs, extracts the captions, and finally posts a comment on the issue with the extracted captions.

What are GitHub Action OIDC tokens?

In November 2021, GitHub vastly improved their integration with cloud providers like AWS and Google Cloud Platform when they introduced OpenID Connect (OIDC) tokens for GitHub Actions. While these tokens are pitched as a security hardening feature, they also simplify any workflow that needs to access external cloud services by removing the need for storing hard-coded secrets in GitHub.

An OIDC token is a bit like a digital driver’s license. It’s a small blob of JSON with some identifying information (”“claims”) signed by a trusted central authority (like GitHub). The magic of these tokens is that they can be passed around and easily verified by any piece of software. It’s part of the same technology (OAuth) that makes single sign-on (SSO) applications possible.

GitHub’s OIDC tokens identify a GitHub Actions Workflow run. So, if you configure your cloud service to accept GitHub’s tokens for authentication, then you don’t need to hard-code any of your cloud credentials in GitHub! The ephemeral OIDC token can be used instead.

How does a Workflow get a GitHub OIDC token?

Every GitHub Workflow run comes with a secret key that can be used to fetch a GitHub OIDC token for the run, via GitHub’s token server. The process for retrieving this token is a little different from an OAuth SSO flow that you may know. Instead of going through a login flow with a web browser, you just need to run one curl command from inside a Workflow to get a token for the Workflow:

TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://StepCATokenExchange" | jq -r .value)

I tried this out with a test workflow, and here’s the payload of the token I got back:

{
    "jti": "d4EXAMPLE11",
    "sub": "repo:tashian/oidctest:ref:refs/heads/main",
    "aud": "api://StepCATokenExchange",
    "ref": "refs/heads/main",
    "sha": "488EXAMPLE40218796a21fa24f4fc55cbdce6ff",
    "repository": "tashian/oidctest",
    "repository_owner": "tashian",
    "repository_owner_id": "4EXAMPLE3",
    "run_id": "3EXAMPLE08",
    "run_number": "21",
    "run_attempt": "3",
    "repository_visibility": "private",
    "repository_id": "5EXAMPLE8",
    "actor_id": "4EXAMPLE3",
    "actor": "tashian",
    "workflow": "GitHub OIDC Demo",
    "head_ref": "",
    "base_ref": "",
    "event_name": "push",
    "ref_type": "branch",
    "job_workflow_ref": "tashian/oidctest/.github/workflows/oidc-demo.yaml@refs/heads/main",
    "iss": "https://token.actions.githubusercontent.com",
    "nbf": 1665009326,
    "exp": 1665010226,
    "iat": 1665009926
  }

There’s a lot of identifying information here. This token is bound to the workflow, the repo, the action that triggered the workflow, and the workflow run itself.

Tutorial: Get a TLS client certificate in a Workflow

Now let’s use the token to get a TLS certificate from a Smallstep Certificate Manager CA:

  1. To follow along, you’ll need to create a Certificate Manager CA if you haven’t already.
  2. Download and install the Step CLI tool, which you’ll use to manage your CA.
  3. Connect your client to your CA. You’ll find the command for this in the Quick Actions section of your authority page in Certificate Manager. Hang on to your CA URL and fingerprint, because you’ll need them again in GitHub.
  4. Add a trust relationship between GitHub and your CA, using an OpenID Connect provisioner:
step ca provisioner add github --type OIDC \
   --configuration-endpoint https://token.actions.githubusercontent.com/.well-known/openid-configuration \
   --client-id "api://SmallstepCAProvisioner"

You can use any Client ID here, it just needs to match the one you use in the request to GitHub's token server, below. 5. Next, in GitHub, create a test repository. Add the following to .github/workflows/oidc-demo.yaml:

name: GitHub OIDC Demo
run-name: ${{ github.actor }} is testing OIDC to X.509 Certs 🚀
on: [push]

permissions:
     id-token: write # This is required for requesting the JWT
     contents: read  # This is required for actions/checkout

env:
     CA_URL: https://example.your.ca.smallstep.com
     CA_FINGERPRINT: c8da28e620ecEXAMPLEc405d92d7350dbec351cef3e4f6a6d2fc9518387ab
     OIDC_CLIENT_ID: api://SmallstepCAProvisioner

jobs:
     OIDC-to-X509:
       runs-on: ubuntu-latest
       steps:
      - name: CA bootstrap
        run: |
          curl -LO https://dl.step.sm/gh-release/cli/gh-release-header/v0.23.1/step-cli_0.23.1_amd64.deb
          sudo dpkg -i step-cli_0.23.1_amd64.deb
          step ca bootstrap --ca-url $CA_URL --fingerprint $CA_FINGERPRINT
      - name: Get a certificate
        run: |
          TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$OIDC_CLIENT_ID" | jq -r .value)
          curl -sLO https://token.actions.githubusercontent.com/.well-known/jwks
          echo $TOKEN | step crypto jwt verify \
            --jwks jwks \
            --aud $OIDC_CLIENT_ID \
            --iss "https://token.actions.githubusercontent.com"
          SUBSCRIBER=$(echo $TOKEN | step crypto jwt inspect --insecure | jq -r .payload.sub)
          step ca certificate $SUBSCRIBER github.crt github.key \
            --provisioner github \
            --token "$TOKEN" \
            --not-after 1h
          step certificate inspect github.crt
  1. Update the CA_URL and CA_FINGERPRINT values to match your Certificate Manager CA. Note also that the OIDC_CLIENT_ID must match whatever client ID you originally configured in your OIDC provisioner configuration.
  2. Now push the YAML to GitHub, and watch the action run. With any luck, you’ll have a certificate (github.crt) and private key (github.key) at the end of the worker run! This short-lived certificate will be valid for an hour (the same duration as the token).

Passing the mic to you...

There’s a couple of next steps you’ll need to take in order to get this setup ready for a production environment.

Once these bits are done, you can extend the workflow to connect to your service and perform actions from there.

What else is possible?

If you haven't already, you can sign up and explore Smallstep Certificate Manager - after all, hardening your environments starts here. From there, you can use GitHub OIDC and Certificate Manager together in the following ways:

Ref: GitHub’s Documentation for GH Actions OIDC

Carl Tashian is an engineer, writer, exec coach, and startup all-rounder. He's currently an Offroad Engineer at Smallstep. He co-founded and built the engineering team at Trove, and he wrote the code that opens your Zipcar. He lives in San Francisco with his wife Siobhan and he loves to play the modular synthesizer 🎛️🎚️