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