SEANK.H.LIAO

circleci gcp workload identity federation

look ma! no secrets

workload identity federation

The idea is: your runtime environment gives you an identity, and you can go exchange that identity for a different one, which has the access you need.

Today, we'll be experimenting with using CircleCI's OIDC identity to get a Google Cloud Platform identity, both to push images and to sign them.

GCP setup

First we'll want to setup GCP to trust our CircleCI org:

 1locals {
 2  project       = "com-seankhliao"
 3  circleci_org  = "e215a767-1102-43db-bab3-0a7246a49b03"
 4  circleci_proj = "daec1ef0-490b-4837-86f2-28e48b76411e"
 5}
 6
 7
 8# Pool of identities
 9resource "google_iam_workload_identity_pool" "pool" {
10  project                   = local.project
11  workload_identity_pool_id = "sigstore-oidc-test"
12}
13
14# CircleCI identities in the pool
15resource "google_iam_workload_identity_pool_provider" "circleci" {
16  project                            = local.project
17  workload_identity_pool_id          = google_iam_workload_identity_pool.pool.workload_identity_pool_id
18  workload_identity_pool_provider_id = "circleci"
19  attribute_mapping = {
20    "google.subject"       = "assertion.sub"
21    "attribute.project_id" = "assertion['oidc.circleci.com/project-id']"
22  }
23  oidc {
24    allowed_audiences = [
25      local.circleci_org
26    ]
27    issuer_uri = "https://oidc.circleci.com/org/${local.circleci_org}"
28  }
29}
30
31# Service account for projects to impersonate as
32resource "google_service_account" "circleci" {
33  account_id = "sigstore-oidc-test-circleci"
34  project = "com-seankhliao"
35}
36
37# granting all workflow runs on the project the permissions to impersonate the service account
38resource "google_service_account_iam_member" "circleci_wi" {
39  service_account_id = google_service_account.circleci.id
40  member = "principalSet://iam.googleapis.com/projects/330311169810/locations/global/workloadIdentityPools/sigstore-oidc-test/attribute.project_id/${local.circleci_proj}"
41  role               = "roles/iam.workloadIdentityUser"
42}
43
44# granting the service account access to create tokens about itself
45resource "google_service_account_iam_member" "circleci_token" {
46  service_account_id = google_service_account.circleci.id
47  member             = "serviceAccount:${google_service_account.circleci.email}"
48  role               = "roles/iam.serviceAccountTokenCreator"
49}
50
51# granting the service account permissions to push to an Artifact Repository
52resource "google_artifact_registry_repository_iam_member" "build-circleci" {
53  project    = local.project
54  location   = "us-central1"
55  repository = "build"
56  role       = "roles/artifactregistry.writer"
57  member     = "serviceAccount:${google_service_account.circleci.email}"
58}

circleci setup

Then we can use the identity that circleci gives us (if we attach any context), and go exchange that for a GCP identity. With the GCP identity, we can get identity and access tokens for pushing and signing respectively.

 1version: 2.1
 2
 3jobs:
 4  all:
 5    # just need an executor
 6    docker:
 7      - image: golang:alpine
 8    environment:
 9      # keyless signing is still experimental
10      COSIGN_EXPERIMENTAL: "1"
11      # artifact repository to push in to
12      KO_DOCKER_REPO: "us-central1-docker.pkg.dev/com-seankhliao/build"
13      # service account to impersonate as
14      SERVICE_ACCOUNT_EMAIL: "sigstore-oidc-test-circleci@com-seankhliao.iam.gserviceaccount.com"
15    steps:
16      # need code to build
17      - checkout
18      # main run
19      - run:
20          name: build-push
21          command: |
22            # basic tools
23            apk add curl jq
24
25            # exchange our local token for a GCP token
26            STS_TOKEN=$(jq -n \
27              --arg token "${CIRCLE_OIDC_TOKEN}" \
28              '{
29                audience:           "//iam.googleapis.com/projects/330311169810/locations/global/workloadIdentityPools/sigstore-oidc-test/providers/circleci", 
30                grantType:          "urn:ietf:params:oauth:grant-type:token-exchange",
31                requestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
32                scope:              "https://www.googleapis.com/auth/cloud-platform",
33                subjectTokenType:   "urn:ietf:params:oauth:token-type:id_token",
34                subjectToken:       $token
35              }' | \
36            curl -0 --fail -s \
37              https://sts.googleapis.com/v1/token \
38              -H 'Content-Type: text/json; charset=utf-8' \
39              -d @- | \
40            jq -r .access_token)
41
42            # impersonate as the service account and generate an identity token for signing
43            IDENTITY_TOKEN=$(jq -n \
44              '{
45                audience: "sigstore",
46                includeEmail: true
47              }' | \
48            curl -0 --fail -s \
49              https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken \
50              -H "Content-Type: text/json; charset=utf-8" \
51              -H "Authorization: Bearer $STS_TOKEN" \
52              -d @- | \
53            jq -r .token)
54
55            # impersonate as the service account and generate an access token for pushing to AR
56            ACCESS_TOKEN=$(jq -n \
57              '{
58                scope: ["https://www.googleapis.com/auth/cloud-platform"]
59              }' | \
60            curl -0 --fail -s \
61              https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateAccessToken \
62              -H "Content-Type: text/json; charset=utf-8" \
63              -H "Authorization: Bearer $STS_TOKEN" \
64              -d @- | \
65            jq -r .accessToken)
66
67            mkdir -p /go/bin
68            # download ko for building & pushing
69            curl -sL https://github.com/ko-build/ko/releases/download/v0.12.0/ko_0.12.0_Linux_x86_64.tar.gz | tar xzf - ko
70            mv ko /go/bin/
71            # download cosign for signing
72            curl -sLo /go/bin/cosign https://github.com/sigstore/cosign/releases/download/v1.13.1/cosign-linux-amd64
73            chmod +x /go/bin/*
74
75            # use the access token to log in (~/.docker/config.json)
76            ko login us-central1-docker.pkg.dev --username oauth2accesstoken --password "${ACCESS_TOKEN}"
77            # build and push our image
78            ko build --image-refs image.txt .
79
80            # sign our image and push our signature
81            # cosign is actually doing another exchange behind the scenes,
82            # exchanging our GCP identity for a short-lived cert signed by fulcio
83            cosign sign --identity-token "${IDENTITY_TOKEN}" $(cat image.txt)
84
85            echo "=== image ==="
86            cat image.txt            
87
88workflows:
89  main:
90    jobs:
91      - all:
92          context:
93            # the job needs a context attached for CircleCI to set CIRCLE_OIDC_TOKEN
94            # nothing needs to be in the context
95            - empty-context