Skip to content

Commit

Permalink
feat: support for KV version 1 and custom-named engines (#12)
Browse files Browse the repository at this point in the history
* feat: kv v1 and engine path

* doc: add custom version and engine path usage docs

Co-authored-by: Richard Simpson <[email protected]>
  • Loading branch information
gVirtu and RichiCoder1 authored Feb 4, 2020
1 parent 3b9239d commit f229481
Show file tree
Hide file tree
Showing 9 changed files with 465 additions and 139 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
env:
VAULT_HOST: localhost
VAULT_PORT: ${{ job.services.vault.ports[8200] }}
- name: use vault action
- name: use vault action (default K/V version 2)
uses: ./
with:
url: http://localhost:${{ job.services.vault.ports[8200] }}
Expand All @@ -119,6 +119,17 @@ jobs:
test secret ;
test secret | NAMED_SECRET ;
nested/test otherSecret ;
- name: use vault action (custom K/V version 1)
uses: ./
with:
url: http://localhost:${{ job.services.vault.ports[8200] }}
token: testtoken
path: my-secret
kv-version: 1
secrets: |
test altSecret ;
test altSecret | NAMED_ALTSECRET ;
nested/test otherAltSecret ;
- name: verify
run: npm run test:e2e

Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# vault-action

A helper action for easily pulling secrets from the default v2 K/V backend of vault.
A helper action for easily pulling secrets from the K/V backend of vault.

Expects [Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2/) of the KV Secrets Engine by default.

## Example Usage

Expand Down Expand Up @@ -39,7 +41,7 @@ with:
url: https://vault.mycompany.com:8200
method: approle
roleId: ${{ secrets.roleId }}
secretId : ${{ secrets.secretId }}
secretId: ${{ secrets.secretId }}
```
## Key Syntax
Expand Down Expand Up @@ -93,9 +95,33 @@ with:
ci/aws secretKey | AWS_SECRET_ACCESS_KEY
```

### 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 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`.

### Namespace

This action could be use with namespace Vault Enterprise feature. You can specify namespace in request :
This action could be use with namespace Vault Enterprise feature. You can specify namespace in request :

```yaml
steps:
Expand Down
60 changes: 51 additions & 9 deletions action.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true });
const vaultNamespace = core.getInput('namespace', { required: false });

let enginePath = core.getInput('path', { required: false });
let kvVersion = core.getInput('kv-version', { required: false });

const secretsInput = core.getInput('secrets', { required: true });
const secrets = parseSecretsInput(secretsInput);

Expand All @@ -21,10 +24,10 @@ async function exportSecrets() {
const vaultRoleId = core.getInput('roleId', { required: true });
const vaultSecretId = core.getInput('secretId', { required: true });
core.debug('Try to retrieve Vault Token from approle');
var options = {
headers: {},
json: { role_id: vaultRoleId, secret_id: vaultSecretId },
responseType: 'json'
var options = {
headers: {},
json: { role_id: vaultRoleId, secret_id: vaultSecretId },
responseType: 'json'
};

if (vaultNamespace != null) {
Expand All @@ -44,6 +47,20 @@ async function exportSecrets() {
break;
}

if (!enginePath) {
enginePath = 'secret';
}

if (!kvVersion) {
kvVersion = '2';
}

if (kvVersion !== '1' && kvVersion !== '2') {
throw Error(`You must provide a valid K/V version (1 or 2). Input: "${kvVersion}"`);
}

kvVersion = parseInt(kvVersion);

for (const secret of secrets) {
const { secretPath, outputName, secretKey } = secret;
const requestOptions = {
Expand All @@ -56,12 +73,13 @@ async function exportSecrets() {
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
}

const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, requestOptions);
const requestPath = (kvVersion === 1)
? `${vaultUrl}/v1/${enginePath}/${secretPath}`
: `${vaultUrl}/v1/${enginePath}/data/${secretPath}`;
const result = await got(requestPath, requestOptions);

const parsedResponse = JSON.parse(result.body);
const vaultKeyData = parsedResponse.data;
const versionData = vaultKeyData.data;
const value = versionData[secretKey];
const secretData = parseResponse(result.body, kvVersion);
const value = secretData[secretKey];
command.issue('add-mask', value);
core.exportVariable(outputName, `${value}`);
core.debug(`✔ ${secretPath} => ${outputName}`);
Expand Down Expand Up @@ -120,6 +138,29 @@ function parseSecretsInput(secretsInput) {
return output;
}

/**
* Parses a JSON response and returns the secret data
* @param {string} responseBody
* @param {number} kvVersion
*/
function parseResponse(responseBody, kvVersion) {
const parsedResponse = JSON.parse(responseBody);
let secretData;

switch(kvVersion) {
case 1: {
secretData = parsedResponse.data;
} break;

case 2: {
const vaultKeyData = parsedResponse.data;
secretData = vaultKeyData.data;
} break;
}

return secretData;
}

/**
* Replaces any forward-slash characters to
* @param {string} dataKey
Expand All @@ -131,5 +172,6 @@ function normalizeOutputKey(dataKey) {
module.exports = {
exportSecrets,
parseSecretsInput,
parseResponse,
normalizeOutputKey
};
88 changes: 76 additions & 12 deletions action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const got = require('got');
const {
exportSecrets,
parseSecretsInput,
parseResponse
} = require('./action');

const { when } = require('jest-when');
Expand Down Expand Up @@ -82,34 +83,83 @@ describe('parseSecretsInput', () => {
})
});

describe('parseResponse', () => {
// https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response
it('parses K/V version 1 response', () => {
const response = JSON.stringify({
data: {
foo: 'bar'
}
})
const output = parseResponse(response, 1);

expect(output).toEqual({
foo: 'bar'
});
});

// https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version
it('parses K/V version 2 response', () => {
const response = JSON.stringify({
data: {
data: {
foo: 'bar'
}
}
})
const output = parseResponse(response, 2);

expect(output).toEqual({
foo: 'bar'
});
});
});


describe('exportSecrets', () => {
beforeEach(() => {
jest.resetAllMocks();

when(core.getInput)
.calledWith('url')
.mockReturnValue('http://vault:8200');
.mockReturnValueOnce('http://vault:8200');

when(core.getInput)
.calledWith('token')
.mockReturnValue('EXAMPLE');
.mockReturnValueOnce('EXAMPLE');
});

function mockInput(key) {
when(core.getInput)
.calledWith('secrets')
.mockReturnValue(key);
.mockReturnValueOnce(key);
}

function mockVaultData(data) {
got.mockResolvedValue({
body: JSON.stringify({
data: {
data
}
})
});
function mockVersion(version) {
when(core.getInput)
.calledWith('kv-version')
.mockReturnValueOnce(version);
}

function mockVaultData(data, version='2') {
switch(version) {
case '1':
got.mockResolvedValue({
body: JSON.stringify({
data
})
});
break;
case '2':
got.mockResolvedValue({
body: JSON.stringify({
data: {
data
}
})
});
break;
}
}

it('simple secret retrieval', async () => {
Expand All @@ -133,4 +183,18 @@ describe('exportSecrets', () => {

expect(core.exportVariable).toBeCalledWith('TEST_NAME', '1');
});
});

it('simple secret retrieval from K/V v1', async () => {
const version = '1';

mockInput('test key');
mockVersion(version);
mockVaultData({
key: 1
}, version);

await exportSecrets();

expect(core.exportVariable).toBeCalledWith('KEY', '1');
});
});
Loading

0 comments on commit f229481

Please sign in to comment.