From 8a7c9e8dd1888312cc63c016247e610750188037 Mon Sep 17 00:00:00 2001 From: "johannes.guenther" Date: Tue, 17 Nov 2020 21:50:03 +0100 Subject: [PATCH] implemented kubernetes auth --- README.md | 12 +++++++ action.yml | 6 ++++ src/action.js | 2 +- src/auth.js | 12 +++++++ src/auth.test.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/auth.test.js diff --git a/README.md b/README.md index 2507276..46e926e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,16 @@ with: caCertificate: ${{ secrets.VAULTCA }} ``` +- **kubernetes**: you must provide the path to the token in the `tokenPath`variable as well as the roleName for kuberentes bases auth this is interesting if [kubernetes auth](https://www.vaultproject.io/docs/auth/kubernetes) in combination with self hosted runners is deployed: +```yaml +... +with: + url: https://vault.mycompany.com:8200 + method: kubernetes + roleName: ${{ secrets.KUBE_ROLENAME }} + tokenPath: /var/run/secrets/kubernetes.io/serviceaccount/token +``` + If any other method is specified and you provide an `authPayload`, the action will attempt to `POST` to `auth/${method}/login` with the provided payload and parse out the client token. ## Key Syntax @@ -247,6 +257,8 @@ Here are all the inputs available through `with`: | `roleId` | The Role Id for App Role authentication | | | | `secretId` | The Secret Id for App Role authentication | | | | `githubToken` | The Github Token to be used to authenticate with Vault | | | +| `roleName` | The rolename of the serviceaccount for the kubernetes authentification | | | +| `tokenPath` | The path to the serviceacconut secret with the jwt-token for kubernetes based authentification | | | | `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | | | `extraHeaders` | A string of newline separated extra headers to include on every request. | | | | `exportEnv` | Whether or not export secrets as environment variables. | `true` | | diff --git a/action.yml b/action.yml index b6b4ae3..6c4aea5 100644 --- a/action.yml +++ b/action.yml @@ -26,6 +26,12 @@ inputs: githubToken: description: 'The Github Token to be used to authenticate with Vault' required: false + roleName: + description: "The role name for the kubernetes authentification" + required: false + tokenPath: + description: "The path to the kubernetes service account secret, the action reads the content of this file on the runner" + required: false authPayload: description: 'The JSON payload to be sent to Vault when using a custom authentication method.' required: false diff --git a/src/action.js b/src/action.js index 7a3c072..fa50ba2 100644 --- a/src/action.js +++ b/src/action.js @@ -5,7 +5,7 @@ const got = require('got').default; const jsonata = require('jsonata'); const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); -const AUTH_METHODS = ['approle', 'token', 'github']; +const AUTH_METHODS = ['approle', 'token', 'github','kubernetes']; async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); diff --git a/src/auth.js b/src/auth.js index 484fd48..f4a350e 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,5 +1,6 @@ // @ts-check const core = require('@actions/core'); +const fs = require('fs'); /*** * Authenticate with Vault and retrieve a Vault token that can be used for requests. @@ -17,6 +18,17 @@ async function retrieveToken(method, client) { const githubToken = core.getInput('githubToken', { required: true }); return await getClientToken(client, method, { token: githubToken }); } + case 'kubernetes': { + const tokenPath = core.getInput('tokenPath', { required: true }) + const data = fs.readFileSync(tokenPath, 'utf8') + const roleName = core.getInput('roleName', { required: true }) + if (!(roleName && data) && data != "") { + throw new Error("Role Name must be set and a kubernetes token must set") + } + const payload = { jwt: data, role: roleName } + return await getClientToken(client, method, payload) + } + default: { if (!method || method === 'token') { return core.getInput('token', { required: true }); diff --git a/src/auth.test.js b/src/auth.test.js new file mode 100644 index 0000000..09bd6f7 --- /dev/null +++ b/src/auth.test.js @@ -0,0 +1,82 @@ +jest.mock('got'); +jest.mock('@actions/core'); +jest.mock('@actions/core/lib/command'); +jest.mock("fs") + +const core = require('@actions/core'); +const got = require('got'); +const fs = require("fs") +const { when } = require('jest-when'); + + +const { + retrieveToken +} = require('./auth'); + + +function mockInput(name, key) { + when(core.getInput) + .calledWith(name) + .mockReturnValueOnce(key); +} + +function mockApiResponse() { + const response = { body: { auth: { client_token: testToken, renewable: true, policies: [], accessor: "accessor" } } } + got.post = jest.fn() + got.post.mockReturnValue(response) +} +const testToken = "testoken"; + +describe("test retrival for token", () => { + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("test retrival with approle", async () => { + const method = 'approle' + mockApiResponse() + const testRoleId = "testRoleId" + const testSecretId = "testSecretId" + mockInput("roleId", testRoleId) + mockInput("secretId", testSecretId) + const token = await retrieveToken(method, got) + expect(token).toEqual(testToken) + const payload = got.post.mock.calls[0][1].json + expect(payload).toEqual({ role_id: testRoleId, secret_id: testSecretId }) + const url = got.post.mock.calls[0][0] + expect(url).toContain('approle') + }) + + it("test retrival with github token", async () => { + const method = 'github' + mockApiResponse() + const githubToken = "githubtoken" + mockInput("githubToken", githubToken) + const token = await retrieveToken(method, got) + expect(token).toEqual(testToken) + const payload = got.post.mock.calls[0][1].json + expect(payload).toEqual({ token: githubToken }) + const url = got.post.mock.calls[0][0] + expect(url).toContain('github') + }) + + it("test retrival with kubernetes", async () => { + const method = 'kubernetes' + const jwtToken = "someJwtToken" + const testRoleName = "testRoleName" + const testTokenPath = "testTokenPath" + mockApiResponse() + mockInput("tokenPath", testTokenPath) + mockInput("roleName", testRoleName) + fs.readFileSync = jest.fn() + fs.readFileSync.mockReturnValueOnce(jwtToken) + const token = await retrieveToken(method, got) + expect(token).toEqual(testToken) + const payload = got.post.mock.calls[0][1].json + expect(payload).toEqual({ jwt: jwtToken, role: testRoleName }) + const url = got.post.mock.calls[0][0] + expect(url).toContain('kubernetes') + }) + +}) \ No newline at end of file