Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: write secrets to vault #481

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/local-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,20 @@ jobs:
url: http://localhost:8200
method: token
token: testtoken
secretsMethod: read
secrets: |
secret/data/test-json-string jsonString;

# Write Secret examples

# Write Simple Secret
# secret/data/writetest secret=TEST ;

# Write Mulitple Secrets at one path
# secret/data/writetest secret=TEST secret1=TEST1 secret2=TEST2 ;

# Json String Secret
# secret/data/writetest secret={"url":"https://google.com/hello","key":"EQWQASAMSADAD"};

- name: Check Secrets
run: |
Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ A helper action for easily pulling secrets from HashiCorp Vault™.
- [Simple Key](#simple-key)
- [Set Output Variable Name](#set-output-variable-name)
- [Multiple Secrets](#multiple-secrets)
- [Write Secrets](#write-secrets)
- [Write Multiple Secrets](#write-multiple-secrets)
- [Write Json Secrets](#write-json-secrets)
- [Other Secret Engines](#other-secret-engines)
- [Adding Extra Headers](#adding-extra-headers)
- [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise)
Expand Down Expand Up @@ -374,6 +377,51 @@ with:
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
```

### Write Secrets

This action can write secrets to vault, so say you had your AWS access Key and you want them to write to vault. You can provide `secretsMethod: write` and provide the secret data as below:

```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws accessKey=someAccessKey;
```

`vault-action` create the secret at provided vault path. You will get `SUCCESS` in response for you saved secrets.

You can also write the multiple secrets at a single path. You can do:

```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws accessKey=someAccessKey secretKey=someSecretKey;
```

### Write Multiple Secrets

This action can take multi-line input, so say you had your AWS keys to be saved to vault. You can do:

```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws/key accessKey=someAccessKey ;
secret/data/ci/aws/secret secretKey=someAccessKey ;
```

### Write Json Secrets

This action can take json string input as a secret value and save it to vault as a json string. You can do:

```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws/ secret={"accessKey":"someAccessKey","secretKey":"someAccessKey"} ;
```

## Other Secret Engines

Vault Action currently supports retrieving secrets from any engine where secrets
Expand Down Expand Up @@ -461,6 +509,7 @@ 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 | | |
| `secretsMethod` | The secretsMethod indicates if you want to read or write secrets to vault. Supported values are `"read"` and `"write"`. If not provided, `default` is `"read"` | | |
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
| `method` | The method to use to authenticate with Vault. | `token` | |
| `role` | Vault role for specified auth method | | |
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ inputs:
url:
description: 'The URL for the vault endpoint'
required: true
secretsMethod:
description: 'The secretsMethod indicates if you want to read or write to vault. Supported values are "read" and "write". If not provided default is "read"'
required: false
secrets:
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
required: false
Expand Down
123 changes: 110 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18516,10 +18516,11 @@ const core = __nccwpck_require__(2186);
const command = __nccwpck_require__(7351);
const got = (__nccwpck_require__(3061)["default"]);
const jsonata = __nccwpck_require__(4245);
const { auth: { retrieveToken }, secrets: { getSecrets } } = __nccwpck_require__(4351);
const { auth: { retrieveToken }, secrets: { getSecrets, writeSecrets } } = __nccwpck_require__(4351);

const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
const SECRETS_METHOD = { Read: "read", Write: "write" };

async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true });
Expand All @@ -18528,6 +18529,7 @@ async function exportSecrets() {
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
const outputToken = (core.getInput('outputToken', { required: false }) || 'false').toLowerCase() != 'false';
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
const secretsMethod = core.getInput('secretsMethod', { required: false });

const secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput);
Expand Down Expand Up @@ -18599,7 +18601,18 @@ async function exportSecrets() {
return request;
});

const results = await getSecrets(requests, client);
let results = null;
switch (secretsMethod) {
case SECRETS_METHOD.Read:
results = await getSecrets(requests, client);
break;
case SECRETS_METHOD.Write:
results = await writeSecrets(requests, client);
break;
default:
results = await getSecrets(requests, client);
break;
}


for (const result of results) {
Expand Down Expand Up @@ -18636,13 +18649,16 @@ async function exportSecrets() {
* @property {string} envVarName
* @property {string} outputVarName
* @property {string} selector
* @property {string} secretsMethod
* @property {Map} secretsData
*/

/**
* Parses a secrets input string into key paths and their resulting environment variable name.
* @param {string} secretsInput
* @param {string} secretsMethod
*/
function parseSecretsInput(secretsInput) {
function parseSecretsInput(secretsInput, secretsMethod) {
if (!secretsInput) {
return []
}
Expand Down Expand Up @@ -18674,18 +18690,58 @@ function parseSecretsInput(secretsInput) {
.map(part => part.trim())
.filter(part => part.length !== 0);

if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
let path = null;
let selector = '';
let secretsData = new Map();
if(secretsMethod === SECRETS_METHOD.Write) {
if (pathParts.length < 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
let writeSelectorParts = null;
let finalSelector = [];
for (let index = 0; index < pathParts.length; index++) {
const element = pathParts[index];
if(index == 0) {
path = element;
continue;
}
//if a secret is for write, it should be saperated by "="
writeSelectorParts = element
.split("=")
.map(part => part.trim())
.filter(part => part.length !== 0);

const [writeSelectorKey, writeSelectorValue] = writeSelectorParts;

/** @type {any} */
const selectorAst = jsonata(writeSelectorKey).ast();
const writeSelector = writeSelectorKey.replace(new RegExp('"', 'g'), '');

if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`Write Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}

if(writeSelector !=='\\') {
finalSelector.push(writeSelector);
secretsData.set(writeSelector, writeSelectorValue);
}
}
selector = finalSelector.join('__');
} else {
if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}

const [path, selectorQuoted] = pathParts;
path = pathParts[0];
const selectorQuoted = pathParts[1];

/** @type {any} */
const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
/** @type {any} */
const selectorAst = jsonata(selectorQuoted).ast();
selector = selectorQuoted.replace(new RegExp('"', 'g'), '');

if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`Read Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
}

let envVarName = outputVarName;
Expand All @@ -18698,7 +18754,9 @@ function parseSecretsInput(secretsInput) {
path,
envVarName,
outputVarName,
selector
selector,
secretsMethod,
secretsData
});
}
return output;
Expand Down Expand Up @@ -18942,6 +19000,7 @@ const jsonata = __nccwpck_require__(4245);
* @typedef {Object} SecretRequest
* @property {string} path
* @property {string} selector
* @property {Map} secretsData
*/

/**
Expand Down Expand Up @@ -19002,6 +19061,43 @@ async function getSecrets(secretRequests, client) {
return results;
}

/**
* @template TRequest
* @param {Array<TRequest>} secretRequests
* @param {import('got').Got} client
* @return {Promise<SecretResponse<TRequest>[]>}
*/
async function writeSecrets(secretRequests, client) {
const results = [];
for (const secretRequest of secretRequests) {
let { path, selector, secretsData } = secretRequest;
const requestPath = `v1/${path}`;
let body;
const jsonata = {};
for (const [key, value] of secretsData) {
jsonata[key] = value;
}

try {
const result = await client.post(requestPath,{
json: {
data: jsonata
}
});
body = result.body;
} catch (error) {
throw error
}
//body = JSON.parse(body); //body.request_id
results.push({
request: secretRequest,
value: 'SUCCESS',
cachedResponse: false
});
}
return results;
}

/**
* Uses a Jsonata selector retrieve a bit of data from the result
* @param {object} data
Expand Down Expand Up @@ -19037,6 +19133,7 @@ async function selectData(data, selector) {

module.exports = {
getSecrets,
writeSecrets,
selectData
}

Expand Down
Loading