diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5aae23f..c587940 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,21 +141,19 @@ jobs: url: http://localhost:8200 token: testtoken secrets: | - test secret ; - test secret | NAMED_SECRET ; - nested/test otherSecret ; + secret/data/test secret ; + secret/data/test secret | NAMED_SECRET ; + secret/data/nested/test otherSecret ; - name: Test Vault Action (default KV V1) uses: ./ with: url: http://localhost:8200 token: testtoken - path: my-secret - kv-version: 1 secrets: | - test altSecret ; - test altSecret | NAMED_ALTSECRET ; - nested/test otherAltSecret ; + my-secret/test altSecret ; + my-secret/test altSecret | NAMED_ALTSECRET ; + my-secret/nested/test otherAltSecret ; - name: Test Vault Action (cubbyhole) uses: ./ @@ -217,9 +215,9 @@ jobs: clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }} clientKey: ${{ secrets.VAULT_CLIENT_KEY }} secrets: | - test secret ; - test secret | NAMED_SECRET ; - nested/test otherSecret ; + secret/data/test secret ; + secret/data/test secret | NAMED_SECRET ; + secret/data/nested/test otherSecret ; - name: Test Vault Action (tlsSkipVerify) uses: ./ @@ -230,22 +228,20 @@ jobs: clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }} clientKey: ${{ secrets.VAULT_CLIENT_KEY }} secrets: | - tlsSkipVerify skip ; + secret/data/tlsSkipVerify skip ; - name: Test Vault Action (default KV V1) uses: ./ with: url: https://localhost:8200 token: ${{ env.VAULT_TOKEN }} - path: my-secret - kv-version: 1 caCertificate: ${{ secrets.VAULTCA }} clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }} clientKey: ${{ secrets.VAULT_CLIENT_KEY }} secrets: | - test altSecret ; - test altSecret | NAMED_ALTSECRET ; - nested/test otherAltSecret ; + my-secret/test altSecret ; + my-secret/test altSecret | NAMED_ALTSECRET ; + my-secret/nested/test otherAltSecret ; - name: Test Vault Action (cubbyhole) uses: ./ diff --git a/README.md b/README.md index 5959b4e..848eea0 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,10 @@ **Please note**: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault or this Vault Action, _please responsibly disclose_ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). -This repository was recently adopted by HashiCorp. We're actively working on adding -additional functionality to this action soon: - -- [ ] TLS -- [ ] mTLS -- [ ] Simplify secret request UX --- A helper action for easily pulling secrets from HashiCorp Vault™. -By default, this action pulls from [Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2/) of the K/V Engine. See examples below for how to [use v1](#using-kv-version-1) as well as [other non-K/V engines](#other-secret-engines). - - [Example Usage](#example-usage) @@ -25,8 +17,6 @@ By default, this action pulls from [Version 2](https://www.vaultproject.io/docs - [Set Output Variable Name](#set-output-variable-name) - [Multiple Secrets](#multiple-secrets) - [Nested Secrets](#nested-secrets) - - [Using K/V version 1](#using-kv-version-1) -- [Custom K/V Engine Path](#custom-kv-engine-path) - [Other Secret Engines](#other-secret-engines) - [Adding Extra Headers](#adding-extra-headers) - [Vault Enterprise Features](#vault-enterprise-features) @@ -51,9 +41,9 @@ jobs: url: https://vault.mycompany.com:8200 token: ${{ secrets.VaultToken }} secrets: | - ci/aws accessKey | AWS_ACCESS_KEY_ID ; - ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; - ci npm_token + secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ; + secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; + secret/data/ci npm_token # ... ``` @@ -100,11 +90,11 @@ Each secret request consists of the `path` and the `key` of the desired secret, ### Simple Key -To retrieve a key `npmToken` from path `ci` that has value `somelongtoken` from vault you could do: +To retrieve a key `npmToken` from path `secret/data/ci` that has value `somelongtoken` from vault you could do: ```yaml with: - secrets: ci npmToken + secrets: secret/data/ci npmToken ``` `vault-action` will automatically normalize the given secret selector key, and set the follow as environment variables for the following steps in the current job: @@ -134,7 +124,7 @@ However, if you want to set it to a specific name, say `NPM_TOKEN`, you could do ```yaml with: - secrets: ci npmToken | NPM_TOKEN + secrets: secret/data/ci npmToken | NPM_TOKEN ``` With that, `vault-action` will now use your requested name and output: @@ -161,65 +151,18 @@ This action can take multi-line input, so say you had your AWS keys stored in a ```yaml with: secrets: | - ci/aws accessKey | AWS_ACCESS_KEY_ID ; - ci/aws secretKey | AWS_SECRET_ACCESS_KEY + secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ; + secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ``` -### Nested Secrets - -By default, `vault-action` will read the key from `data.data` in the response for the K/V v2 engine (default), or `data` for K/V v1 and other Secret Engines (see below for more info). -If you need to retrieve a sub-key from a JSON document, or are interested in some other component of the Vault response, you can specify a different key as the path to the desired out. - -_**Important**_: You must specify an [Output Variable Name](#set-output-variable-name) when using this method. - -```yaml -with: - secrets: | - my/path pair.key | NESTED_SECRET ; -``` - -Under the covers, we're using [JSONata](https://jsonata.org/) to provide the querying functionality. Any valid JSONata syntax is valid here, and will be outputted as a `JSON.stringify`-ied result. - -### Using K/V version 1 - -By default, `vault-action` expects a K/V engine using [version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html). - -In order to work with a [v1 engine](https://www.vaultproject.io/docs/secrets/kv/kv-v1/), the `kv-version` parameter may be passed: - -```yaml -with: - kv-version: 1 -``` - -## Custom K/V Engine Path - -When you enable the K/V Engine, by default it's placed at the path `secret`, so a secret named `ci` will be accessed from `secret/ci`. However, [if you enabled the secrets engine using a custom `path`](https://www.vaultproject.io/docs/commands/secrets/enable/#inlinecode--path-4), you -can pass it as follows: - -```yaml -with: - path: my-secrets - secrets: ci npmToken -``` - -This way, the `ci` secret in the example above will be retrieved from `my-secrets/ci`. - ## Other Secret Engines -While this action primarily supports the K/V engine, it is possible to request secrets from other engines in Vault. +Vault Action currently supports retrieving secrets from any engine where secrets +are retrieved via `GET` requests. This means secret engines such as PKI are currently +not supported due to their requirement of sending parameters along with the request +(such as `common_name`). -To do so when specifying the `Secret Path`, just append a leading forward slash (`/`) and specify the path as described in the Vault API documentation. - -For example, to retrieve a stored secret from the [`cubbyhole` engine](https://www.vaultproject.io/api-docs/secret/cubbyhole/), assuming you have a stored secret at the path `foo` with the contents: - -```json -{ - "foo": "bar", - "zip": "zap" -} -``` - -You could request the contents like so: +For example, to request a secret from the `cubbyhole` secret engine: ```yaml with: @@ -247,21 +190,6 @@ steps: run: "my-cli --token '${{ steps.secrets.outputs.MY_KEY }}'" ``` -Secrets pulled from the same `Secret Path` are cached by default. So if you, for example, are using the `aws` engine and retrieve a key, only a single key for a given path is returned. - -e.g.: - -```yaml -with: - secrets: | - /aws/creds/ci access_key | AWS_ACCESS_KEY_ID ; - /aws/creds/ci secret_key | AWS_SECRET_ACCESS_KEY -``` - -would work fine. - -*NOTE: Per [Nested Secrets](#nested-secrets), the `Key` is pulled from the `data` property of the response.* - ## Adding Extra Headers If you ever need to add extra headers to the vault request, say if you need to authenticate with a firewall, all you need to do is set `extraHeaders`: @@ -269,8 +197,8 @@ If you ever need to add extra headers to the vault request, say if you need to a ```yaml with: secrets: | - ci/aws accessKey | AWS_ACCESS_KEY_ID ; - ci/aws secretKey | AWS_SECRET_ACCESS_KEY + secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ; + secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY extraHeaders: | X-Secure-Id: ${{ secrets.SECURE_ID }} X-Secure-Secret: ${{ secrets.SECURE_SECRET }} @@ -295,9 +223,9 @@ steps: token: ${{ secrets.VaultToken }} namespace: ns1 secrets: | - ci/aws accessKey | AWS_ACCESS_KEY_ID ; - ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; - ci npm_token + secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ; + secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; + secret/ci npm_token ``` ## Reference @@ -309,8 +237,6 @@ Here are all the inputs available through `with`: | `url` | The URL for the vault endpoint | | ✔ | | `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | ✔ | | `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | | -| `path` | The path of a non-default K/V engine | | | -| `kv-version` | The version of the K/V engine to use. | `2` | | | `method` | The method to use to authenticate with Vault. | `token` | | | `token` | The Vault Token to be used to authenticate with Vault | | | | `roleId` | The Role Id for App Role authentication | | | diff --git a/action.yml b/action.yml index 652c676..cc02c11 100644 --- a/action.yml +++ b/action.yml @@ -10,13 +10,6 @@ inputs: namespace: description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default' required: false - path: - description: 'The path of a non-default K/V engine' - required: false - kv-version: - description: 'The version of the K/V engine to use.' - default: '2' - required: false method: description: 'The method to use to authenticate with Vault.' default: 'token' diff --git a/dist/index.js b/dist/index.js index 5182c2c..d14bbd0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10552,9 +10552,9 @@ async function getSecrets(secretRequests, client) { const responseCache = new Map(); const results = []; for (const secretRequest of secretRequests) { - const { path, selector } = secretRequest; + let { path, selector } = secretRequest; - const requestPath = `v1${path}`; + const requestPath = `v1/${path}`; let body; let cachedResponse = false; if (responseCache.has(requestPath)) { @@ -10566,7 +10566,13 @@ async function getSecrets(secretRequests, client) { responseCache.set(requestPath, body); } - const value = selectData(JSON.parse(body), selector); + selector = "data." + selector + body = JSON.parse(body) + if (body.data["data"] != undefined) { + selector = "data." + selector + } + + const value = selectData(body, selector); results.push({ request: secretRequest, value, @@ -14099,7 +14105,6 @@ const jsonata = __webpack_require__(350); const { auth: { retrieveToken }, secrets: { getSecrets } } = __webpack_require__(676); const AUTH_METHODS = ['approle', 'token', 'github']; -const VALID_KV_VERSION = [-1, 1, 2]; async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); @@ -14107,10 +14112,6 @@ async function exportSecrets() { const extraHeaders = parseHeadersInput('extraHeaders', { required: false }); const exportEnv = core.getInput('exportEnv', { required: false }) != 'false'; - let enginePath = core.getInput('path', { required: false }); - /** @type {number | string} */ - let kvVersion = core.getInput('kv-version', { required: false }); - const secretsInput = core.getInput('secrets', { required: true }); const secretRequests = parseSecretsInput(secretsInput); @@ -14158,32 +14159,9 @@ async function exportSecrets() { defaultOptions.headers['X-Vault-Token'] = vaultToken; const client = got.extend(defaultOptions); - if (!enginePath) { - enginePath = 'secret'; - } - - if (!kvVersion) { - kvVersion = 2; - } - kvVersion = +kvVersion; - - if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) { - throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); - } - const requests = secretRequests.map(request => { const { path, selector } = request; - - if (path.startsWith('/')) { - return request; - } - const kvPath = (kvVersion === 2) - ? `/${enginePath}/data/${path}` - : `/${enginePath}/${path}`; - const kvSelector = (kvVersion === 2) - ? `data.data.${selector}` - : `data.${selector}`; - return { ...request, path: kvPath, selector: kvSelector }; + return request; }); const results = await getSecrets(requests, client); diff --git a/integrationTests/basic/integration.test.js b/integrationTests/basic/integration.test.js index a9a89e5..6c911d3 100644 --- a/integrationTests/basic/integration.test.js +++ b/integrationTests/basic/integration.test.js @@ -42,9 +42,21 @@ describe('integration', () => { } }); + await got(`${vaultUrl}/v1/secret/data/foobar`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + data: { + fookv2: 'bar', + }, + } + }); + // Enable custom secret engine try { - await got(`${vaultUrl}/v1/sys/mounts/my-secret`, { + await got(`${vaultUrl}/v1/sys/mounts/secret-kv1`, { method: 'POST', headers: { 'X-Vault-Token': 'testtoken', @@ -62,7 +74,7 @@ describe('integration', () => { } } - await got(`${vaultUrl}/v1/my-secret/test`, { + await got(`${vaultUrl}/v1/secret-kv1/test`, { method: 'POST', headers: { 'X-Vault-Token': 'testtoken', @@ -72,7 +84,17 @@ describe('integration', () => { } }); - await got(`${vaultUrl}/v1/my-secret/nested/test`, { + await got(`${vaultUrl}/v1/secret-kv1/foobar`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + fookv1: 'bar', + } + }); + + await got(`${vaultUrl}/v1/secret-kv1/nested/test`, { method: 'POST', headers: { 'X-Vault-Token': 'testtoken', @@ -101,20 +123,8 @@ describe('integration', () => { .mockReturnValueOnce(secrets); } - function mockEngineName(name) { - when(core.getInput) - .calledWith('path') - .mockReturnValueOnce(name); - } - - function mockVersion(version) { - when(core.getInput) - .calledWith('kv-version') - .mockReturnValueOnce(version); - } - it('get simple secret', async () => { - mockInput('test secret'); + mockInput('secret/data/test secret'); await exportSecrets(); @@ -122,7 +132,7 @@ describe('integration', () => { }); it('re-map secret', async () => { - mockInput('test secret | TEST_KEY'); + mockInput('secret/data/test secret | TEST_KEY'); await exportSecrets(); @@ -130,7 +140,7 @@ describe('integration', () => { }); it('get nested secret', async () => { - mockInput('nested/test otherSecret'); + mockInput('secret/data/nested/test otherSecret'); await exportSecrets(); @@ -139,9 +149,9 @@ describe('integration', () => { it('get multiple secrets', async () => { mockInput(` - test secret ; - test secret | NAMED_SECRET ; - nested/test otherSecret ;`); + secret/data/test secret ; + secret/data/test secret | NAMED_SECRET ; + secret/data/nested/test otherSecret ;`); await exportSecrets(); @@ -152,10 +162,16 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET'); }); + it('leading slash kvv2', async () => { + mockInput('/secret/data/foobar fookv2'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('FOOKV2', 'bar'); + }); + it('get secret from K/V v1', async () => { - mockInput('test secret'); - mockEngineName('my-secret'); - mockVersion('1'); + mockInput('secret-kv1/test secret'); await exportSecrets(); @@ -163,15 +179,21 @@ describe('integration', () => { }); it('get nested secret from K/V v1', async () => { - mockInput('nested/test otherSecret'); - mockEngineName('my-secret'); - mockVersion('1'); + mockInput('secret-kv1/nested/test otherSecret'); await exportSecrets(); expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERCUSTOMSECRET'); }); + it('leading slash kvv1', async () => { + mockInput('/secret-kv1/foobar fookv1'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('FOOKV1', 'bar'); + }); + describe('generic engines', () => { beforeAll(async () => { await got(`${vaultUrl}/v1/cubbyhole/test`, { diff --git a/integrationTests/enterprise/enterprise.test.js b/integrationTests/enterprise/enterprise.test.js index 90ad22c..b791022 100644 --- a/integrationTests/enterprise/enterprise.test.js +++ b/integrationTests/enterprise/enterprise.test.js @@ -56,7 +56,7 @@ describe('integration', () => { }); it('get simple secret', async () => { - mockInput('test secret'); + mockInput('secret/data/test secret'); await exportSecrets(); @@ -64,7 +64,7 @@ describe('integration', () => { }); it('re-map secret', async () => { - mockInput('test secret | TEST_KEY'); + mockInput('secret/data/test secret | TEST_KEY'); await exportSecrets(); @@ -72,7 +72,7 @@ describe('integration', () => { }); it('get nested secret', async () => { - mockInput('nested/test otherSecret'); + mockInput('secret/data/nested/test otherSecret'); await exportSecrets(); @@ -81,9 +81,9 @@ describe('integration', () => { it('get multiple secrets', async () => { mockInput(` - test secret ; - test secret | NAMED_SECRET ; - nested/test otherSecret ;`); + secret/data/test secret ; + secret/data/test secret | NAMED_SECRET ; + secret/data/nested/test otherSecret ;`); await exportSecrets(); @@ -95,9 +95,7 @@ describe('integration', () => { }); it('get secret from K/V v1', async () => { - mockInput('test secret'); - mockEngineName('my-secret'); - mockVersion('1'); + mockInput('my-secret/test secret'); await exportSecrets(); @@ -105,9 +103,7 @@ describe('integration', () => { }); it('get nested secret from K/V v1', async () => { - mockInput('nested/test otherSecret'); - mockEngineName('my-secret'); - mockVersion('1'); + mockInput('my-secret/nested/test otherSecret'); await exportSecrets(); @@ -229,7 +225,7 @@ describe('authenticate with approle', () => { }); it('authenticate with approle', async() => { - mockInput('test secret'); + mockInput('secret/data/test secret'); await exportSecrets(); diff --git a/src/action.js b/src/action.js index 9404a42..e6875ee 100644 --- a/src/action.js +++ b/src/action.js @@ -6,7 +6,6 @@ const jsonata = require('jsonata'); const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); const AUTH_METHODS = ['approle', 'token', 'github']; -const VALID_KV_VERSION = [-1, 1, 2]; async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); @@ -14,10 +13,6 @@ async function exportSecrets() { const extraHeaders = parseHeadersInput('extraHeaders', { required: false }); const exportEnv = core.getInput('exportEnv', { required: false }) != 'false'; - let enginePath = core.getInput('path', { required: false }); - /** @type {number | string} */ - let kvVersion = core.getInput('kv-version', { required: false }); - const secretsInput = core.getInput('secrets', { required: true }); const secretRequests = parseSecretsInput(secretsInput); @@ -65,32 +60,9 @@ async function exportSecrets() { defaultOptions.headers['X-Vault-Token'] = vaultToken; const client = got.extend(defaultOptions); - if (!enginePath) { - enginePath = 'secret'; - } - - if (!kvVersion) { - kvVersion = 2; - } - kvVersion = +kvVersion; - - if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) { - throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); - } - const requests = secretRequests.map(request => { const { path, selector } = request; - - if (path.startsWith('/')) { - return request; - } - const kvPath = (kvVersion === 2) - ? `/${enginePath}/data/${path}` - : `/${enginePath}/${path}`; - const kvSelector = (kvVersion === 2) - ? `data.data.${selector}` - : `data.${selector}`; - return { ...request, path: kvPath, selector: kvSelector }; + return request; }); const results = await getSecrets(requests, client); diff --git a/src/secrets.js b/src/secrets.js index a259293..a364925 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -25,9 +25,9 @@ async function getSecrets(secretRequests, client) { const responseCache = new Map(); const results = []; for (const secretRequest of secretRequests) { - const { path, selector } = secretRequest; + let { path, selector } = secretRequest; - const requestPath = `v1${path}`; + const requestPath = `v1/${path}`; let body; let cachedResponse = false; if (responseCache.has(requestPath)) { @@ -39,7 +39,13 @@ async function getSecrets(secretRequests, client) { responseCache.set(requestPath, body); } - const value = selectData(JSON.parse(body), selector); + selector = "data." + selector + body = JSON.parse(body) + if (body.data["data"] != undefined) { + selector = "data." + selector + } + + const value = selectData(body, selector); results.push({ request: secretRequest, value,