mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-07 15:16:56 +00:00
feat(): add support for github provided jwt auth (#257)
* fix: update `privateKeyRaw` condition * fix: add `contents: read` permission * fix: get token via `@actions/core` - Update README - Switch to use `getIDToken` method for Github token retrieval - Bump `@actions/core` to 1.6.0 - Add `jwtGithubAudience` input - Remove unnecessary code * fix: add description for `jwtGithubAudience` * fix: move default value for `jwtGithubAudience` to `action.yml` * docs: fix typo in README & grammar * test: add tests * fix: reset `dist/index.js` * fix: remove default value for `jwtGithubAudience` from `action.yml` * fix: reset `dist/index.js` * fix: reset `dist/index.js`
This commit is contained in:
parent
b8c90c7243
commit
c502100fbe
7 changed files with 34680 additions and 14979 deletions
22
README.md
22
README.md
|
|
@ -86,7 +86,28 @@ with:
|
||||||
githubToken: ${{ secrets.MY_GITHUB_TOKEN }}
|
githubToken: ${{ secrets.MY_GITHUB_TOKEN }}
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
caCertificate: ${{ secrets.VAULTCA }}
|
||||||
```
|
```
|
||||||
|
- **jwt**: (Github OIDC) you must provide a `role` parameter, additionally you can pass `jwtGithubAudience` parameter.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
method: jwt
|
||||||
|
role: github-action
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notice:** For Github provided OIDC token to work, the workflow should have `id-token: write` & `contents: read` specified in the `permissions` section of the workflow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
- **jwt**: you must provide a `role` & `jwtPrivateKey` parameters, additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
|
- **jwt**: you must provide a `role` & `jwtPrivateKey` parameters, additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
...
|
||||||
with:
|
with:
|
||||||
|
|
@ -278,6 +299,7 @@ Here are all the inputs available through `with`:
|
||||||
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
|
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
|
||||||
| `jwtPrivateKey` | Base64 encoded Private key to sign JWT | | |
|
| `jwtPrivateKey` | Base64 encoded Private key to sign JWT | | |
|
||||||
| `jwtKeyPassword` | Password for key stored in jwtPrivateKey (if needed) | | |
|
| `jwtKeyPassword` | Password for key stored in jwtPrivateKey (if needed) | | |
|
||||||
|
| `jwtGithubAudience` | Identifies the recipient ("aud" claim) that the JWT is intended for |`sigstore`| |
|
||||||
| `jwtTtl` | Time in seconds, after which token expires | | 3600 |
|
| `jwtTtl` | Time in seconds, after which token expires | | 3600 |
|
||||||
| `kubernetesTokenPath` | The path to the service-account secret with the jwt token for kubernetes based authentication |`/var/run/secrets/kubernetes.io/serviceaccount/token` | |
|
| `kubernetesTokenPath` | The path to the service-account secret with the jwt token for kubernetes based authentication |`/var/run/secrets/kubernetes.io/serviceaccount/token` | |
|
||||||
| `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | |
|
| `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | |
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ inputs:
|
||||||
jwtKeyPassword:
|
jwtKeyPassword:
|
||||||
description: 'Password for key stored in jwtPrivateKey (if needed)'
|
description: 'Password for key stored in jwtPrivateKey (if needed)'
|
||||||
required: false
|
required: false
|
||||||
|
jwtGithubAudience:
|
||||||
|
description: 'Identifies the recipient ("aud" claim) that the JWT is intended for'
|
||||||
|
required: false
|
||||||
jwtTtl:
|
jwtTtl:
|
||||||
description: 'Time in seconds, after which token expires'
|
description: 'Time in seconds, after which token expires'
|
||||||
required: false
|
required: false
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
jest.mock('@actions/core');
|
jest.mock('@actions/core');
|
||||||
jest.mock('@actions/core/lib/command');
|
jest.mock('@actions/core/lib/command');
|
||||||
const core = require('@actions/core');
|
const core = require('@actions/core');
|
||||||
|
const rsasign = require('jsrsasign');
|
||||||
const {
|
const {
|
||||||
|
privateRsaKey,
|
||||||
privateRsaKeyBase64,
|
privateRsaKeyBase64,
|
||||||
publicRsaKey
|
publicRsaKey
|
||||||
} = require('./rsa_keys');
|
} = require('./rsa_keys');
|
||||||
|
|
@ -13,6 +15,42 @@ const { exportSecrets } = require('../../src/action');
|
||||||
|
|
||||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Github OIDC response mock
|
||||||
|
* @param {string} aud Audience claim
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action") {
|
||||||
|
const alg = 'RS256';
|
||||||
|
const header = { alg: alg, typ: 'JWT' };
|
||||||
|
const now = rsasign.KJUR.jws.IntDate.getNow();
|
||||||
|
const payload = {
|
||||||
|
jti: "unique-id",
|
||||||
|
sub: "repo:hashicorp/vault-action:ref:refs/heads/master",
|
||||||
|
aud,
|
||||||
|
ref: "refs/heads/master",
|
||||||
|
sha: "commit-sha",
|
||||||
|
repository: "hashicorp/vault-action",
|
||||||
|
repository_owner: "hashicorp",
|
||||||
|
run_id: "1",
|
||||||
|
run_number: "1",
|
||||||
|
run_attempt: "1",
|
||||||
|
actor: "github-username",
|
||||||
|
workflow: "Workflow Name",
|
||||||
|
head_ref: "",
|
||||||
|
base_ref: "",
|
||||||
|
event_name: "push",
|
||||||
|
ref_type: "branch",
|
||||||
|
job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/master",
|
||||||
|
iss: 'vault-action',
|
||||||
|
iat: now,
|
||||||
|
nbf: now,
|
||||||
|
exp: now + 3600,
|
||||||
|
};
|
||||||
|
const decryptedKey = rsasign.KEYUTIL.getKey(privateRsaKey);
|
||||||
|
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
|
||||||
|
}
|
||||||
|
|
||||||
describe('jwt auth', () => {
|
describe('jwt auth', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Verify Connection
|
// Verify Connection
|
||||||
|
|
@ -94,33 +132,107 @@ describe('jwt auth', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('authenticate with private key', () => {
|
||||||
jest.resetAllMocks();
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('url')
|
.calledWith('url')
|
||||||
.mockReturnValueOnce(`${vaultUrl}`);
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('method')
|
.calledWith('method')
|
||||||
.mockReturnValueOnce('jwt');
|
.mockReturnValueOnce('jwt');
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('jwtPrivateKey')
|
.calledWith('jwtPrivateKey')
|
||||||
.mockReturnValueOnce(privateRsaKeyBase64);
|
.mockReturnValueOnce(privateRsaKeyBase64);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('role')
|
.calledWith('role')
|
||||||
.mockReturnValueOnce('default');
|
.mockReturnValueOnce('default');
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('secrets')
|
.calledWith('secrets')
|
||||||
.mockReturnValueOnce('secret/data/test secret');
|
.mockReturnValueOnce('secret/data/test secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates', async () => {
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully authenticates', async () => {
|
describe('authenticate with Github OIDC', () => {
|
||||||
await exportSecrets();
|
beforeAll(async () => {
|
||||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': 'testtoken',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
role_type: 'jwt',
|
||||||
|
bound_audiences: null,
|
||||||
|
bound_claims: {
|
||||||
|
iss: 'vault-action',
|
||||||
|
aud: 'sigstore',
|
||||||
|
},
|
||||||
|
user_claim: 'iss',
|
||||||
|
policies: ['reader']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url')
|
||||||
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('method')
|
||||||
|
.mockReturnValueOnce('jwt');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('jwtPrivateKey')
|
||||||
|
.mockReturnValueOnce('');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role')
|
||||||
|
.mockReturnValueOnce('default');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets')
|
||||||
|
.mockReturnValueOnce('secret/data/test secret');
|
||||||
|
|
||||||
|
when(core.getIDToken)
|
||||||
|
.calledWith()
|
||||||
|
.mockReturnValueOnce(mockGithubOIDCResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates', async () => {
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role')
|
||||||
|
.mockReturnValueOnce('default-sigstore');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('jwtGithubAudience')
|
||||||
|
.mockReturnValueOnce('sigstore');
|
||||||
|
|
||||||
|
when(core.getIDToken)
|
||||||
|
.calledWith()
|
||||||
|
.mockReturnValueOnce(mockGithubOIDCResponse('sigstore'));
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ f52E9W2iFNt3sxB0KFtOkbkCAwEAAQ==
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
privateRsaKey,
|
||||||
privateRsaKeyBase64,
|
privateRsaKeyBase64,
|
||||||
publicRsaKey
|
publicRsaKey
|
||||||
};
|
};
|
||||||
|
|
|
||||||
49466
package-lock.json
generated
49466
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -52,7 +52,7 @@
|
||||||
"@actions/core": ">=1 <2"
|
"@actions/core": ">=1 <2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/core": "^1.2.3",
|
"@actions/core": "^1.6.0",
|
||||||
"@types/got": "^9.6.11",
|
"@types/got": "^9.6.11",
|
||||||
"@types/jest": "^26.0.13",
|
"@types/jest": "^26.0.13",
|
||||||
"@zeit/ncc": "^0.22.3",
|
"@zeit/ncc": "^0.22.3",
|
||||||
|
|
|
||||||
13
src/auth.js
13
src/auth.js
|
|
@ -23,12 +23,21 @@ async function retrieveToken(method, client) {
|
||||||
return await getClientToken(client, method, path, { token: githubToken });
|
return await getClientToken(client, method, path, { token: githubToken });
|
||||||
}
|
}
|
||||||
case 'jwt': {
|
case 'jwt': {
|
||||||
|
/** @type {string} */
|
||||||
|
let jwt;
|
||||||
const role = core.getInput('role', { required: true });
|
const role = core.getInput('role', { required: true });
|
||||||
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: true });
|
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false });
|
||||||
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
|
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
|
||||||
const keyPassword = core.getInput('jwtKeyPassword', { required: false });
|
const keyPassword = core.getInput('jwtKeyPassword', { required: false });
|
||||||
const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour
|
const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour
|
||||||
const jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
|
const githubAudience = core.getInput('jwtGithubAudience', { required: false });
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
jwt = await core.getIDToken(githubAudience)
|
||||||
|
} else {
|
||||||
|
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
|
||||||
|
}
|
||||||
|
|
||||||
return await getClientToken(client, method, path, { jwt: jwt, role: role });
|
return await getClientToken(client, method, path, { jwt: jwt, role: role });
|
||||||
}
|
}
|
||||||
case 'kubernetes': {
|
case 'kubernetes': {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue