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:
|
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: './'
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# 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:
|
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
|
```yaml
|
||||||
- uses: 'google-github-actions/auth@v0'
|
- uses: 'google-github-actions/auth@v0'
|
||||||
@ -11,33 +13,33 @@
|
|||||||
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:
|
||||||
@ -49,35 +51,54 @@
|
|||||||
|
|
||||||
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
|
## Subject exceeds the 127 byte limit
|
||||||
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.
|
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 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
|
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
|
with the Google Cloud IAM team][iam-feedback]. The only mitigation is to use
|
||||||
shorter repo names or shorter branch names.
|
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
|
## 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
|
credentials file. This means creating a pull request, compiling a binary, or
|
||||||
building a Docker container, will include said credential file. There are a
|
building a Docker container, will include said credential file. There are a few
|
||||||
few ways to fix this issue:
|
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
|
- 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:
|
||||||
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
18
src/utils.ts
18
src/utils.ts
@ -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';
|
||||||
|
}
|
||||||
|
@ -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'));
|
||||||
|
|
||||||
|
@ -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'));
|
||||||
|
|
||||||
|
@ -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/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user