Skip to content

Commit

Permalink
Merge pull request #1 from buildkite-plugins/initial
Browse files Browse the repository at this point in the history
Google Cloud Workload Identity Federation Buildkite Plugin
  • Loading branch information
steveh authored Apr 27, 2023
2 parents f571ba0 + ccfe5b3 commit 48f2bff
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
steps:
- label: ":shell: Tests"
plugins:
- plugin-tester#v1.0.0: ~

- label: ":sparkles: Lint"
plugins:
- plugin-linter#v3.1.0:
id: gcp-workload-identity-federation

- label: ":shell: Shellcheck"
plugins:
- shellcheck#v1.3.0:
files: hooks/**
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
# gcp-workload-identity-federation-buildkite-plugin
# Google Cloud Workload Identity Federation Buildkite Plugin

A Buildkite plugin to assume a Google Cloud service account using [workload identity federation](https://cloud.google.com/iam/docs/workload-identity-federation).

The plugin requests an OIDC token from Buildkite and uses it to a populate Google Cloud credentials file.

The path to the file is populated in `GOOGLE_APPLICATION_CREDENTIALS` for SDKs that use [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials), and in `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` for the `gcloud` CLI.

## Google Cloud configuration

You should already have a Google Cloud project and a Service Account to assume. See [Google's documentation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers) for more detailed instructions for these steps.

1. Create a [Workload Identity Pool](https://console.cloud.google.com/iam-admin/workload-identity-pools).

We recommend creating a different pool for each security boundary.

In this example we're using `buildkite-example-pipeline`.

2. Add a provider to the pool.

Use OpenID Connect, and give it a name like `buildkite`.

Use `https://agent.buildkite.com` as the Issuer.

Copy the value of the default audience or provide your own.

3. Configure provider attributes.

Because Google limits the length of attributes to 127 characters, we suggest the following mapping:

| Google | OIDC |
| --- | --- |
| `google.subject` | `"organization:" + assertion.sub.split(":")[1] + ":pipeline:" + assertion.sub.split(":")[3]` |
| `attribute.pipeline_slug` | `assertion.pipeline_slug` |
| `attribute.build_branch` | `assertion.build_branch` |

With this mapping you can use a [CEL](https://github.com/google/cel-spec) expression to restrict which pipelines can assume the service account:

```cel
google.subject == "organization:acme:pipeline:buildkite-example-pipeline"
```

4. Grant access to the service account.

5. Configure this plugin using the workload provider audience without the leading `https:`, and the service account email address.

## Example

Add the following to your `pipeline.yml`:

```yml
steps:
- command: |
echo "Credentials are located at \$GOOGLE_APPLICATION_CREDENTIALS"
plugins:
- gcp-workload-identity-federation#v1.0.0:
audience: "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite"
service-account: "[email protected]"
```
## Configuration
### `audience` (Required, string)

- The default audience as shown on the Workload Identity Federation Provider page, without the `https:` prefix, or a custom audience that you configure.

### `service-account` (Required, string)

- The service account for which you want to acquire an access token.

## Developing

To run testing, shellchecks and plugin linting use use `bk run` with the [Buildkite CLI](https://github.com/buildkite/cli).

```bash
bk run
```

Or if you want to run just the tests, you can use the docker [Plugin Tester](https://github.com/buildkite-plugins/buildkite-plugin-tester):

```bash
docker run --rm -ti -v "${PWD}":/plugin buildkite/plugin-tester:latest
```

## Contributing

1. Fork the repo
2. Make the changes
3. Run the tests
4. Commit and push your changes
5. Send a pull request
39 changes: 39 additions & 0 deletions hooks/pre-command
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

set -euo pipefail

if [[ -z "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE:-}" ]]; then
echo "🚨 Missing 'audience' plugin configuration"
exit 1
fi

if [[ -z "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT:-}" ]]; then
echo "🚨 Missing 'service-account' plugin configuration"
exit 1
fi

# Create a temporary directory with both BSD and GNU mktemp
TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'buildkiteXXXX')

echo "~~~ :buildkite: Requesting OIDC token from Buildkite"

buildkite-agent oidc request-token --audience "$BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE" > "$TMPDIR"/token.json

echo "~~~ :gcloud: Configuring Google Cloud credentials"

cat << JSON > "$TMPDIR"/credentials.json
{
"type": "external_account",
"audience": "$BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT:generateAccessToken",
"credential_source": {
"file": "$TMPDIR/token.json"
}
}
JSON

export BUILDKITE_OIDC_TMPDIR=$TMPDIR
export GOOGLE_APPLICATION_CREDENTIALS=$TMPDIR/credentials.json
export CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=$GOOGLE_APPLICATION_CREDENTIALS
9 changes: 9 additions & 0 deletions hooks/pre-exit
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

set -euo pipefail

if [[ -v BUILDKITE_OIDC_TMPDIR ]]; then
rm -rf "$BUILDKITE_OIDC_TMPDIR"

echo "Removed credentials from $BUILDKITE_OIDC_TMPDIR"
fi
17 changes: 17 additions & 0 deletions plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: gcp-workload-identity-federation
description: Grant pipelines access to Google Cloud resources using Workload Identity Federation
author: https://github.com/buildkite-plugins
public: true
requirements:
- bash
- buildkite-agent
configuration:
properties:
audience:
type: string
service-account:
type: string
required:
- audience
- service-account
additionalProperties: false
12 changes: 12 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": [
"config:base",
":disableDependencyDashboard"
],
"docker-compose": {
"digest": {
"enabled": false
}
},
"labels": ["dependencies"]
}
85 changes: 85 additions & 0 deletions tests/pre-command.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bats

# Uncomment to enable stub debug output:
# export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty
# export MKTEMP_STUB_DEBUG=/dev/tty

setup() {
load "$BATS_PLUGIN_PATH/load.bash"
}

@test "fails when mktemp fails" {
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite"
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="[email protected]"

stub mktemp "-d : exit 1"
stub mktemp "-d -t 'buildkiteXXXX' : exit 1"

run "$PWD/hooks/pre-command"

assert_failure
}

@test "fails when audience is missing" {
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="[email protected]"

run "$PWD/hooks/pre-command"

assert_failure
}

@test "fails when service account is missing" {
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite"

run "$PWD/hooks/pre-command"

assert_failure
}

@test "succeeds when mktemp fails once" {
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite"
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="[email protected]"

stub mktemp "-d : exit 1"
stub mktemp "-d -t 'buildkiteXXXX' : echo $BATS_TEST_TMPDIR"
stub buildkite-agent "echo dummy-jwt"

run "$PWD/hooks/pre-command"

assert_success

unstub mktemp
unstub buildkite-agent
}

@test "exports credentials" {
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite"
export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="[email protected]"

stub mktemp "-d : echo $BATS_TEST_TMPDIR"
stub buildkite-agent "oidc request-token --audience //iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite : echo dummy-jwt"

run "$PWD/hooks/pre-command"

assert_success

assert_output --partial "Requesting OIDC token from Buildkite"
assert_output --partial "Configuring Google Cloud credentials"

diff $BATS_TEST_TMPDIR/token.json <(echo dummy-jwt)
diff $BATS_TEST_TMPDIR/credentials.json <(cat << JSON
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken",
"credential_source": {
"file": "$BATS_TEST_TMPDIR/token.json"
}
}
JSON)
unstub mktemp
unstub buildkite-agent
}
23 changes: 23 additions & 0 deletions tests/pre-exit.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bats

setup() {
load "$BATS_PLUGIN_PATH/load.bash"
}

@test "removes tmp directory" {
export BUILDKITE_OIDC_TMPDIR=$BATS_TEST_TMPDIR

run "$PWD/hooks/pre-exit"

assert_success

assert_output --partial "Removed credentials from $BATS_TEST_TMPDIR"
}

@test "does nothing if the directory is not set" {
run "$PWD/hooks/pre-exit"

assert_success

assert_output ""
}

0 comments on commit 48f2bff

Please sign in to comment.