Skip to content

Commit c502100

Browse files
authored
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`
1 parent b8c90c7 commit c502100

7 files changed

Lines changed: 34175 additions & 14474 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,28 @@ with:
8686
githubToken: ${{ secrets.MY_GITHUB_TOKEN }}
8787
caCertificate: ${{ secrets.VAULTCA }}
8888
```
89+
- **jwt**: (Github OIDC) you must provide a `role` parameter, additionally you can pass `jwtGithubAudience` parameter.
90+
91+
```yaml
92+
...
93+
with:
94+
url: https://vault.mycompany.com:8200
95+
method: jwt
96+
role: github-action
97+
```
98+
99+
**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
100+
101+
```yaml
102+
...
103+
permissions:
104+
id-token: write
105+
contents: read
106+
...
107+
```
108+
89109
- **jwt**: you must provide a `role` & `jwtPrivateKey` parameters, additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
110+
90111
```yaml
91112
...
92113
with:
@@ -278,6 +299,7 @@ Here are all the inputs available through `with`:
278299
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
279300
| `jwtPrivateKey` | Base64 encoded Private key to sign JWT | | |
280301
| `jwtKeyPassword` | Password for key stored in jwtPrivateKey (if needed) | | |
302+
| `jwtGithubAudience` | Identifies the recipient ("aud" claim) that the JWT is intended for |`sigstore`| |
281303
| `jwtTtl` | Time in seconds, after which token expires | | 3600 |
282304
| `kubernetesTokenPath` | The path to the service-account secret with the jwt token for kubernetes based authentication |`/var/run/secrets/kubernetes.io/serviceaccount/token` | |
283305
| `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | |

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ inputs:
6969
jwtKeyPassword:
7070
description: 'Password for key stored in jwtPrivateKey (if needed)'
7171
required: false
72+
jwtGithubAudience:
73+
description: 'Identifies the recipient ("aud" claim) that the JWT is intended for'
74+
required: false
7275
jwtTtl:
7376
description: 'Time in seconds, after which token expires'
7477
required: false

integrationTests/basic/jwt_auth.test.js

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
jest.mock('@actions/core');
22
jest.mock('@actions/core/lib/command');
33
const core = require('@actions/core');
4+
const rsasign = require('jsrsasign');
45
const {
6+
privateRsaKey,
57
privateRsaKeyBase64,
68
publicRsaKey
79
} = require('./rsa_keys');
@@ -13,6 +15,42 @@ const { exportSecrets } = require('../../src/action');
1315

1416
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
1517

