feat: ensure cred file is created with a predictable name (#130)
This commit is contained in:
parent
3b7fb59565
commit
48c46e6a59
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@ -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: './'
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,45 +1,47 @@
|
||||
# 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:
|
||||
## Permission denied
|
||||
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v0'
|
||||
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:
|
||||
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. 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. 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 `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
|
||||
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`.
|
||||
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:
|
||||
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. 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_
|
||||
1. Ensure the value for `workload_identity_provider` is the full _Provider_
|
||||
name, **not** the _Pool_ name:
|
||||
|
||||
```diff
|
||||
@ -47,39 +49,58 @@
|
||||
+ 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
|
||||
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.
|
||||
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
|
||||
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.
|
||||
## Subject exceeds the 127 byte limit
|
||||
|
||||
- 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.
|
||||
If you get an error like:
|
||||
|
||||
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:
|
||||
```text
|
||||
The size of mapped attribute exceeds the 127 bytes limit.
|
||||
```
|
||||
|
||||
- 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:
|
||||
|
||||
```text
|
||||
@ -92,7 +113,7 @@
|
||||
This ensures that no authentication data is present during artifact
|
||||
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:
|
||||
|
||||
```text
|
||||
|
@ -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<string> {
|
||||
const outputFile = randomFilepath(outputDir);
|
||||
return await writeSecureFile(outputFile, JSON.stringify(this.#credentials));
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
return await writeSecureFile(outputPath, JSON.stringify(this.#credentials));
|
||||
}
|
||||
}
|
||||
|
@ -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<string> {
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -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<void> {
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
18
src/utils.ts
18
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';
|
||||
}
|
||||
|
@ -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'));
|
||||
|
||||
|
@ -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'));
|
||||
|
||||
|
@ -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/);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user