diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1fc148b0..d329b05e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,8 @@ jobs: with: node-version: '16.x' - - name: 'npm ci' - run: 'npm ci' + - name: 'npm build' + run: 'npm ci && npm run build' - name: 'npm lint' run: 'npm run lint' @@ -54,6 +54,9 @@ jobs: with: node-version: '16.x' + - name: 'npm build' + run: 'npm ci && npm run build' + - id: 'auth-default' name: 'auth-default' uses: './' @@ -119,6 +122,9 @@ jobs: with: node-version: '16.x' + - name: 'npm build' + run: 'npm ci && npm run build' + - id: 'auth-default' name: 'auth-default' uses: './' @@ -181,11 +187,8 @@ jobs: with: node-version: '16.x' - - name: 'npm ci' - run: 'npm ci' - - name: 'npm build' - run: 'npm run build' + run: 'npm ci && npm run build' - name: 'auth-default' uses: './' diff --git a/README.md b/README.md index 45001bb1..ddc3ed52 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,15 @@ and permissions on Google Cloud. the checkout step or putting it after `auth` will cause future steps to be unable to authenticate. +- If you plan to create binaries, containers, pull requests, or other + releases, add the following to your `.gitignore` to prevent accidentially + committing credentials to your release artifact: + + ```text + # Ignore generated credentials from google-github-actions/auth + gha-creds-*.json + ``` + ## Usage diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index fc01ef56..20386426 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -1,107 +1,128 @@ # Troubleshooting -- When troubleshooting "permission denied" errors from `auth` for Workload - Identity, the first step is to ask the `auth` plugin to generate an OAuth - access token. Do this by adding `token_format: 'access_token'` to your YAML: - - ```yaml - - uses: 'google-github-actions/auth@v0' - with: - # ... - token_format: 'access_token' +## Permission denied + +When troubleshooting "permission denied" errors from `auth` for Workload +Identity, the first step is to ask the `auth` plugin to generate an OAuth access +token. Do this by adding `token_format: 'access_token'` to your YAML: + +```yaml +- uses: 'google-github-actions/auth@v0' + with: + # ... + token_format: 'access_token' +``` + +If your workflow _succeeds_ after adding the step to generate an access token, +it means Workload Identity Federation is configured correctly and the issue is +in subsequent actions. You can remove the `token_format` from your YAML. To +further debug: + +1. Look at the [debug logs][debug-logs] to see exactly which step is failing. + Ensure you are using the latest version of that GitHub Action. + +1. Make sure you use `actions/checkout@v2` **before** the `auth` action in your + workflow. + +1. If the failing action is from `google-github-action/*`, please file an issue + in the corresponding repository. + +1. If the failing action is from an external action, please file an issue + against that repository. The `auth` action exports Google Application + Default Credentials (ADC). Ask the action author to ensure they are + processing ADC correctly and using the latest versions of the Google client + libraries. Please note that we do not have control over actions outside of + `google-github-actions`. + +If your workflow _fails_ after adding the the step to generate an access token, +it likely means there is a misconfiguration with Workload Identity. Here are +some common sources of errors: + +1. Look at the [debug logs][debug-logs] to see exactly which step is failing. + Ensure you are using the latest version of that GitHub Action. + +1. Ensure the value for `workload_identity_provider` is the full _Provider_ + name, **not** the _Pool_ name: + + ```diff + - projects/NUMBER/locations/global/workloadIdentityPools/POOL + + projects/NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER ``` - If your workflow _succeeds_ after adding the step to generate an access - token, it means Workload Identity Federation is configured correctly and the - issue is in subsequent actions. You can remove the `token_format` from your - YAML. To further debug: - - 1. Look at the [debug logs][debug-logs] to see exactly which step is - failing. Ensure you are using the latest version of that GitHub Action. - - 1. Make sure you use `actions/checkout@v2` **before** the `auth` action in - your workflow. - - 1. If the failing action is from `google-github-action/*`, please file an - issue in the corresponding repository. - - 1. If the failing action is from an external action, please file an issue - against that repository. The `auth` action exports Google Application - Default Credentials (ADC). Ask the action author to ensure they are - processing ADC correctly and using the latest versions of the Google - client libraries. Please note that we do not have control over actions - outside of `google-github-actions`. - - If your workflow _fails_ after adding the the step to generate an access - token, it likely means there is a misconfiguration with Workload Identity. - Here are some common sources of errors: - - 1. Look at the [debug logs][debug-logs] to see exactly which step is - failing. Ensure you are using the latest version of that GitHub Action. - - 1. Ensure the value for `workload_identity_provider` is the full _Provider_ - name, **not** the _Pool_ name: - - ```diff - - projects/NUMBER/locations/global/workloadIdentityPools/POOL - + projects/NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER - ``` - - 1. Ensure you have created an **Attribute Mapping** for any **Attribute - Conditions** or **Service Account Impersonation** principals. You cannot - create an Attribute Condition unless you map that value from the - incoming GitHub OIDC token. You cannot grant permissions to impersonate - a Service Account on an attribute unless you map that value from the - incoming GitHub OIDC token. - - 1. Ensure you have waited at least 5 minutes between making changes to the - Workload Identity Pool and Workload Identity Provider. Changes to these - resources are eventually consistent. - -- "The size of mapped attribute exceeds the 127 bytes limit." This error - indicates that the GitHub OIDC token had a claim that exceeded the maximum - allowed value of 127 bytes. In general, 1 byte = 1 character. This most - common reason this occurs is due to long repo names or long branch names. - - **This is a limit imposed by Google Cloud IAM.** We have no control over - this value. It is documented [here][wif-byte-limit]. Please [file feedback - with the Google Cloud IAM team][iam-feedback]. The only mitigation is to use - shorter repo names or shorter branch names. - -- The credentials file was bundled into my binary, container, or pull request! - By default, the `auth` action exports credentials to the current workspace - so that the credentials are automatically available to future steps and - Docker-based actions. The credentials file is automatically removed when the - job finishes. - - This means, after `auth` completes, the workspace is dirty and contains a - credentials file. This means creating a pull request, compiling a binary, or - building a Docker container, will include said credential file. There are a - few ways to fix this issue: - - - Re-order your steps. In most cases, you can re-order your steps such - that `auth` comes _after_ the "compilation" step: - - ```text - 1. Checkout - 2. Compile (e.g. "docker build", "go build", "git add") - 3. Auth - 4. Push - ``` - - This ensures that no authentication data is present during artifact - creation. - - - In situations where `auth` must occur before compilation, you can use - the output to exclude the credential: - - ```text - 1. Checkout - 2. Auth - 3. Inject "${{ steps.auth.outputs.credentials_file_path }}" into ignore file (e.g. .gitignore, .dockerignore) - 4. Compile (e.g. "docker build", "go build", "git add") - 5. Push - ``` +1. Ensure you have created an **Attribute Mapping** for any **Attribute + Conditions** or **Service Account Impersonation** principals. You cannot + create an Attribute Condition unless you map that value from the incoming + GitHub OIDC token. You cannot grant permissions to impersonate a Service + Account on an attribute unless you map that value from the incoming GitHub + OIDC token. + +1. Ensure you have waited at least 5 minutes between making changes to the + Workload Identity Pool and Workload Identity Provider. Changes to these + resources are eventually consistent. + + +## Subject exceeds the 127 byte limit + +If you get an error like: + +```text +The size of mapped attribute exceeds the 127 bytes limit. +``` + +it means that the GitHub OIDC token had a claim that exceeded the maximum +allowed value of 127 bytes. In general, 1 byte = 1 character. This most common +reason this occurs is due to long repo names or long branch names. + +**This is a limit imposed by Google Cloud IAM.** We have no control over +this value. It is documented [here][wif-byte-limit]. Please [file feedback +with the Google Cloud IAM team][iam-feedback]. The only mitigation is to use +shorter repo names or shorter branch names. + + +## Dirty git or bundled credentials + +By default, the `auth` action exports credentials to the current workspace so +that the credentials are automatically available to future steps and +Docker-based actions. The credentials file is automatically removed when the job +finishes. + +This means, after the `auth` action runs, the workspace is dirty and contains a +credentials file. This means creating a pull request, compiling a binary, or +building a Docker container, will include said credential file. There are a few +ways to fix this issue: + +- Add and commit the following lines to your `.gitignore`: + + ```text + # Ignore generated credentials from google-github-actions/auth + gha-creds-*.json + ``` + + **This requires the `auth` action be v0.6.0 or later.** + +- Re-order your steps. In most cases, you can re-order your steps such + that `auth` comes _after_ the "compilation" step: + + ```text + 1. Checkout + 2. Compile (e.g. "docker build", "go build", "git add") + 3. Auth + 4. Push + ``` + + This ensures that no authentication data is present during artifact + creation. + +- In situations where `auth` must occur before compilation, you can use + the output to exclude the credential: + + ```text + 1. Checkout + 2. Auth + 3. Inject "${{ steps.auth.outputs.credentials_file_path }}" into ignore file (e.g. .gitignore, .dockerignore) + 4. Compile (e.g. "docker build", "go build", "git add") + 5. Push + ``` [debug-logs]: https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging [iam-feedback]: https://cloud.google.com/iam/docs/getting-support diff --git a/src/client/credentials_json_client.ts b/src/client/credentials_json_client.ts index 985686d8..db75a066 100644 --- a/src/client/credentials_json_client.ts +++ b/src/client/credentials_json_client.ts @@ -4,7 +4,6 @@ import { createSign } from 'crypto'; import { isServiceAccountKey, parseCredential, - randomFilepath, ServiceAccountKey, toBase64, writeSecureFile, @@ -124,8 +123,7 @@ export class CredentialsJSONClient implements AuthClient { * createCredentialsFile creates a Google Cloud credentials file that can be * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. */ - async createCredentialsFile(outputDir: string): Promise { - const outputFile = randomFilepath(outputDir); - return await writeSecureFile(outputFile, JSON.stringify(this.#credentials)); + async createCredentialsFile(outputPath: string): Promise { + return await writeSecureFile(outputPath, JSON.stringify(this.#credentials)); } } diff --git a/src/client/workload_identity_client.ts b/src/client/workload_identity_client.ts index ec9374dd..7aa24053 100644 --- a/src/client/workload_identity_client.ts +++ b/src/client/workload_identity_client.ts @@ -1,7 +1,7 @@ 'use strict'; import { URL } from 'url'; -import { randomFilepath, writeSecureFile } from '@google-github-actions/actions-utils'; +import { writeSecureFile } from '@google-github-actions/actions-utils'; import { AuthClient } from './auth_client'; import { BaseClient } from '../base'; @@ -182,7 +182,7 @@ export class WorkloadIdentityClient implements AuthClient { * createCredentialsFile creates a Google Cloud credentials file that can be * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. */ - async createCredentialsFile(outputDir: string): Promise { + async createCredentialsFile(outputPath: string): Promise { const requestURL = new URL(this.#oidcTokenRequestURL); // Append the audience value to the request. @@ -209,7 +209,6 @@ export class WorkloadIdentityClient implements AuthClient { }, }; - const outputFile = randomFilepath(outputDir); - return await writeSecureFile(outputFile, JSON.stringify(data)); + return await writeSecureFile(outputPath, JSON.stringify(data)); } } diff --git a/src/main.ts b/src/main.ts index 6b27813b..37c6ac84 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,7 @@ 'use strict'; +import { join as pathjoin } from 'path'; + import { debug as logDebug, exportVariable, @@ -26,7 +28,7 @@ import { WorkloadIdentityClient } from './client/workload_identity_client'; import { CredentialsJSONClient } from './client/credentials_json_client'; import { AuthClient } from './client/auth_client'; import { BaseClient } from './base'; -import { buildDomainWideDelegationJWT } from './utils'; +import { buildDomainWideDelegationJWT, generateCredentialsFilename } from './utils'; const secretsWarning = `If you are specifying input values via GitHub secrets, ensure the secret ` + @@ -153,7 +155,9 @@ async function run(): Promise { } // Create credentials file. - const credentialsPath = await client.createCredentialsFile(githubWorkspace); + const outputFile = generateCredentialsFilename(); + const outputPath = pathjoin(githubWorkspace, outputFile); + const credentialsPath = await client.createCredentialsFile(outputPath); logInfo(`Created credentials file at "${credentialsPath}"`); // Output to be available to future steps. diff --git a/src/utils.ts b/src/utils.ts index 49163c1c..b1ec4676 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ 'use strict'; +import { randomFilename } from '@google-github-actions/actions-utils'; + /** * buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a * DWD exchange. The JWT must be signed and then exchanged with the OAuth @@ -35,3 +37,19 @@ export function buildDomainWideDelegationJWT( return JSON.stringify(body); } + +/** + * generateCredentialsFilename creates a predictable filename under which + * credentials are written. This string is the filename, not the filepath. It must match the format: + * + * gha-creds-[a-z0-9]{16}.json + * + * For example: + * + * gha-creds-ef801c3bb35b52e5.json + * + * @return Filename + */ +export function generateCredentialsFilename(): string { + return 'gha-creds-' + randomFilename(8) + '.json'; +} diff --git a/tests/client/credentials_json_client.test.ts b/tests/client/credentials_json_client.test.ts index 85cfe976..79f41df0 100644 --- a/tests/client/credentials_json_client.test.ts +++ b/tests/client/credentials_json_client.test.ts @@ -3,9 +3,12 @@ import 'mocha'; import { expect } from 'chai'; +import { join as pathjoin } from 'path'; import { readFileSync } from 'fs'; import { tmpdir } from 'os'; +import { randomFilename } from '@google-github-actions/actions-utils'; + import { CredentialsJSONClient } from '../../src/client/credentials_json_client'; // Yes, this is a real private key. No, it's not valid for authenticating @@ -104,14 +107,14 @@ describe('CredentialsJSONClient', () => { describe('#createCredentialsFile', () => { it('writes the file', async () => { - const tmp = tmpdir(); + const outputFile = pathjoin(tmpdir(), randomFilename()); const client = new CredentialsJSONClient({ credentialsJSON: credentialsJSON, }); const exp = JSON.parse(credentialsJSON); - const pth = await client.createCredentialsFile(tmp); + const pth = await client.createCredentialsFile(outputFile); const data = readFileSync(pth); const got = JSON.parse(data.toString('utf8')); diff --git a/tests/client/workload_identity_client.test.ts b/tests/client/workload_identity_client.test.ts index 075f70b0..d1e80210 100644 --- a/tests/client/workload_identity_client.test.ts +++ b/tests/client/workload_identity_client.test.ts @@ -4,7 +4,11 @@ import 'mocha'; import { expect } from 'chai'; import { tmpdir } from 'os'; +import { join as pathjoin } from 'path'; import { readFileSync } from 'fs'; + +import { randomFilename } from '@google-github-actions/actions-utils'; + import { WorkloadIdentityClient } from '../../src/client/workload_identity_client'; describe('WorkloadIdentityClient', () => { @@ -71,7 +75,7 @@ describe('WorkloadIdentityClient', () => { describe('#createCredentialsFile', () => { it('writes the file', async () => { - const tmp = tmpdir(); + const outputFile = pathjoin(tmpdir(), randomFilename()); const client = new WorkloadIdentityClient({ projectID: 'my-project', providerID: 'my-provider', @@ -101,7 +105,7 @@ describe('WorkloadIdentityClient', () => { type: 'external_account', }; - const pth = await client.createCredentialsFile(tmp); + const pth = await client.createCredentialsFile(outputFile); const data = readFileSync(pth); const got = JSON.parse(data.toString('utf8')); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index fff25157..45e4f211 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -3,7 +3,7 @@ import 'mocha'; import { expect } from 'chai'; -import { buildDomainWideDelegationJWT } from '../src/utils'; +import { buildDomainWideDelegationJWT, generateCredentialsFilename } from '../src/utils'; describe('Utils', () => { describe('#buildDomainWideDelegationJWT', () => { @@ -54,4 +54,13 @@ describe('Utils', () => { }); }); }); + + describe('#generateCredentialsFilename', () => { + it('returns a string matching the regex', () => { + for (let i = 0; i < 10; i++) { + const filename = generateCredentialsFilename(); + expect(filename).to.match(/gha-creds-[0-9a-z]{16}\.json/); + } + }); + }); });