18+
/**
19+
* Returns Github OIDC response mock
20+
* @param {string} aud Audience claim
21+
* @returns {string}
22+
*/
23+
function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action") {
24+
const alg = 'RS256';
25+
const header = { alg: alg, typ: 'JWT' };
26+
const now = rsasign.KJUR.jws.IntDate.getNow();
27+
const payload = {
28+
jti: "unique-id",
29+
sub: "repo:hashicorp/vault-action:ref:refs/heads/master",
30+
aud,
31+
ref: "refs/heads/master",
32+
sha: "commit-sha",
33+
repository: "hashicorp/vault-action",
34+
repository_owner: "hashicorp",
35+
run_id: "1",
36+
run_number: "1",
37+
run_attempt: "1",
38+
actor: "github-username",
39+
workflow: "Workflow Name",
40+
head_ref: "",
41+
base_ref: "",
42+
event_name: "push",
43+
ref_type: "branch",
44+
job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/master",
45+
iss: 'vault-action',
46+
iat: now,
47+
nbf: now,
48+
exp: now + 3600,
49+
};
50+
const decryptedKey = rsasign.KEYUTIL.getKey(privateRsaKey);
51+
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
52+
}
53+
1654
describe('jwt auth', () => {
1755
beforeAll(async () => {
1856
// Verify Connection
@@ -94,33 +132,107 @@ describe('jwt auth', () => {
94132
});
95133
});
96134

97-
beforeEach(() => {
98-
jest.resetAllMocks();
135+
describe('authenticate with private key', () => {
136+
beforeEach(() => {
137+
jest.resetAllMocks();
138+
139+
when(core.getInput)
140+
.calledWith('url')
141+
.mockReturnValueOnce(`${vaultUrl}`);
99142

100-
when(core.getInput)
101-
.calledWith('url')
102-
.mockReturnValueOnce(`${vaultUrl}`);
143+
when(core.getInput)
144+
.calledWith('method')
145+
.mockReturnValueOnce('jwt');
103146

104-
when(core.getInput)
105-
.calledWith('method')
106-
.mockReturnValueOnce('jwt');
147+
when(core.getInput)
148+
.calledWith('jwtPrivateKey')
149+
.mockReturnValueOnce(privateRsaKeyBase64);
107150

108-
when(core.getInput)
109-
.calledWith('jwtPrivateKey')
110-
.mockReturnValueOnce(privateRsaKeyBase64);
151+
when(core.getInput)
152+
.calledWith('role')
153+
.mockReturnValueOnce('default');
111154

112-
when(core.getInput)
113-
.calledWith('role')
114-
.mockReturnValueOnce('default');
155+
when(core.getInput)
156+
.calledWith('secrets')
157+
.mockReturnValueOnce('secret/data/test secret');
158+
});
115159

116-
when(core.getInput)
117-
.calledWith('secrets')
118-
.mockReturnValueOnce('secret/data/test secret');
160+
it('successfully authenticates', async () => {
161+
await exportSecrets();
162+
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
163+
});
119164
});
120165

121-
it('successfully authenticates', async () => {
122-
await exportSecrets();
123-
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
166+
describe('authenticate with Github OIDC', () => {
167+
beforeAll(async () => {
168+
await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, {
169+
method: 'POST',
170+
headers: {
171+
'X-Vault-Token': 'testtoken',
172+
},
173+
json: {
174+
role_type: 'jwt',
175+
bound_audiences: null,
176+
bound_claims: {
177+
iss: 'vault-action',
178+
aud: 'sigstore',
179+
},
180+
user_claim: 'iss',
181+
policies: ['reader']
182+
}
183+
});
184+
})
185+
186+
beforeEach(() => {
187+
jest.resetAllMocks();
188+
189+
when(core.getInput)
190+
.calledWith('url')
191+
.mockReturnValueOnce(`${vaultUrl}`);
192+
193+
when(core.getInput)
194+
.calledWith('method')
195+
.mockReturnValueOnce('jwt');
196+
197+
when(core.getInput)
198+
.calledWith('jwtPrivateKey')
199+
.mockReturnValueOnce('');
200+
201+
when(core.getInput)
202+
.calledWith('role')
203+
.mockReturnValueOnce('default');
204+
205+
when(core.getInput)
206+
.calledWith('secrets')
207+
.mockReturnValueOnce('secret/data/test secret');
208+
209+
when(core.getIDToken)
210+
.calledWith()
211+
.mockReturnValueOnce(mockGithubOIDCResponse());
212+
});
213+
214+
it('successfully authenticates', async () => {
215+
await exportSecrets();
216+
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
217+
});
218+
219+
it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => {
220+
when(core.getInput)
221+
.calledWith('role')
222+
.mockReturnValueOnce('default-sigstore');
223+
224+
when(core.getInput)
225+
.calledWith('jwtGithubAudience')
226+
.mockReturnValueOnce('sigstore');
227+
228+
when(core.getIDToken)
229+
.calledWith()
230+
.mockReturnValueOnce(mockGithubOIDCResponse('sigstore'));
231+
232+
await exportSecrets();
233+
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
234+
})
235+
124236
});
125237

126238
});

integrationTests/basic/rsa_keys.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ f52E9W2iFNt3sxB0KFtOkbkCAwEAAQ==
7272
`;
7373

7474
module.exports = {
75+
privateRsaKey,
7576
privateRsaKeyBase64,
7677
publicRsaKey
7778
};

0 commit comments

Comments
 (0)