feat: ensure cred file is created with a predictable name (#130)

This commit is contained in:
Seth Vargo 2022-02-03 12:57:50 -05:00 committed by GitHub
parent 3b7fb59565
commit 48c46e6a59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 101 deletions

View File

@ -25,8 +25,8 @@ jobs:
with: with:
node-version: '16.x' node-version: '16.x'
- name: 'npm ci' - name: 'npm build'
run: 'npm ci' run: 'npm ci && npm run build'
- name: 'npm lint' - name: 'npm lint'
run: 'npm run lint' run: 'npm run lint'
@ -54,6 +54,9 @@ jobs:
with: with:
node-version: '16.x' node-version: '16.x'
- name: 'npm build'
run: 'npm ci && npm run build'
- id: 'auth-default' - id: 'auth-default'
name: 'auth-default' name: 'auth-default'
uses: './' uses: './'
@ -119,6 +122,9 @@ jobs:
with: with:
node-version: '16.x' node-version: '16.x'
- name: 'npm build'
run: 'npm ci && npm run build'
- id: 'auth-default' - id: 'auth-default'
name: 'auth-default' name: 'auth-default'
uses: './' uses: './'
@ -181,11 +187,8 @@ jobs:
with: with:
node-version: '16.x' node-version: '16.x'
- name: 'npm ci'
run: 'npm ci'
- name: 'npm build' - name: 'npm build'
run: 'npm run build' run: 'npm ci && npm run build'
- name: 'auth-default' - name: 'auth-default'
uses: './' uses: './'

View File

@ -36,6 +36,15 @@ and permissions on Google Cloud.
the checkout step or putting it after `auth` will cause future steps to be the checkout step or putting it after `auth` will cause future steps to be
unable to authenticate. 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 ## Usage

View File

@ -1,45 +1,47 @@
# Troubleshooting # Troubleshooting
- When troubleshooting "permission denied" errors from `auth` for Workload ## Permission denied
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 When troubleshooting "permission denied" errors from `auth` for Workload
- uses: 'google-github-actions/auth@v0' 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: with:
# ... # ...
token_format: 'access_token' token_format: 'access_token'
``` ```
If your workflow _succeeds_ after adding the step to generate an access If your workflow _succeeds_ after adding the step to generate an access token,
token, it means Workload Identity Federation is configured correctly and the it means Workload Identity Federation is configured correctly and the issue is
issue is in subsequent actions. You can remove the `token_format` from your in subsequent actions. You can remove the `token_format` from your YAML. To
YAML. To further debug: further debug:
1. Look at the [debug logs][debug-logs] to see exactly which step is 1. Look at the [debug logs][debug-logs] to see exactly which step is failing.
failing. Ensure you are using the latest version of that GitHub Action. Ensure you are using the latest version of that GitHub Action.
1. Make sure you use `actions/checkout@v2` **before** the `auth` action in 1. Make sure you use `actions/checkout@v2` **before** the `auth` action in your
your workflow. workflow.
1. If the failing action is from `google-github-action/*`, please file an 1. If the failing action is from `google-github-action/*`, please file an issue
issue in the corresponding repository. in the corresponding repository.
1. If the failing action is from an external action, please file an issue 1. If the failing action is from an external action, please file an issue
against that repository. The `auth` action exports Google Application against that repository. The `auth` action exports Google Application
Default Credentials (ADC). Ask the action author to ensure they are Default Credentials (ADC). Ask the action author to ensure they are
processing ADC correctly and using the latest versions of the Google processing ADC correctly and using the latest versions of the Google client
client libraries. Please note that we do not have control over actions libraries. Please note that we do not have control over actions outside of
outside of `google-github-actions`. `google-github-actions`.
If your workflow _fails_ after adding the the step to generate an access If your workflow _fails_ after adding the the step to generate an access token,
token, it likely means there is a misconfiguration with Workload Identity. it likely means there is a misconfiguration with Workload Identity. Here are
Here are some common sources of errors: some common sources of errors:
1. Look at the [debug logs][debug-logs] to see exactly which step is 1. Look at the [debug logs][debug-logs] to see exactly which step is failing.
failing. Ensure you are using the latest version of that GitHub Action. Ensure you are using the latest version of that GitHub Action.
1. Ensure the value for `workload_identity_provider` is the full _Provider_ 1. Ensure the value for `workload_identity_provider` is the full _Provider_
name, **not** the _Pool_ name: name, **not** the _Pool_ name:
```diff ```diff
@ -47,39 +49,58 @@
+ projects/NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER + projects/NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
``` ```
1. Ensure you have created an **Attribute Mapping** for any **Attribute 1. Ensure you have created an **Attribute Mapping** for any **Attribute
Conditions** or **Service Account Impersonation** principals. You cannot Conditions** or **Service Account Impersonation** principals. You cannot
create an Attribute Condition unless you map that value from the create an Attribute Condition unless you map that value from the incoming
incoming GitHub OIDC token. You cannot grant permissions to impersonate GitHub OIDC token. You cannot grant permissions to impersonate a Service
a Service Account on an attribute unless you map that value from the Account on an attribute unless you map that value from the incoming GitHub
incoming GitHub OIDC token. OIDC token.
1. Ensure you have waited at least 5 minutes between making changes to the 1. Ensure you have waited at least 5 minutes between making changes to the
Workload Identity Pool and Workload Identity Provider. Changes to these Workload Identity Pool and Workload Identity Provider. Changes to these
resources are eventually consistent. 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 ## Subject exceeds the 127 byte limit
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! If you get an error like:
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 ```text
credentials file. This means creating a pull request, compiling a binary, or The size of mapped attribute exceeds the 127 bytes limit.
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 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: that `auth` comes _after_ the "compilation" step:
```text ```text
@ -92,7 +113,7 @@
This ensures that no authentication data is present during artifact This ensures that no authentication data is present during artifact
creation. creation.
- In situations where `auth` must occur before compilation, you can use - In situations where `auth` must occur before compilation, you can use
the output to exclude the credential: the output to exclude the credential:
```text ```text

View File

@ -4,7 +4,6 @@ import { createSign } from 'crypto';
import { import {
isServiceAccountKey, isServiceAccountKey,
parseCredential, parseCredential,
randomFilepath,
ServiceAccountKey, ServiceAccountKey,
toBase64, toBase64,
writeSecureFile, writeSecureFile,
@ -124,8 +123,7 @@ export class CredentialsJSONClient implements AuthClient {
* createCredentialsFile creates a Google Cloud credentials file that can be * createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/ */
async createCredentialsFile(outputDir: string): Promise<string> { async createCredentialsFile(outputPath: string): Promise<string> {
const outputFile = randomFilepath(outputDir); return await writeSecureFile(outputPath, JSON.stringify(this.#credentials));
return await writeSecureFile(outputFile, JSON.stringify(this.#credentials));
} }
} }

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { URL } from 'url'; 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 { AuthClient } from './auth_client';
import { BaseClient } from '../base'; import { BaseClient } from '../base';
@ -182,7 +182,7 @@ export class WorkloadIdentityClient implements AuthClient {
* createCredentialsFile creates a Google Cloud credentials file that can be * createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/ */
async createCredentialsFile(outputDir: string): Promise<string> { async createCredentialsFile(outputPath: string): Promise<string> {
const requestURL = new URL(this.#oidcTokenRequestURL); const requestURL = new URL(this.#oidcTokenRequestURL);
// Append the audience value to the request. // Append the audience value to the request.
@ -209,7 +209,6 @@ export class WorkloadIdentityClient implements AuthClient {
}, },
}; };
const outputFile = randomFilepath(outputDir); return await writeSecureFile(outputPath, JSON.stringify(data));
return await writeSecureFile(outputFile, JSON.stringify(data));
} }
} }

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import { join as pathjoin } from 'path';
import { import {
debug as logDebug, debug as logDebug,
exportVariable, exportVariable,
@ -26,7 +28,7 @@ import { WorkloadIdentityClient } from './client/workload_identity_client';
import { CredentialsJSONClient } from './client/credentials_json_client'; import { CredentialsJSONClient } from './client/credentials_json_client';
import { AuthClient } from './client/auth_client'; import { AuthClient } from './client/auth_client';
import { BaseClient } from './base'; import { BaseClient } from './base';
import { buildDomainWideDelegationJWT } from './utils'; import { buildDomainWideDelegationJWT, generateCredentialsFilename } from './utils';
const secretsWarning = const secretsWarning =
`If you are specifying input values via GitHub secrets, ensure the secret ` + `If you are specifying input values via GitHub secrets, ensure the secret ` +
@ -153,7 +155,9 @@ async function run(): Promise<void> {
} }
// Create credentials file. // 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}"`); logInfo(`Created credentials file at "${credentialsPath}"`);
// Output to be available to future steps. // Output to be available to future steps.

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import { randomFilename } from '@google-github-actions/actions-utils';
/** /**
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a * buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
* DWD exchange. The JWT must be signed and then exchanged with the OAuth * DWD exchange. The JWT must be signed and then exchanged with the OAuth
@ -35,3 +37,19 @@ export function buildDomainWideDelegationJWT(
return JSON.stringify(body); 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';
}

View File

@ -3,9 +3,12 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { join as pathjoin } from 'path';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { randomFilename } from '@google-github-actions/actions-utils';
import { CredentialsJSONClient } from '../../src/client/credentials_json_client'; import { CredentialsJSONClient } from '../../src/client/credentials_json_client';
// Yes, this is a real private key. No, it's not valid for authenticating // Yes, this is a real private key. No, it's not valid for authenticating
@ -104,14 +107,14 @@ describe('CredentialsJSONClient', () => {
describe('#createCredentialsFile', () => { describe('#createCredentialsFile', () => {
it('writes the file', async () => { it('writes the file', async () => {
const tmp = tmpdir(); const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new CredentialsJSONClient({ const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON, credentialsJSON: credentialsJSON,
}); });
const exp = JSON.parse(credentialsJSON); const exp = JSON.parse(credentialsJSON);
const pth = await client.createCredentialsFile(tmp); const pth = await client.createCredentialsFile(outputFile);
const data = readFileSync(pth); const data = readFileSync(pth);
const got = JSON.parse(data.toString('utf8')); const got = JSON.parse(data.toString('utf8'));

View File

@ -4,7 +4,11 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { join as pathjoin } from 'path';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { randomFilename } from '@google-github-actions/actions-utils';
import { WorkloadIdentityClient } from '../../src/client/workload_identity_client'; import { WorkloadIdentityClient } from '../../src/client/workload_identity_client';
describe('WorkloadIdentityClient', () => { describe('WorkloadIdentityClient', () => {
@ -71,7 +75,7 @@ describe('WorkloadIdentityClient', () => {
describe('#createCredentialsFile', () => { describe('#createCredentialsFile', () => {
it('writes the file', async () => { it('writes the file', async () => {
const tmp = tmpdir(); const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityClient({ const client = new WorkloadIdentityClient({
projectID: 'my-project', projectID: 'my-project',
providerID: 'my-provider', providerID: 'my-provider',
@ -101,7 +105,7 @@ describe('WorkloadIdentityClient', () => {
type: 'external_account', type: 'external_account',
}; };
const pth = await client.createCredentialsFile(tmp); const pth = await client.createCredentialsFile(outputFile);
const data = readFileSync(pth); const data = readFileSync(pth);
const got = JSON.parse(data.toString('utf8')); const got = JSON.parse(data.toString('utf8'));

View File

@ -3,7 +3,7 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { buildDomainWideDelegationJWT } from '../src/utils'; import { buildDomainWideDelegationJWT, generateCredentialsFilename } from '../src/utils';
describe('Utils', () => { describe('Utils', () => {
describe('#buildDomainWideDelegationJWT', () => { 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/);
}
});
});
}); });