5
0
Fork 0
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:
Alex Kulikovskikh 2021-10-08 12:46:21 -04:00 committed by GitHub
parent b8c90c7243
commit c502100fbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 34680 additions and 14979 deletions

View file

@ -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. | | |

View file

@ -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

View file

@ -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');
})
}); });
}); });

View file

@ -72,6 +72,7 @@ f52E9W2iFNt3sxB0KFtOkbkCAwEAAQ==
`; `;
module.exports = { module.exports = {
privateRsaKey,
privateRsaKeyBase64, privateRsaKeyBase64,
publicRsaKey publicRsaKey
}; };

49466
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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': {