mirror of
https://github.com/hashicorp/vault-action.git
synced 2026-07-26 00:13:16 +03:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb61006b6d | |||
| 14a4a058b4 | |||
| 2d9c2b9f1b | |||
| d27529ebde | |||
| cd5a8995f3 | |||
| 72c092c8af | |||
| 9c1dce9ef6 | |||
| 9866ce3e18 | |||
| 1f5b7d55d8 | |||
| d1655aec40 | |||
| 1d767e3957 | |||
| c253c155ba | |||
| 3a9100e7d5 | |||
| 256bfb9e6a | |||
| 3bbbc68bd0 | |||
| 74bc2a617b | |||
| 76780d43f5 | |||
| 46540966f1 | |||
| cc5270ec14 | |||
| 130d1f5f4f | |||
| d34ee148bc | |||
| 77bab83f42 | |||
| 5e3dd4f01b | |||
| 7318a98db7 | |||
| b08bc4993d | |||
| 579f9fd8c2 | |||
| 1226471c04 | |||
| fdaeeffa26 | |||
| 0f409d4023 | |||
| 8fa61e9099 | |||
| 132f1c6930 | |||
| f558cc7838 | |||
| d0e05af6a3 | |||
| 1f8e723e55 | |||
| 32d00a142f | |||
| 32838a0d48 | |||
| ed59bea637 | |||
| 2537991e61 | |||
| 7d98524254 | |||
| f380d921ae | |||
| ae2f303e37 | |||
| 55a11671e0 | |||
| ad04ab9377 | |||
| 2dcbd17a34 | |||
| d2b5111993 | |||
| f5817917e5 | |||
| bbf40c0251 | |||
| cebb457349 | |||
| 6ee4dd3797 | |||
| a502b7fa66 | |||
| abba4a3980 | |||
| e025870ee9 | |||
| 4f536680b6 | |||
| fd9b31c94c | |||
| 8e9712e63b | |||
| 25737aea2b | |||
| c2b108a1cb | |||
| 7c940db65a | |||
| d9ec4185f7 | |||
| 203b1c7ae9 | |||
| 0580f85e79 | |||
| b222012f94 | |||
| 40fb8d7236 | |||
| 4aed62f922 | |||
| 4679f8aa3c | |||
| 2f64a97498 | |||
| 25c4aec690 | |||
| c14a190aaa | |||
| 843e7fa30a | |||
| 876cdcfdd3 | |||
| 67281159df | |||
| 61dd38f491 | |||
| 168f9696e8 | |||
| c502100fbe | |||
| b8c90c7243 | |||
| d5a69ceca1 | |||
| f3e4110f8d | |||
| 8c00f7e4fd | |||
| ab957d2aec | |||
| 0451f06f9f | |||
| b6210c5a51 | |||
| 09740f679f | |||
| 72c7a899ca | |||
| 0cf3bd6a39 | |||
| 0723fd7348 | |||
| 0405c26bc9 | |||
| 2eea93c25e | |||
| 4d9cf786d6 | |||
| 575042000a | |||
| cc8ad16bec | |||
| cd1614aee3 | |||
| f3a53220d9 | |||
| 3526e1be65 | |||
| f60544fbda | |||
| 9e8f22534f | |||
| 8417c61f8a | |||
| 0cca23248c | |||
| 8dad53ed10 | |||
| 937d792a86 | |||
| 5e5c06a3c8 | |||
| ff547e1516 | |||
| 1d4917ec58 | |||
| 2fb78ab91e | |||
| c261a0ad69 | |||
| 6ac603b23e | |||
| 638924aeb4 | |||
| 668d59f96b | |||
| 208b49ff6c | |||
| 53c0f5bcca | |||
| b4cef0fa36 | |||
| 7bc0a10ff8 | |||
| e5febde8e7 | |||
| 2ca76a4465 | |||
| 5b42d37bab | |||
| 124e6eecdb | |||
| 43c4212f70 | |||
| d4487e4dd0 | |||
| c7f47bbfcf | |||
| 11e2f12f99 | |||
| 6c64e41adc | |||
| 2f76ad395b | |||
| 307c9ae581 | |||
| b35ef16370 | |||
| 952d5d48e4 | |||
| 649323229c | |||
| 35be1a3b49 | |||
| d81a7a778d | |||
| 3a8d733522 | |||
| 1db79efc8e | |||
| 488fc1ce12 | |||
| 722353f009 | |||
| 20a8e522c2 | |||
| 35e0a09f2c | |||
| 5cb0f68cdc | |||
| 2977272bf8 | |||
| 2c1083c570 | |||
| 401e08df03 | |||
| e27b45646f | |||
| 76ca623af3 | |||
| c8b383ee4e | |||
| a214c8c65a | |||
| f7f0d5a289 | |||
| 633de935e7 | |||
| 5de67274a5 | |||
| e031fa076e | |||
| e5ef6c9c11 | |||
| 6cf013b5c0 | |||
| 098ee568ed | |||
| e055dfd1a4 | |||
| b3a02289e7 | |||
| 683c9b2557 | |||
| e59c94afbe | |||
| cd01494ae2 | |||
| a2d854da53 | |||
| d1321fc438 | |||
| 5fb792e09f | |||
| c63c61cd7e | |||
| 944a2bea12 | |||
| 2f554c06ec | |||
| 25ecb3dd46 | |||
| 734d76c388 | |||
| 22e3f3e09e | |||
| 727494f451 | |||
| fa68886b06 | |||
| a7527a3e8a | |||
| 9878eba70a | |||
| 83d944ba1a | |||
| 567ec72c33 | |||
| 5c464962be | |||
| ea29244204 | |||
| 5de02d5a14 | |||
| 8845b5c724 | |||
| c80d0b130c | |||
| 01bb0f9bc9 | |||
| 5a70924133 | |||
| b5fdbf352b | |||
| 268a32f886 | |||
| f2b4d0b645 | |||
| 675d33e3da | |||
| 50ece41861 | |||
| ed8303ca53 | |||
| 0e719ef42a | |||
| 795c9eddca | |||
| 9e8c0bad27 | |||
| 751a3d19ad | |||
| cc7ceeef06 | |||
| fa23030c10 | |||
| 4561f9e26e | |||
| cb2908ac94 | |||
| 198a7ed7d2 | |||
| 4ab6f6070f | |||
| bef2eb0b90 | |||
| 0ece1da433 | |||
| 7fb0d673d1 | |||
| 4edbc9a77a | |||
| 5357098084 | |||
| 3dfe7ff808 | |||
| 1049321b7a | |||
| ec10b5e257 |
@@ -1 +0,0 @@
|
||||
ko_fi: richicoder
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: RichiCoder1
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
The yaml of the `vault-action` step, with any sensitive information masked or removed.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Log Output**
|
||||
For the most verbose logs, [add a secret called `ACTIONS_STEP_DEBUG` with the value `true`](https://github.com/actions/toolkit/blob/main/docs/action-debugging.md). Then, re-run the workflow if possible and post the *raw logs* for the step here with any sensitive information masked or removed.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT] "
|
||||
labels: enhancement
|
||||
assignees: RichiCoder1
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
+254
-108
@@ -1,168 +1,314 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
- main
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: setup npm cache
|
||||
uses: actions/cache@v1
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Setup NPM Cache
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: npm install
|
||||
|
||||
- name: NPM Install
|
||||
run: npm ci
|
||||
- name: npm build
|
||||
|
||||
- name: NPM Build
|
||||
run: npm run build
|
||||
- name: npm run test
|
||||
|
||||
- name: NPM Run Test
|
||||
run: npm run test
|
||||
|
||||
integration:
|
||||
integrationOSS:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
vaultBasic:
|
||||
image: vault:1.2.3
|
||||
ports:
|
||||
- 8200/tcp
|
||||
env:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: testtoken
|
||||
options: --cap-add=IPC_LOCK
|
||||
vaultEnterprise:
|
||||
image: hashicorp/vault-enterprise:1.3.0_ent
|
||||
ports:
|
||||
- 8200/tcp
|
||||
env:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: testtoken
|
||||
options: --cap-add=IPC_LOCK
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Run docker-compose
|
||||
run: docker-compose up -d vault
|
||||
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Setup NPM Cache
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: NPM Install
|
||||
run: npm ci
|
||||
|
||||
- name: NPM Build
|
||||
run: npm run build
|
||||
|
||||
- name: NPM Run test;integration:basic
|
||||
run: npm run test:integration:basic
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: 8200
|
||||
CI: true
|
||||
|
||||
integrationEnterprise:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: setup npm cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: npm install
|
||||
run: npm ci
|
||||
- name: npm build
|
||||
run: npm run build
|
||||
- name: npm run test:integration:basic
|
||||
run: npm run test:integration:basic
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: ${{ job.services.vaultBasic.ports[8200] }}
|
||||
CI: true
|
||||
- name: npm run test:integration:enterprise
|
||||
run: npm run test:integration:enterprise
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: ${{ job.services.vaultEnterprise.ports[8200] }}
|
||||
CI: true
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Run docker-compose
|
||||
run: docker-compose up -d vault-enterprise
|
||||
env:
|
||||
VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }}
|
||||
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Setup NPM Cache
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: NPM Install
|
||||
run: npm ci
|
||||
|
||||
- name: NPM Build
|
||||
run: npm run build
|
||||
|
||||
- name: NPM Run test:integration:enterprise
|
||||
run: npm run test:integration:enterprise
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: 8200
|
||||
CI: true
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
vault:
|
||||
image: vault:1.3.0
|
||||
ports:
|
||||
- 8200/tcp
|
||||
env:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: testtoken
|
||||
options: --cap-add=IPC_LOCK
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: setup npm cache
|
||||
uses: actions/cache@v1
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Run docker-compose
|
||||
run: docker-compose up -d vault
|
||||
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Setup NPM Cache
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: npm install
|
||||
|
||||
- name: NPM Install
|
||||
run: npm ci
|
||||
- name: npm build
|
||||
|
||||
- name: NPM Build
|
||||
run: npm run build
|
||||
- name: setup vault
|
||||
|
||||
- name: Setup Vault
|
||||
run: node ./integrationTests/e2e/setup.js
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: ${{ job.services.vault.ports[8200] }}
|
||||
- name: use vault action (default K/V version 2)
|
||||
VAULT_PORT: 8200
|
||||
|
||||
- name: Test Vault Action (default KV V2)
|
||||
uses: ./
|
||||
id: kv-secrets
|
||||
with:
|
||||
url: http://localhost:${{ job.services.vault.ports[8200] }}
|
||||
url: http://localhost:8200
|
||||
token: testtoken
|
||||
secrets: |
|
||||
test secret ;
|
||||
test secret | NAMED_SECRET ;
|
||||
nested/test otherSecret ;
|
||||
- name: use vault action (custom K/V version 1)
|
||||
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:${{ job.services.vault.ports[8200] }}
|
||||
url: http://localhost:8200
|
||||
token: testtoken
|
||||
path: my-secret
|
||||
kv-version: 1
|
||||
secrets: |
|
||||
test altSecret ;
|
||||
test altSecret | NAMED_ALTSECRET ;
|
||||
nested/test otherAltSecret ;
|
||||
- name: use vault action (using cubbyhole engine)
|
||||
my-secret/test altSecret ;
|
||||
my-secret/test altSecret | NAMED_ALTSECRET ;
|
||||
my-secret/nested/test otherAltSecret ;
|
||||
|
||||
- name: Test Vault Action (cubbyhole)
|
||||
uses: ./
|
||||
with:
|
||||
url: http://localhost:${{ job.services.vault.ports[8200] }}
|
||||
url: http://localhost:8200
|
||||
token: testtoken
|
||||
secrets: |
|
||||
/cubbyhole/test foo ;
|
||||
/cubbyhole/test zip | NAMED_CUBBYSECRET ;
|
||||
- name: verify
|
||||
run: npm run test:e2e
|
||||
|
||||
publish:
|
||||
if: github.event_name == 'push' && contains(github.ref, 'master')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, integration, e2e]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: Verify Vault Action Outputs
|
||||
run: npm run test:integration:e2e
|
||||
env:
|
||||
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
||||
|
||||
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 1/2)
|
||||
uses: ./
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: setup npm cache
|
||||
uses: actions/cache@v1
|
||||
url: http://localhost:8200/
|
||||
token: testtoken
|
||||
secrets: |
|
||||
secret/data/test secret | SUBSEQUENT_TEST_SECRET;
|
||||
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 2/2)
|
||||
uses: ./
|
||||
with:
|
||||
url: http://localhost:8200/
|
||||
token: testtoken
|
||||
secrets: |
|
||||
secret/data/subsequent-test secret | SUBSEQUENT_TEST_SECRET;
|
||||
|
||||
e2e-tls:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Run docker-compose
|
||||
run: docker-compose up -d vault-tls
|
||||
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Setup NPM Cache
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: npm install
|
||||
run: npm ci
|
||||
- name: release
|
||||
if: success() && endsWith(github.ref, 'master')
|
||||
run: npx semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: NPM Install
|
||||
run: npm ci
|
||||
|
||||
- name: NPM Build
|
||||
run: npm run build
|
||||
|
||||
- name: Setup Vault
|
||||
run: node ./integrationTests/e2e-tls/setup.js
|
||||
env:
|
||||
VAULT_HOST: localhost
|
||||
VAULT_PORT: 8200
|
||||
VAULTCA: ${{ secrets.VAULTCA }}
|
||||
VAULT_CLIENT_CERT: ${{ secrets.VAULT_CLIENT_CERT }}
|
||||
VAULT_CLIENT_KEY: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||
|
||||
- name: Test Vault Action (default KV V2)
|
||||
uses: ./
|
||||
id: kv-secrets
|
||||
with:
|
||||
url: https://localhost:8200
|
||||
token: ${{ env.VAULT_TOKEN }}
|
||||
caCertificate: ${{ secrets.VAULTCA }}
|
||||
clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }}
|
||||
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||
secrets: |
|
||||
secret/data/test secret ;
|
||||
secret/data/test secret | NAMED_SECRET ;
|
||||
secret/data/nested/test otherSecret ;
|
||||
|
||||
- name: Test Vault Action (tlsSkipVerify)
|
||||
uses: ./
|
||||
with:
|
||||
url: https://localhost:8200
|
||||
token: ${{ env.VAULT_TOKEN }}
|
||||
tlsSkipVerify: true
|
||||
clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }}
|
||||
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||
secrets: |
|
||||
secret/data/tlsSkipVerify skip ;
|
||||
|
||||
- name: Test Vault Action (default KV V1)
|
||||
uses: ./
|
||||
with:
|
||||
url: https://localhost:8200
|
||||
token: ${{ env.VAULT_TOKEN }}
|
||||
caCertificate: ${{ secrets.VAULTCA }}
|
||||
clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }}
|
||||
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||
secrets: |
|
||||
my-secret/test altSecret ;
|
||||
my-secret/test altSecret | NAMED_ALTSECRET ;
|
||||
my-secret/nested/test otherAltSecret ;
|
||||
|
||||
- name: Test Vault Action (cubbyhole)
|
||||
uses: ./
|
||||
with:
|
||||
url: https://localhost:8200
|
||||
token: ${{ env.VAULT_TOKEN }}
|
||||
secrets: |
|
||||
/cubbyhole/test foo ;
|
||||
/cubbyhole/test zip | NAMED_CUBBYSECRET ;
|
||||
caCertificate: ${{ secrets.VAULTCA }}
|
||||
clientCertificate: ${{ secrets.VAULT_CLIENT_CERT }}
|
||||
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||
|
||||
- name: Verify Vault Action Outputs
|
||||
run: npm run test:integration:e2e-tls
|
||||
env:
|
||||
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
||||
|
||||
# Removing publish step for now.
|
||||
# publish:
|
||||
# if: github.event_name == 'push' && contains(github.ref, 'main')
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [build, integration, e2e]
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: actions/setup-node@v3
|
||||
# with:
|
||||
# node-version: '16.14.0'
|
||||
# - name: setup npm cache
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
# - name: npm install
|
||||
# run: npm ci
|
||||
# - name: release
|
||||
# if: success() && endsWith(github.ref, 'main')
|
||||
# run: npx semantic-release
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: JIRA Sync
|
||||
on:
|
||||
issues:
|
||||
types: [opened, closed, deleted, reopened]
|
||||
pull_request_target:
|
||||
types: [opened, closed, reopened]
|
||||
issue_comment: # Also triggers when commenting on a PR from the conversation view
|
||||
types: [created]
|
||||
jobs:
|
||||
sync:
|
||||
uses: hashicorp/vault-workflows-common/.github/workflows/jira.yaml@main
|
||||
secrets:
|
||||
JIRA_SYNC_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }}
|
||||
JIRA_SYNC_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }}
|
||||
JIRA_SYNC_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }}
|
||||
with:
|
||||
teams-array: '["applications-eco"]'
|
||||
@@ -0,0 +1,24 @@
|
||||
# This is a sample workflow to help test contributions
|
||||
# Change the branch name, url and token to fit with your own environment
|
||||
|
||||
# Use 'on: push' instead of 'on: local-test' if you wish to run the test on github
|
||||
# If running locally with act, run the workflow with 'act local-test'
|
||||
|
||||
# Don't forget to revert the file changes and invalidate any tokens that were committed before opening a pull-request
|
||||
on: local-test
|
||||
|
||||
name: local-test
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: local-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Import Secrets
|
||||
uses: hashicorp/vault-action@YOUR_BRANCH_NAME
|
||||
with:
|
||||
url: http://localhost:8200
|
||||
method: token
|
||||
token: testtoken
|
||||
secrets: |
|
||||
secret/data/test secret | SAMPLE_SECRET;
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--runInBand"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-integration-tests",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--runInBand",
|
||||
"--config=${workspaceFolder}/integrationTests/basic/jest.config.js"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"markdown-toc.depthFrom": 2
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
## Unreleased
|
||||
|
||||
## 2.6.0 (June 7, 2023)
|
||||
|
||||
Features:
|
||||
|
||||
* Add ability to set the `vault_token` output to contain the Vault token after authentication [GH-441](https://github.com/hashicorp/vault-action/pull/441)
|
||||
* Add support for userpass and ldap authentication methods [GH-440](https://github.com/hashicorp/vault-action/pull/440)
|
||||
* Define an output, `errorMessage`, for vault-action's error messages so subsequent steps can read the errors [GH-446](https://github.com/hashicorp/vault-action/pull/446)
|
||||
|
||||
Bugs:
|
||||
|
||||
* Handle undefined response in getSecrets error handler [GH-431](https://github.com/hashicorp/vault-action/pull/431)
|
||||
|
||||
## 2.5.0 (Jan 26th, 2023)
|
||||
|
||||
Features:
|
||||
|
||||
* Adds ability to automatically decode secrets from base64, hex, and utf8 encodings. [GH-408](https://github.com/hashicorp/vault-action/pull/408)
|
||||
|
||||
Improvements:
|
||||
|
||||
* Improves error messages for Vault authentication failures [GH-409](https://github.com/hashicorp/vault-action/pull/409)
|
||||
* bump jest from 28.1.1 to 29.3.1 [GH-397](https://github.com/hashicorp/vault-action/pull/397)
|
||||
* bump @types/jest from 28.1.3 to 29.2.6 [GH-397](https://github.com/hashicorp/vault-action/pull/397), [GH-413](https://github.com/hashicorp/vault-action/pull/413)
|
||||
* bump jsrsasign from 10.5.27 to 10.6.1 [GH-401](https://github.com/hashicorp/vault-action/pull/401)
|
||||
* bump json5 from 2.2.1 to 2.2.3 [GH-404](https://github.com/hashicorp/vault-action/pull/404)
|
||||
* bump minimatch from 3.0.4 to 3.1.2 [GH-410](https://github.com/hashicorp/vault-action/pull/410)
|
||||
|
||||
## 2.4.3 (Nov 8th, 2022)
|
||||
|
||||
Improvements:
|
||||
|
||||
* bump jest-when from 3.5.1 to 3.5.2 [GH-388](https://github.com/hashicorp/vault-action/pull/388)
|
||||
* bump semantic-release from 19.0.3 to 19.0.5 [GH-360](https://github.com/hashicorp/vault-action/pull/360)
|
||||
* bump jsrsasign from 10.5.25 to 10.5.27 [GH-358](https://github.com/hashicorp/vault-action/pull/358)
|
||||
* bump @actions/core from 1.9.0 to 1.10.0 [GH-371](https://github.com/hashicorp/vault-action/pull/371)
|
||||
* update runtime to node16 for action [GH-375](https://github.com/hashicorp/vault-action/pull/375)
|
||||
|
||||
## 2.4.2 (Aug 15, 2022)
|
||||
|
||||
Bugs:
|
||||
|
||||
* Errors due to replication delay for tokens will now be retried [GH-333](https://github.com/hashicorp/vault-action/pull/333)
|
||||
|
||||
Improvements:
|
||||
* bump got from 11.5.1 to 11.8.5 [GH-344](https://github.com/hashicorp/vault-action/pull/344)
|
||||
|
||||
## 2.4.1 (April 28th, 2022)
|
||||
|
||||
Improvements:
|
||||
* Make secrets parameter optional [GH-299](https://github.com/hashicorp/vault-action/pull/299)
|
||||
* auth/jwt: make "role" input optional [GH-291](https://github.com/hashicorp/vault-action/pull/291)
|
||||
* Write a better error message when secret not found [GH-306](https://github.com/hashicorp/vault-action/pull/306)
|
||||
* bump jest-when from 2.7.2 to 3.5.1 [GH-294](https://github.com/hashicorp/vault-action/pull/294)
|
||||
* bump node-fetch from 2.6.1 to 2.6.7 [GH-308](https://github.com/hashicorp/vault-action/pull/308)
|
||||
* bump @types/jest from 26.0.23 to 27.4.1 [GH-297](https://github.com/hashicorp/vault-action/pull/297)
|
||||
* bump trim-off-newlines from 1.0.1 to 1.0.3 [GH-309](https://github.com/hashicorp/vault-action/pull/309)
|
||||
* bump moment from 2.28.0 to 2.29.2 [GH-304](https://github.com/hashicorp/vault-action/pull/304)
|
||||
* bump @types/got from 9.6.11 to 9.6.12 [GH-266](https://github.com/hashicorp/vault-action/pull/266)
|
||||
|
||||
## 2.4.0 (October 21st, 2021)
|
||||
|
||||
Features:
|
||||
* GitHub provided JWT auth is now supported [GH-257](https://github.com/hashicorp/vault-action/pull/257)
|
||||
|
||||
## 2.3.1 (August 23rd, 2021)
|
||||
|
||||
Improvements:
|
||||
* bump normalize-url from 4.5.0 to 4.5.1 [GH-227](https://github.com/hashicorp/vault-action/pull/227)
|
||||
* bump path-parse from 1.0.6 to 1.0.7 [GH-239](https://github.com/hashicorp/vault-action/pull/239)
|
||||
|
||||
## 2.3.0 (June 23rd, 2021)
|
||||
|
||||
Features:
|
||||
* K8s auth method is now supported [GH-218](https://github.com/hashicorp/vault-action/pull/218)
|
||||
* Custom auth method mount points is configurable [GH-218](https://github.com/hashicorp/vault-action/pull/218)
|
||||
|
||||
## 2.2.0 (May 6th, 2021)
|
||||
|
||||
Security:
|
||||
* multi-line secrets are now properly masked in logs [GH-208](https://github.com/hashicorp/vault-action/pull/208)
|
||||
[CVE-2021-32074](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-32074)
|
||||
|
||||
Features:
|
||||
* JWT auth method is now supported [GH-188](https://github.com/hashicorp/vault-action/pull/188)
|
||||
|
||||
## 2.1.2 (January 21st, 2021)
|
||||
|
||||
Bugs:
|
||||
* fixed bug where newlines were being rendered for multi-line secrets [GH-173](https://github.com/hashicorp/vault-action/pull/173)
|
||||
|
||||
## 2.1.1 (December 15th, 2020)
|
||||
|
||||
Improvements:
|
||||
* bump jest from 26.5.0 to 26.6.3 [GH-150](https://github.com/hashicorp/vault-action/pull/150)
|
||||
* bump semantic-release from 17.1.2 to 17.3.0 [GH-158](https://github.com/hashicorp/vault-action/pull/158)
|
||||
* bump got from 11.7.0 to 11.8.1 [GH-163](https://github.com/hashicorp/vault-action/pull/163)
|
||||
* bump @types/jest from 26.0.14 to 26.0.19 [GH-164](https://github.com/hashicorp/vault-action/pull/164)
|
||||
* bump ini from 1.3.5 to 1.3.8 [GH-167](https://github.com/hashicorp/vault-action/pull/167)
|
||||
|
||||
## 2.1.0 (October 6th, 2020)
|
||||
|
||||
Features:
|
||||
* Added `exportToken` to share the Vault token as an environment variable [GH-127](https://github.com/hashicorp/vault-action/pull/127)
|
||||
|
||||
Security:
|
||||
* `action/core` updated to 1.2.6 to address minor CVE [GH-130](https://github.com/hashicorp/vault-action/pull/130)
|
||||
|
||||
## 2.0.1 (September 15th, 2020)
|
||||
|
||||
Improvements:
|
||||
* bump node-fetch from 2.6.0 to 2.6.1 [GH-110](https://github.com/hashicorp/vault-action/pull/110)
|
||||
* bump lodash from 4.17.15 to 4.17.20 [GH-111](https://github.com/hashicorp/vault-action/pull/111)
|
||||
* bump npm from 6.14.4 to 6.14.8 [GH-112](https://github.com/hashicorp/vault-action/pull/112)
|
||||
* bump @types/jest from 25.1.5 to 26.0.13 [GH-114](https://github.com/hashicorp/vault-action/pull/114)
|
||||
* bump @actions/core from 1.2.3 to 1.2.5 [GH-115](https://github.com/hashicorp/vault-action/pull/115)
|
||||
* bump jest from 25.2.7 to 26.4.2 [GH-116](https://github.com/hashicorp/vault-action/pull/116)
|
||||
* bump got from 11.5.1 to 11.6.2 [GH-117](https://github.com/hashicorp/vault-action/pull/117)
|
||||
* update jsonata to version 1.8.3 [GH-118](https://github.com/hashicorp/vault-action/pull/118)
|
||||
* update dev dependencies [GH-119](https://github.com/hashicorp/vault-action/pull/119)
|
||||
* update kind-of to version 6.0.3 [GH-120](https://github.com/hashicorp/vault-action/pull/120)
|
||||
|
||||
## 2.0.0 (August 24th, 2020)
|
||||
|
||||
Features:
|
||||
|
||||
* Added TLS and mTLS support [GH-97](https://github.com/hashicorp/vault-action/pull/97)
|
||||
|
||||
Improvements:
|
||||
|
||||
* Breaking change: removed KV specific parameters and generalized secret UX [GH-102](https://github.com/hashicorp/vault-action/pull/102)
|
||||
|
||||
Bugs:
|
||||
@@ -1,8 +1,41 @@
|
||||
# vault-action
|
||||
# Vault GitHub Action
|
||||
|
||||
---
|
||||
|
||||
**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).
|
||||
|
||||
---
|
||||
|
||||
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).
|
||||
<!-- TOC -->
|
||||
|
||||
- [Vault GitHub Action](#vault-github-action)
|
||||
- [Example Usage](#example-usage)
|
||||
- [Authentication Methods](#authentication-methods)
|
||||
- [JWT with GitHub OIDC Tokens](#jwt-with-github-oidc-tokens)
|
||||
- [AppRole](#approle)
|
||||
- [Token](#token)
|
||||
- [GitHub](#github)
|
||||
- [JWT with OIDC Provider](#jwt-with-oidc-provider)
|
||||
- [Kubernetes](#kubernetes)
|
||||
- [Userpass](#userpass)
|
||||
- [Ldap](#ldap)
|
||||
- [Other Auth Methods](#other-auth-methods)
|
||||
- [Key Syntax](#key-syntax)
|
||||
- [Simple Key](#simple-key)
|
||||
- [Set Output Variable Name](#set-output-variable-name)
|
||||
- [Multiple Secrets](#multiple-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)
|
||||
- [Namespace](#namespace)
|
||||
- [Reference](#reference)
|
||||
- [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs)
|
||||
- [Normalization](#normalization)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Example Usage
|
||||
|
||||
@@ -13,70 +46,304 @@ jobs:
|
||||
steps:
|
||||
# ...
|
||||
- name: Import Secrets
|
||||
uses: RichiCoder1/vault-action
|
||||
id: import-secrets
|
||||
uses: hashicorp/vault-action@v2
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
token: ${{ secrets.VaultToken }}
|
||||
token: ${{ secrets.VAULT_TOKEN }}
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
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
|
||||
# ...
|
||||
```
|
||||
|
||||
## Authentication method
|
||||
|
||||
While most workflows will likely use a vault token, you can also use an `approle` to authenticate with vaule. You can configure which by using the `method` parameter:
|
||||
|
||||
- **token**: (by default) you must provide a token parameter
|
||||
Retrieved secrets are available as environment variables or outputs for subsequent steps:
|
||||
```yaml
|
||||
...
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
token: ${{ secrets.VaultToken }}
|
||||
#...
|
||||
- name: Step following 'Import Secrets'
|
||||
run: |
|
||||
ACCESS_KEY_ID = "${{ env.AWS_ACCESS_KEY_ID }}"
|
||||
SECRET_ACCESS_KEY = "${{ steps.import-secrets.outputs.AWS_SECRET_ACCESS_KEY }}"
|
||||
# ...
|
||||
```
|
||||
- **approle**: you must provide a roleId & secretId parameter
|
||||
|
||||
If your project needs a format other than env vars and step outputs, you can use additional steps to transform them into the desired format.
|
||||
For example, a common pattern is to save all the secrets in a JSON file:
|
||||
```yaml
|
||||
#...
|
||||
- name: Step following 'Import Secrets'
|
||||
run: |
|
||||
touch secrets.json
|
||||
echo "${{ toJson(steps.import-secrets.outputs) }}" >> secrets.json
|
||||
# ...
|
||||
```
|
||||
|
||||
Which with our example would yield a file containing:
|
||||
```json
|
||||
{
|
||||
"ACCESS_KEY_ID": "MY_KEY_ID",
|
||||
"SECRET_ACCESS_KEY": "MY_SECRET_KEY",
|
||||
"NPM_TOKEN": "MY_NPM_TOKEN"
|
||||
}
|
||||
```
|
||||
|
||||
Note that all secrets are masked so programs need to read the file themselves otherwise all values will be replaced with a `***` placeholder.
|
||||
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
Consider using a [Vault authentication method](https://www.vaultproject.io/docs/auth) such as the JWT auth method with
|
||||
[GitHub OIDC tokens](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) or the AppRole auth method. You can configure which by using the `method` parameter.
|
||||
|
||||
### JWT with GitHub OIDC Tokens
|
||||
|
||||
You can configure trust between a GitHub Actions workflow
|
||||
and Vault using the
|
||||
[GitHub's OIDC provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect).
|
||||
Each GitHub Actions workflow receives an auto-generated OIDC token with claims
|
||||
to establish the identity of the workflow.
|
||||
|
||||
__Vault Configuration__
|
||||
|
||||
<details>
|
||||
<summary>Click to toggle instructions for configuring Vault.</summary>
|
||||
|
||||
Set up Vault with the [JWT auth method](https://www.vaultproject.io/api/auth/jwt#configure).
|
||||
Pass the following parameters to your auth method configuration:
|
||||
|
||||
- `oidc_discovery_url`: `https://token.actions.githubusercontent.com`
|
||||
- `bound_issuer`: `https://token.actions.githubusercontent.com`
|
||||
|
||||
|
||||
Configure a [Vault role](https://www.vaultproject.io/api/auth/jwt#create-role) for the auth method.
|
||||
|
||||
- `role_type`: `jwt`
|
||||
|
||||
- `bound_audiences`: `"https://github.com/<org>"`. Update this parameter if
|
||||
you change the `aud` claim in the GitHub OIDC token via the
|
||||
`jwtGithubAudience` parameter in the action config.
|
||||
|
||||
- `user_claim`: Set this to a claim name (e.g., `repository`) in the
|
||||
[GitHub OIDC token](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
|
||||
|
||||
- `bound_claims` OR `bound_subject`: match on [GitHub subject claims](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims).
|
||||
|
||||
- For wildcard (non-exact) matches, use `bound_claims`.
|
||||
|
||||
- `bound_claims_type`: `glob`
|
||||
|
||||
- `bound_claims`: JSON object. Maps one or more claim names to corresponding wildcard values.
|
||||
```json
|
||||
{"sub": "repo:<orgName>/*"}
|
||||
```
|
||||
|
||||
- For exact matches, use `bound_subject`.
|
||||
|
||||
- `bound_claims_type`: `string`
|
||||
|
||||
- `bound_subject`: Must exactly match the `sub` claim in the OIDC token.
|
||||
```plaintext
|
||||
repo:<orgName/repoName>:ref:refs/heads/branchName
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
__GitHub Actions Workflow__
|
||||
|
||||
In the GitHub Actions workflow, the workflow needs permissions to read contents
|
||||
and write the ID token.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
retrieve-secret:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
```
|
||||
|
||||
In the action, provide the name of the Vault role you created to the `role` parameter.
|
||||
You can optionally set the `jwtGithubAudience` parameter to change the `aud`
|
||||
claim from its default.
|
||||
|
||||
```yaml
|
||||
...
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
role: <Vault JWT Auth Role Name>
|
||||
method: jwt
|
||||
jwtGithubAudience: sigstore # set the GitHub token's aud claim
|
||||
```
|
||||
|
||||
### AppRole
|
||||
|
||||
The [AppRole auth method](https://www.vaultproject.io/docs/auth/approle) allows
|
||||
your GitHub Actions workflow to authenticate to Vault with a pre-defined role.
|
||||
Set the role ID and secret ID as GitHub secrets and pass them to the
|
||||
`roleId` and `secretId` parameters.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: approle
|
||||
roleId: ${{ secrets.roleId }}
|
||||
secretId: ${{ secrets.secretId }}
|
||||
roleId: ${{ secrets.VAULT_ROLE_ID }}
|
||||
secretId: ${{ secrets.VAULT_SECRET_ID }}
|
||||
```
|
||||
|
||||
### Token
|
||||
|
||||
For the default method of authenticating to Vault,
|
||||
use a [Vault token](https://www.vaultproject.io/docs/concepts/tokens).
|
||||
Set the Vault token as a GitHub secret and pass
|
||||
it to the `token` parameter.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
token: ${{ secrets.VAULT_TOKEN }}
|
||||
```
|
||||
|
||||
### GitHub
|
||||
|
||||
The [GitHub auth method](https://www.vaultproject.io/docs/auth/github)
|
||||
requires `read:org` permissions for authentication. The auto-generated `GITHUB_TOKEN`
|
||||
created for projects does not have these permissions and GitHub does not allow this
|
||||
token's permissions to be modified. A new GitHub Token secret must be created with
|
||||
`read:org` permissions to use this authentication method.
|
||||
|
||||
Pass the GitHub token as a GitHub secret into the `githubToken` parameter.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: github
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### JWT with OIDC Provider
|
||||
|
||||
You can configure trust between your own OIDC Provider and Vault
|
||||
with the JWT auth method. Provide a `role` & `jwtPrivateKey` parameters,
|
||||
additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: jwt
|
||||
role: <Vault JWT Auth Role Name>
|
||||
jwtPrivateKey: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||
jwtKeyPassword: ${{ secrets.JWT_KEY_PASS }}
|
||||
jwtTtl: 3600 # 1 hour, default value
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Consider the [Kubernetes auth method](https://www.vaultproject.io/docs/auth/kubernetes)
|
||||
when using self-hosted runners on Kubernetes. You must provide the `role` parameter
|
||||
for the Vault role associated with the Kubernetes auth method.
|
||||
You can optionally override the `kubernetesTokenPath` parameter for
|
||||
custom-mounted serviceAccounts.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: kubernetes
|
||||
role: <Vault Kubernetes Auth Role Name>
|
||||
kubernetesTokenPath: /var/run/secrets/kubernetes.io/serviceaccount/token # default token path
|
||||
```
|
||||
|
||||
### Userpass
|
||||
|
||||
The [Userpass auth method](https://developer.hashicorp.com/vault/docs/auth/userpass) allows
|
||||
your GitHub Actions workflow to authenticate to Vault with a username and password.
|
||||
Set the username and password as GitHub secrets and pass them to the
|
||||
`username` and `password` parameters.
|
||||
|
||||
This is not the same as ldap or okta auth methods.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: userpass
|
||||
username: ${{ secrets.VAULT_USERNAME }}
|
||||
password: ${{ secrets.VAULT_PASSWORD }}
|
||||
```
|
||||
|
||||
### Ldap
|
||||
|
||||
The [LDAP auth method](https://developer.hashicorp.com/vault/docs/auth/ldap) allows
|
||||
your GitHub Actions workflow to authenticate to Vault with a username and password inturn verfied with ldap servers.
|
||||
Set the username and password as GitHub secrets and pass them to the
|
||||
`username` and `password` parameters.
|
||||
|
||||
```yaml
|
||||
with:
|
||||
url: https://vault.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: ldap
|
||||
username: ${{ secrets.VAULT_USERNAME }}
|
||||
password: ${{ secrets.VAULT_PASSWORD }}
|
||||
```
|
||||
|
||||
### Other Auth Methods
|
||||
|
||||
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
|
||||
|
||||
The `secrets` parameter is a set of multiple secret requests separated by the `;` character.
|
||||
|
||||
Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
|
||||
Each secret request consists of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
|
||||
Note that the selector is using [JSONata](https://docs.jsonata.org/overview.html) and certain characters in keys may need to be escaped.
|
||||
|
||||
```raw
|
||||
{{ Secret Path }} {{ Secret Key }} | {{ Output Environment Variable Name }}
|
||||
{{ Secret Path }} {{ Secret Key or Selector }} | {{ Env/Output Variable Name }}
|
||||
```
|
||||
|
||||
### 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 data key, and output:
|
||||
`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:
|
||||
|
||||
```bash
|
||||
NPMTOKEN=somelongtoken
|
||||
```
|
||||
|
||||
### Set Environment Variable Name
|
||||
You can also access the secret via outputs:
|
||||
|
||||
However, if you want to set it to a specific environmental variable, say `NPM_TOKEN`, you could do this instead:
|
||||
```yaml
|
||||
steps:
|
||||
# ...
|
||||
- name: Import Secrets
|
||||
id: secrets
|
||||
# Import config...
|
||||
- name: Sensitive Operation
|
||||
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
|
||||
```
|
||||
|
||||
_**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._
|
||||
|
||||
### Set Output Variable Name
|
||||
|
||||
However, if you want to set it to a specific name, say `NPM_TOKEN`, you could do this instead:
|
||||
|
||||
```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:
|
||||
@@ -85,6 +352,17 @@ With that, `vault-action` will now use your requested name and output:
|
||||
NPM_TOKEN=somelongtoken
|
||||
```
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
# ...
|
||||
- name: Import Secrets
|
||||
id: secrets
|
||||
# Import config...
|
||||
- name: Sensitive Operation
|
||||
run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'"
|
||||
|
||||
```
|
||||
|
||||
### Multiple Secrets
|
||||
|
||||
This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do:
|
||||
@@ -92,50 +370,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
|
||||
```
|
||||
|
||||
### Using K/V version 1
|
||||
## Other Secret Engines
|
||||
|
||||
By default, `vault-action` expects a K/V engine using [version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html).
|
||||
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`).
|
||||
|
||||
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.
|
||||
|
||||
To do so when specifying the `Secret Path`, just append a leading formard 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:
|
||||
@@ -147,46 +393,171 @@ with:
|
||||
Resulting in:
|
||||
|
||||
```bash
|
||||
FOO=bar MY_KEY=zap
|
||||
FOO=bar
|
||||
MY_KEY=zap
|
||||
```
|
||||
|
||||
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.
|
||||
```yaml
|
||||
steps:
|
||||
# ...
|
||||
- name: Import Secrets
|
||||
id: secrets
|
||||
# Import config...
|
||||
- name: Sensitive Operation
|
||||
run: "my-cli --token '${{ steps.secrets.outputs.foo }}'"
|
||||
- name: Another Sensitive Operation
|
||||
run: "my-cli --token '${{ steps.secrets.outputs.MY_KEY }}'"
|
||||
```
|
||||
|
||||
e.g.:
|
||||
## 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`:
|
||||
|
||||
```yaml
|
||||
with:
|
||||
secrets: |
|
||||
/aws/creds/ci access_key | AWS_ACCESS_KEY_ID ;
|
||||
/aws/creds/ci secret_key | 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 }}
|
||||
```
|
||||
|
||||
would work fine.
|
||||
This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault.
|
||||
|
||||
NOTE: The `Secret Key` is pulled from the `data` property of the response.
|
||||
## HashiCorp Cloud Platform or Vault Enterprise
|
||||
|
||||
## Vault Enterprise Features
|
||||
If you are using [HCP Vault](https://cloud.hashicorp.com/products/vault)
|
||||
or Vault Enterprise, you may need additional parameters in
|
||||
your GitHub Actions workflow.
|
||||
|
||||
### Namespace
|
||||
|
||||
If you need to retrieve secrets from a specific vault namespace, all that's required is an additional parameter specifying the namespace.
|
||||
If you need to retrieve secrets from a specific Vault namespace, set the `namespace`
|
||||
parameter specifying the namespace. In HCP Vault, the namespace defaults to `admin`.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
# ...
|
||||
- name: Import Secrets
|
||||
uses: RichiCoder1/vault-action
|
||||
uses: hashicorp/vault-action
|
||||
with:
|
||||
url: https://vault-enterprise.mycompany.com:8200
|
||||
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||
method: token
|
||||
token: ${{ secrets.VaultToken }}
|
||||
namespace: ns1
|
||||
token: ${{ secrets.VAULT_TOKEN }}
|
||||
namespace: admin
|
||||
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
|
||||
```
|
||||
|
||||
## Masking
|
||||
## Reference
|
||||
|
||||
This action uses Github Action's built in masking, so all variables will automatically be masked if printed to the console or to logs.
|
||||
Here are all the inputs available through `with`:
|
||||
|
||||
| Input | Description | Default | Required |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
|
||||
| `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 | | |
|
||||
| `method` | The method to use to authenticate with Vault. | `token` | |
|
||||
| `role` | Vault role for specified auth method | | |
|
||||
| `path` | Custom vault path, if the auth method was enabled at a different path | | |
|
||||
| `token` | The Vault Token to be used to authenticate with Vault | | |
|
||||
| `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 | | |
|
||||
| `jwtPrivateKey` | Base64 encoded Private key to sign JWT | | |
|
||||
| `jwtKeyPassword` | Password for key stored in jwtPrivateKey (if needed) | | |
|
||||
| `jwtGithubAudience` | Identifies the recipient ("aud" claim) that the JWT is intended for |`sigstore`| |
|
||||
| `jwtTtl` | Time in seconds, after which token expires | | 3600 |
|
||||
| `kubernetesTokenPath` | The path to the service-account secret with the jwt token for kubernetes based authentication |`/var/run/secrets/kubernetes.io/serviceaccount/token` | |
|
||||
| `username` | The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods | | |
|
||||
| `password` | The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods | | |
|
||||
| `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` | |
|
||||
| `exportToken` | Whether or not export Vault token as environment variables (i.e VAULT_TOKEN). | `false` | |
|
||||
| `outputToken` | Whether or not to set the `vault_token` output to contain the Vault token after authentication. | `false` | |
|
||||
| `caCertificate` | Base64 encoded CA certificate the server certificate was signed with. | | |
|
||||
| `clientCertificate` | Base64 encoded client certificate the action uses to authenticate with Vault when mTLS is enabled. | | |
|
||||
| `clientKey` | Base64 encoded client key the action uses to authenticate with Vault when mTLS is enabled. | | |
|
||||
| `tlsSkipVerify` | When set to true, disables verification of server certificates when testing the action. | `false` | |
|
||||
|
||||
## Masking - Hiding Secrets from Logs
|
||||
|
||||
This action uses GitHub Action's built-in masking, so all variables will automatically be masked (aka hidden) if printed to the console or to logs.
|
||||
**This only obscures secrets from output logs.** If someone has the ability to edit your workflows, then they are able to read and therefore write secrets to somewhere else just like normal GitHub Secrets.
|
||||
|
||||
## Normalization
|
||||
|
||||
To make it simpler to consume certain secrets as env vars, if no Env/Output Var Name is specified `vault-action` will replace and `.` chars with `__`, remove any other non-letter or number characters. If you're concerned about the result, it's recommended to provide an explicit Output Var Key.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you wish to contribute to this project, the following dependencies are recommended for local development:
|
||||
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install dependencies, build project and run tests
|
||||
- [docker](https://docs.docker.com/get-docker/) to run the pre-configured vault containers for acceptance tests
|
||||
- [docker-compose](https://docs.docker.com/compose/) to spin up the pre-configured vault containers for acceptance tests
|
||||
- [act](https://github.com/nektos/act) to run the vault-action locally
|
||||
|
||||
### Build
|
||||
|
||||
Use npm to install dependencies and build the project:
|
||||
|
||||
```sh
|
||||
$ npm install && npm run build
|
||||
```
|
||||
|
||||
### Vault test instance
|
||||
|
||||
The Github Action needs access to a working Vault instance to function.
|
||||
Multiple docker configurations are available via the docker-compose.yml file to run containers compatible with the various acceptance test suites.
|
||||
|
||||
```sh
|
||||
$ docker-compose up -d vault # Choose one of: vault, vault-enterprise, vault-tls depending on which tests you would like to run
|
||||
```
|
||||
|
||||
Instead of using one of the dockerized instance, you can also use your own local or remote Vault instance by exporting these environment variables:
|
||||
|
||||
```sh
|
||||
$ export VAULT_HOST=<YOUR VAULT CLUSTER LOCATION> # localhost if undefined
|
||||
$ export VAULT_PORT=<YOUR VAULT PORT> # 8200 if undefined
|
||||
$ export VAULT_TOKEN=<YOUR VAULT TOKEN> # testtoken if undefined
|
||||
```
|
||||
|
||||
### Running unit tests
|
||||
|
||||
Unit tests can be executed at any time with no dependencies or prior setup.
|
||||
|
||||
```sh
|
||||
$ npm test
|
||||
```
|
||||
|
||||
### Running acceptance tests
|
||||
|
||||
With a succesful build to take your local changes into account and a working Vault instance configured, you can now run acceptance tests to validate if any regressions were introduced.
|
||||
|
||||
```sh
|
||||
$ npm run test:integration:basic # Choose one of: basic, enterprise, e2e, e2e-tls
|
||||
```
|
||||
|
||||
### Running the action locally
|
||||
|
||||
You can use the [act](https://github.com/nektos/act) command to test your changes locally if desired. Unfortunately it is not currently possible to use uncommitted local changes for a shared workfow. You will still need to push
|
||||
the changes you would like to validate beforehand. Even if a commit is necessary, this is still a more detailed and faster feedback loop than waiting for the action to be executed by Github in a different repository.
|
||||
|
||||
Push your changes into a feature branch.
|
||||
```sh
|
||||
$ git checkout -b my-feature-branch
|
||||
$ git commit -m "testing new changes"
|
||||
$ git push
|
||||
```
|
||||
|
||||
Edit the ./.github/workflows/local-test.yaml file to use your new feature branch. You may have to additionally edit the vault url, token and secret path if you are not using one of the provided containerized instance.
|
||||
Run your feature branch locally.
|
||||
```sh
|
||||
$ act local-test
|
||||
```
|
||||
@@ -1,208 +0,0 @@
|
||||
const core = require('@actions/core');
|
||||
const command = require('@actions/core/lib/command');
|
||||
const got = require('got');
|
||||
|
||||
const AUTH_METHODS = ['approle', 'token'];
|
||||
const VALID_KV_VERSION = [-1, 1, 2];
|
||||
|
||||
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 secretRequests = parseSecretsInput(secretsInput);
|
||||
|
||||
const vaultMethod = core.getInput('method', { required: false }) || 'token';
|
||||
if (!AUTH_METHODS.includes(vaultMethod)) {
|
||||
throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
|
||||
}
|
||||
|
||||
let vaultToken = null;
|
||||
switch (vaultMethod) {
|
||||
case 'approle':
|
||||
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'
|
||||
};
|
||||
|
||||
if (vaultNamespace != null) {
|
||||
options.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||
}
|
||||
|
||||
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
|
||||
if (result && result.body && result.body.auth && result.body.auth.client_token) {
|
||||
vaultToken = result.body.auth.client_token;
|
||||
core.debug('✔ Vault Token has retrieved from approle');
|
||||
} else {
|
||||
throw Error(`No token was retrieved with the role_id and secret_id provided.`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
vaultToken = core.getInput('token', { required: true });
|
||||
break;
|
||||
}
|
||||
|
||||
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 responseCache = new Map();
|
||||
for (const secretRequest of secretRequests) {
|
||||
const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest;
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
};
|
||||
|
||||
if (vaultNamespace != null) {
|
||||
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||
}
|
||||
|
||||
let requestPath = `${vaultUrl}/v1`;
|
||||
const kvRequest = !secretPath.startsWith('/')
|
||||
if (!kvRequest) {
|
||||
requestPath += secretPath;
|
||||
} else {
|
||||
requestPath += (kvVersion === 2)
|
||||
? `/${enginePath}/data/${secretPath}`
|
||||
: `/${enginePath}/${secretPath}`;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (responseCache.has(requestPath)) {
|
||||
body = responseCache.get(requestPath);
|
||||
core.debug('ℹ using cached response');
|
||||
} else {
|
||||
const result = await got(requestPath, requestOptions);
|
||||
body = result.body;
|
||||
responseCache.set(requestPath, body);
|
||||
}
|
||||
|
||||
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
|
||||
|
||||
const secretData = getResponseData(body, dataDepth);
|
||||
const value = selectData(secretData, secretSelector);
|
||||
command.issue('add-mask', value);
|
||||
core.exportVariable(outputName, `${value}`);
|
||||
core.debug(`✔ ${secretPath} => ${outputName}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a secrets input string into key paths and their resulting environment variable name.
|
||||
* @param {string} secretsInput
|
||||
*/
|
||||
function parseSecretsInput(secretsInput) {
|
||||
const secrets = secretsInput
|
||||
.split(';')
|
||||
.filter(key => !!key)
|
||||
.map(key => key.trim())
|
||||
.filter(key => key.length !== 0);
|
||||
|
||||
/** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */
|
||||
const output = [];
|
||||
for (const secret of secrets) {
|
||||
let path = secret;
|
||||
let outputName = null;
|
||||
|
||||
const renameSigilIndex = secret.lastIndexOf('|');
|
||||
if (renameSigilIndex > -1) {
|
||||
path = secret.substring(0, renameSigilIndex).trim();
|
||||
outputName = secret.substring(renameSigilIndex + 1).trim();
|
||||
|
||||
if (outputName.length < 1) {
|
||||
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const pathParts = path
|
||||
.split(/\s+/)
|
||||
.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}"`)
|
||||
}
|
||||
|
||||
const [secretPath, secretSelector] = pathParts;
|
||||
|
||||
// If we're not using a mapped name, normalize the key path into a variable name.
|
||||
if (!outputName) {
|
||||
outputName = normalizeOutputKey(secretSelector);
|
||||
}
|
||||
|
||||
output.push({
|
||||
secretPath,
|
||||
outputName,
|
||||
secretSelector,
|
||||
isJSONPath: secretSelector.startsWith('$')
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON response and returns the secret data
|
||||
* @param {string} responseBody
|
||||
* @param {number} kvVersion
|
||||
*/
|
||||
function getResponseData(responseBody, dataLevel) {
|
||||
let secretData = JSON.parse(responseBody);
|
||||
|
||||
for (let i = 0; i < dataLevel; i++) {
|
||||
secretData = secretData['data'];
|
||||
}
|
||||
return secretData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON response and returns the secret data
|
||||
* @param {Object} data
|
||||
* @param {string} selector
|
||||
*/
|
||||
function selectData(data, selector, isJSONPath) {
|
||||
if (!isJSONPath) {
|
||||
return data[selector];
|
||||
}
|
||||
|
||||
// TODO: JSONPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any forward-slash characters to
|
||||
* @param {string} dataKey
|
||||
*/
|
||||
function normalizeOutputKey(dataKey) {
|
||||
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
||||
}
|
||||
|
||||
function parseBoolInput(input) {
|
||||
if (input === null || input === undefined || input.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
return Boolean(input);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exportSecrets,
|
||||
parseSecretsInput,
|
||||
parseResponse: getResponseData,
|
||||
normalizeOutputKey
|
||||
};
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
jest.mock('got');
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
|
||||
const core = require('@actions/core');
|
||||
const got = require('got');
|
||||
const {
|
||||
exportSecrets,
|
||||
parseSecretsInput,
|
||||
parseResponse
|
||||
} = require('./action');
|
||||
|
||||
const { when } = require('jest-when');
|
||||
|
||||
describe('parseSecretsInput', () => {
|
||||
it('parses simple secret', () => {
|
||||
const output = parseSecretsInput('test key');
|
||||
expect(output).toContainEqual({
|
||||
secretPath: 'test',
|
||||
secretSelector: 'key',
|
||||
outputName: 'KEY',
|
||||
isJSONPath: false
|
||||
});
|
||||
});
|
||||
|
||||
it('parses mapped secret', () => {
|
||||
const output = parseSecretsInput('test key|testName');
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toMatchObject({
|
||||
outputName: 'testName',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on invalid mapped name', () => {
|
||||
expect(() => parseSecretsInput('test key|'))
|
||||
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
|
||||
});
|
||||
|
||||
it('fails on invalid path for mapped', () => {
|
||||
expect(() => parseSecretsInput('|testName'))
|
||||
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
|
||||
});
|
||||
|
||||
it('parses multiple secrets', () => {
|
||||
const output = parseSecretsInput('first a;second b;');
|
||||
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[0]).toMatchObject({
|
||||
secretPath: 'first',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
secretPath: 'second',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses multiple complex secret input', () => {
|
||||
const output = parseSecretsInput('first a;second b|secondName');
|
||||
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[0]).toMatchObject({
|
||||
outputName: 'A',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
outputName: 'secondName',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses multiline input', () => {
|
||||
const output = parseSecretsInput(`
|
||||
first a;
|
||||
second b;
|
||||
third c | SOME_C;`);
|
||||
|
||||
expect(output).toHaveLength(3);
|
||||
expect(output[0]).toMatchObject({
|
||||
secretPath: 'first',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
outputName: 'B',
|
||||
});
|
||||
expect(output[2]).toMatchObject({
|
||||
outputName: 'SOME_C',
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
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')
|
||||
.mockReturnValueOnce('http://vault:8200');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token')
|
||||
.mockReturnValueOnce('EXAMPLE');
|
||||
});
|
||||
|
||||
function mockInput(key) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets')
|
||||
.mockReturnValueOnce(key);
|
||||
}
|
||||
|
||||
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 () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
});
|
||||
|
||||
it('mapped secret retrieval', async () => {
|
||||
mockInput('test key|TEST_NAME');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
|
||||
await 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');
|
||||
});
|
||||
});
|
||||
+78
-6
@@ -1,20 +1,92 @@
|
||||
name: 'Vault Secrets'
|
||||
description: 'A Github Action that allows you to consume the v2 K/V backend of HashiCorp Vault as secure environment variables'
|
||||
description: 'A Github Action that allows you to consume HashiCorp Vault™ secrets as secure environment variables'
|
||||
inputs:
|
||||
url:
|
||||
description: 'The URL for the vault endpoint'
|
||||
required: true
|
||||
token:
|
||||
description: 'The Vault Token to be used to authenticate with Vault'
|
||||
required: true
|
||||
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: true
|
||||
required: false
|
||||
namespace:
|
||||
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
|
||||
required: false
|
||||
method:
|
||||
description: 'The method to use to authenticate with Vault.'
|
||||
default: 'token'
|
||||
required: false
|
||||
role:
|
||||
description: 'Vault role for specified auth method'
|
||||
required: false
|
||||
path:
|
||||
description: 'Custom Vault path, if the auth method was mounted at a different path'
|
||||
required: false
|
||||
token:
|
||||
description: 'The Vault Token to be used to authenticate with Vault'
|
||||
required: false
|
||||
roleId:
|
||||
description: 'The Role Id for App Role authentication'
|
||||
required: false
|
||||
secretId:
|
||||
description: 'The Secret Id for App Role authentication'
|
||||
required: false
|
||||
githubToken:
|
||||
description: 'The Github Token to be used to authenticate with Vault'
|
||||
required: false
|
||||
kubernetesTokenPath:
|
||||
description: 'The path to the Kubernetes service account secret'
|
||||
required: false
|
||||
default: '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||
username:
|
||||
description: 'The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
|
||||
required: false
|
||||
password:
|
||||
description: 'The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
|
||||
required: false
|
||||
authPayload:
|
||||
description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
|
||||
required: false
|
||||
extraHeaders:
|
||||
description: 'A string of newline separated extra headers to include on every request.'
|
||||
required: false
|
||||
exportEnv:
|
||||
description: 'Whether or not export secrets as environment variables.'
|
||||
default: 'true'
|
||||
required: false
|
||||
exportToken:
|
||||
description: 'Whether or not export Vault token as environment variables.'
|
||||
default: 'false'
|
||||
required: false
|
||||
caCertificate:
|
||||
description: 'Base64 encoded CA certificate to verify the Vault server certificate.'
|
||||
required: false
|
||||
clientCertificate:
|
||||
description: 'Base64 encoded client certificate for mTLS communication with the Vault server.'
|
||||
required: false
|
||||
clientKey:
|
||||
description: 'Base64 encoded client key for mTLS communication with the Vault server.'
|
||||
required: false
|
||||
tlsSkipVerify:
|
||||
description: 'When set to true, disables verification of the Vault server certificate. Setting this to true in production is not recommended.'
|
||||
required: false
|
||||
default: 'false'
|
||||
jwtPrivateKey:
|
||||
description: 'Base64 encoded Private key to sign JWT'
|
||||
required: false
|
||||
jwtKeyPassword:
|
||||
description: 'Password for key stored in jwtPrivateKey (if needed)'
|
||||
required: false
|
||||
jwtGithubAudience:
|
||||
description: 'Identifies the recipient ("aud" claim) that the JWT is intended for'
|
||||
required: false
|
||||
jwtTtl:
|
||||
description: 'Time in seconds, after which token expires'
|
||||
required: false
|
||||
default: 3600
|
||||
secretEncodingType:
|
||||
description: 'The encoding type of the secret to decode. If not specified, the secret will not be decoded. Supported values: base64, hex, utf8'
|
||||
required: false
|
||||
runs:
|
||||
using: 'node12'
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: 'unlock'
|
||||
|
||||
Vendored
+18475
-5374
File diff suppressed because one or more lines are too long
+20
-4
@@ -2,16 +2,32 @@
|
||||
version: "3.0"
|
||||
services:
|
||||
vault:
|
||||
image: vault:1.3.0
|
||||
image: vault:latest
|
||||
environment:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: testtoken
|
||||
ports:
|
||||
- 8200:8200
|
||||
privileged: true
|
||||
vault-enterprise:
|
||||
image: hashicorp/vault-enterprise:1.3.0_ent
|
||||
image: hashicorp/vault-enterprise:latest
|
||||
environment:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: testtoken
|
||||
VAULT_LICENSE: ${VAULT_LICENSE_CI}
|
||||
ports:
|
||||
- 8201:8200
|
||||
privileged: true
|
||||
- 8200:8200
|
||||
privileged: true
|
||||
vault-tls:
|
||||
image: vault:latest
|
||||
hostname: vault-tls
|
||||
environment:
|
||||
VAULT_CAPATH: /etc/vault/ca.crt
|
||||
ports:
|
||||
- 8200:8200
|
||||
privileged: true
|
||||
volumes:
|
||||
- ${PWD}/integrationTests/e2e-tls/configs:/etc/vault
|
||||
- vault-data:/var/lib/vault:rw
|
||||
entrypoint: vault server -config=/etc/vault/config.hcl
|
||||
|
||||
volumes:
|
||||
vault-data:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
const core = require('@actions/core');
|
||||
|
||||
const got = require('got');
|
||||
const { when } = require('jest-when');
|
||||
|
||||
const { exportSecrets } = require('../../src/action');
|
||||
|
||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||
|
||||
describe('authenticate with approle', () => {
|
||||
let roleId;
|
||||
let secretId;
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret/data/approle-test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
secret: 'SUPERSECRET_WITH_APPROLE',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Enable approle
|
||||
try {
|
||||
await got(`${vaultUrl}/v1/sys/auth/approle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
type: 'approle'
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const {response} = error;
|
||||
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
|
||||
// Approle might already be enabled from previous test runs
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create policies
|
||||
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
"name":"test",
|
||||
"policy":"path \"auth/approle/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/approle/role/my-role/role-id\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\npath \"auth/approle/role/my-role/secret-id\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\n\npath \"secret/data/*\" {\n capabilities = [\"list\"]\n}\npath \"secret/metadata/*\" {\n capabilities = [\"list\"]\n}\n\npath \"secret/data/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\n"
|
||||
},
|
||||
});
|
||||
|
||||
// Create approle
|
||||
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
policies: 'test'
|
||||
},
|
||||
});
|
||||
|
||||
// Get role-id
|
||||
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
roleId = roldIdResponse.body.data.role_id;
|
||||
|
||||
// Get secret-id
|
||||
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
secretId = secretIdResponse.body.data.secret_id;
|
||||
} catch(err) {
|
||||
console.warn('Create approle', err.response.body);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('method', expect.anything())
|
||||
.mockReturnValueOnce('approle');
|
||||
when(core.getInput)
|
||||
.calledWith('roleId', expect.anything())
|
||||
.mockReturnValueOnce(roleId);
|
||||
when(core.getInput)
|
||||
.calledWith('secretId', expect.anything())
|
||||
.mockReturnValueOnce(secretId);
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
});
|
||||
|
||||
function mockInput(secrets) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(secrets);
|
||||
}
|
||||
|
||||
it('authenticate with approle', async() => {
|
||||
mockInput('secret/data/approle-test secret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE');
|
||||
})
|
||||
});
|
||||
@@ -5,23 +5,24 @@ const core = require('@actions/core');
|
||||
const got = require('got');
|
||||
const { when } = require('jest-when');
|
||||
|
||||
const { exportSecrets } = require('../../action');
|
||||
const { exportSecrets } = require('../../src/action');
|
||||
|
||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||
|
||||
describe('integration', () => {
|
||||
beforeAll(async () => {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret/data/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
@@ -33,21 +34,33 @@ describe('integration', () => {
|
||||
await got(`${vaultUrl}/v1/secret/data/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
otherSecret: 'OTHERSUPERSECRET',
|
||||
"other-Secret-dash": 'OTHERSUPERSECRET',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret/data/foobar`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
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',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
type: 'kv'
|
||||
@@ -62,23 +75,33 @@ describe('integration', () => {
|
||||
}
|
||||
}
|
||||
|
||||
await got(`${vaultUrl}/v1/my-secret/test`, {
|
||||
await got(`${vaultUrl}/v1/secret-kv1/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
secret: 'CUSTOMSECRET',
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/my-secret/nested/test`, {
|
||||
await got(`${vaultUrl}/v1/secret-kv1/foobar`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
otherSecret: 'OTHERCUSTOMSECRET',
|
||||
fookv1: 'bar',
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret-kv1/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
"other-Secret-dash": 'OTHERCUSTOMSECRET',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -87,34 +110,30 @@ describe('integration', () => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url')
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token')
|
||||
.mockReturnValueOnce('testtoken');
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce(vaultToken);
|
||||
});
|
||||
|
||||
function mockInput(secrets) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets')
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(secrets);
|
||||
}
|
||||
|
||||
function mockEngineName(name) {
|
||||
when(core.getInput)
|
||||
.calledWith('path')
|
||||
.mockReturnValueOnce(name);
|
||||
}
|
||||
it('prints a nice error message when secret not found', async () => {
|
||||
mockInput(`secret/data/test secret ;
|
||||
secret/data/test secret | NAMED_SECRET ;
|
||||
secret/data/notFound kehe | NO_SIR ;`);
|
||||
|
||||
function mockVersion(version) {
|
||||
when(core.getInput)
|
||||
.calledWith('kv-version')
|
||||
.mockReturnValueOnce(version);
|
||||
}
|
||||
expect(exportSecrets()).rejects.toEqual(Error(`Unable to retrieve result for "secret/data/notFound" because it was not found: {"errors":[]}`));
|
||||
})
|
||||
|
||||
it('get simple secret', async () => {
|
||||
mockInput('test secret');
|
||||
mockInput('secret/data/test secret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -122,7 +141,7 @@ describe('integration', () => {
|
||||
});
|
||||
|
||||
it('re-map secret', async () => {
|
||||
mockInput('test secret | TEST_KEY');
|
||||
mockInput('secret/data/test secret | TEST_KEY');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -130,18 +149,18 @@ describe('integration', () => {
|
||||
});
|
||||
|
||||
it('get nested secret', async () => {
|
||||
mockInput('nested/test otherSecret');
|
||||
mockInput(`secret/data/nested/test "other-Secret-dash"`);
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET');
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERSUPERSECRET');
|
||||
});
|
||||
|
||||
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 "other-Secret-dash" ;`);
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -149,13 +168,19 @@ describe('integration', () => {
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||
expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET');
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET');
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', '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,13 +188,19 @@ 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 "other-Secret-dash"');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERCUSTOMSECRET');
|
||||
expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERCUSTOMSECRET');
|
||||
});
|
||||
|
||||
it('leading slash kvv1', async () => {
|
||||
mockInput('/secret-kv1/foobar fookv1');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('FOOKV1', 'bar');
|
||||
});
|
||||
|
||||
describe('generic engines', () => {
|
||||
@@ -177,7 +208,7 @@ describe('integration', () => {
|
||||
await got(`${vaultUrl}/v1/cubbyhole/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
foo: "bar",
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
const core = require('@actions/core');
|
||||
const rsasign = require('jsrsasign');
|
||||
const {
|
||||
privateRsaKey,
|
||||
privateRsaKeyBase64,
|
||||
publicRsaKey
|
||||
} = require('./rsa_keys');
|
||||
|
||||
const got = require('got');
|
||||
const { when } = require('jest-when');
|
||||
|
||||
const { exportSecrets } = require('../../src/action');
|
||||
|
||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||
|
||||
/**
|
||||
* Returns Github OIDC response mock
|
||||
* @param {string} aud Audience claim
|
||||
* @returns {string}
|
||||
*/
|
||||
function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action") {
|
||||
const alg = 'RS256';
|
||||
const header = { alg: alg, typ: 'JWT' };
|
||||
const now = rsasign.KJUR.jws.IntDate.getNow();
|
||||
const payload = {
|
||||
jti: "unique-id",
|
||||
sub: "repo:hashicorp/vault-action:ref:refs/heads/main",
|
||||
aud,
|
||||
ref: "refs/heads/main",
|
||||
sha: "commit-sha",
|
||||
repository: "hashicorp/vault-action",
|
||||
repository_owner: "hashicorp",
|
||||
run_id: "1",
|
||||
run_number: "1",
|
||||
run_attempt: "1",
|
||||
actor: "github-username",
|
||||
workflow: "Workflow Name",
|
||||
head_ref: "",
|
||||
base_ref: "",
|
||||
event_name: "push",
|
||||
ref_type: "branch",
|
||||
job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/main",
|
||||
iss: 'vault-action',
|
||||
iat: now,
|
||||
nbf: now,
|
||||
exp: now + 3600,
|
||||
};
|
||||
const decryptedKey = rsasign.KEYUTIL.getKey(privateRsaKey);
|
||||
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
|
||||
}
|
||||
|
||||
// The sign call inside this function takes a while to run, so cache the default JWT in a constant.
|
||||
const defaultGithubJwt = mockGithubOIDCResponse();
|
||||
|
||||
describe('jwt auth', () => {
|
||||
beforeAll(async () => {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await got(`${vaultUrl}/v1/sys/auth/jwt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
type: 'jwt'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const {response} = error;
|
||||
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
|
||||
// Auth method might already be enabled from previous test runs
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await got(`${vaultUrl}/v1/sys/policy/reader`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
policy: `
|
||||
path "*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/auth/jwt/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
jwt_validation_pubkeys: publicRsaKey,
|
||||
default_role: "default"
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/auth/jwt/role/default`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
role_type: 'jwt',
|
||||
bound_audiences: null,
|
||||
bound_claims: {
|
||||
iss: 'vault-action'
|
||||
},
|
||||
user_claim: 'iss',
|
||||
policies: ['reader']
|
||||
}
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret/data/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
secret: 'SUPERSECRET',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate with private key', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('method', expect.anything())
|
||||
.mockReturnValueOnce('jwt');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('jwtPrivateKey', expect.anything())
|
||||
.mockReturnValueOnce(privateRsaKeyBase64);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('role', expect.anything())
|
||||
.mockReturnValueOnce('default');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce('secret/data/test secret');
|
||||
});
|
||||
|
||||
it('successfully authenticates', async () => {
|
||||
await exportSecrets();
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate with Github OIDC', () => {
|
||||
beforeAll(async () => {
|
||||
await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
role_type: 'jwt',
|
||||
bound_audiences: null,
|
||||
bound_claims: {
|
||||
iss: 'vault-action',
|
||||
aud: 'sigstore',
|
||||
},
|
||||
user_claim: 'iss',
|
||||
policies: ['reader']
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('method', expect.anything())
|
||||
.mockReturnValueOnce('jwt');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('jwtPrivateKey', expect.anything())
|
||||
.mockReturnValueOnce('');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce('secret/data/test secret');
|
||||
});
|
||||
|
||||
it('successfully authenticates', async () => {
|
||||
when(core.getInput)
|
||||
.calledWith('role', expect.anything())
|
||||
.mockReturnValueOnce('default');
|
||||
|
||||
when(core.getIDToken)
|
||||
.calledWith(undefined)
|
||||
.mockReturnValueOnce(defaultGithubJwt);
|
||||
|
||||
await exportSecrets();
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||
});
|
||||
|
||||
it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => {
|
||||
when(core.getInput)
|
||||
.calledWith('role', expect.anything())
|
||||
.mockReturnValueOnce('default-sigstore');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('jwtGithubAudience', expect.anything())
|
||||
.mockReturnValueOnce('sigstore');
|
||||
|
||||
when(core.getIDToken)
|
||||
.calledWith(expect.anything())
|
||||
.mockReturnValueOnce(mockGithubOIDCResponse('sigstore'));
|
||||
|
||||
await exportSecrets();
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||
})
|
||||
|
||||
it('successfully authenticates as default role without specifying it', async () => {
|
||||
when(core.getInput)
|
||||
.calledWith('role', expect.anything())
|
||||
.mockReturnValueOnce(null);
|
||||
|
||||
when(core.getIDToken)
|
||||
.calledWith(undefined)
|
||||
.mockReturnValueOnce(defaultGithubJwt);
|
||||
|
||||
await exportSecrets();
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
const privateRsaKey = `
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEArcch89X6VuWj/CQtVfaCXUl0Pcv8IJRgICN8X+3zFNrbiTdh
|
||||
kTtrOhdkbEU5VaW6aQiCXX5+4C1T2sXXXT682XJhIjKepyX3aY50Fh59pLCciwAK
|
||||
c5wPy3PVMOhup15u9reiQKxps1SNrqVLyZNjha83qbN9IJvQcMnQghAjjPUeGPMa
|
||||
MMzG1GOnuPOWIiM2kxqRpbugwwTyuepPnakmfkWqVtMIRprPLY6d3liDIUSSRZ7o
|
||||
6vbmgeF+9U4DyaimKVNngrmi+mW0OnyH1eJYLrJEY9tZaRF8xraMZiOcBcyAt6S/
|
||||
TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY8QIDAQABAoIBAQCncXT5qnipOmSk
|
||||
E4fLiNdcY+aplN+/1Lg6v3acSH8s3SUkNkTA1+wd8WRGHv171VCk3BohVD2UbJib
|
||||
+H3nzwfQzjFh7jyI+kBHaYfZuE+AXNy54rQvaXSeqWIG2i+k/Y0WFSM2BetjbFmI
|
||||
qqU3+dive4G69sPeo8RYqV1LtZlLu11j1K1sptcmMi75/cFAB6/uURapNLI978sr
|
||||
cIaOV2BbLs4Yk7ji4YAtpvL+mky9KF56QAsLspBKgsU/Oxy5FkgFORPlaVn9qBFv
|
||||
cdXBsZOWFnZ2+F/OA55WJeQoyO8E9l5+N8TGKpzXbmkUWyKiqbN+AviZYrK6KPxi
|
||||
zXS4SH0xAoGBANUDIhoUTM+u6dJze/i/sSe90k6UOBsSxA8Q7rOfksda4F0JEBPl
|
||||
l4kbfmMVMEeIJrHNaFRE8r/p/J8sQCg4w/wDB5LEdHGaxf3b2Fbai+QHuElrbOXP
|
||||
vQ/UOaaMQKFJlwlfOHpbDzXM3bMdGwfT6DCCoQyrAvVRE/x8veXJ9Fl/AoGBANDZ
|
||||
B+sjiVbIjbWA9debx/QeEjoLB68Pi8DleCOgbtF7c4jJPPDRLz0fce16ePVFpiw4
|
||||
Mu1E8QQdMTxWY+Y4ERNPwXj//PhD5xDfWYdRJ6IgKFK1bqKIwm7BmJn3WLD2JH2J
|
||||
mLR0Wfa7M8OmWBbAS8fe5NvubeqERmbMs+f+eeWPAoGAR4Cgvt5XllNhm8o2MB6w
|
||||
qeV3JfdtCfF3rJMDfXowPAkOTUyQgA1Om7CF8V6YcTqLup13yunGDpPNv+SLuLSt
|
||||
XPfrX+HgMI5Crd9RNH5x/N52hvavfEkKbrjPjU+BFmLsdzHmdHQCnA2j0c8QVsVU
|
||||
KIyA4Q66lHxd2CBLYkozYqMCgYAxiZkoPBiifhWm3LzzdF78V3mpTN54tq5Ghed2
|
||||
Q+KlS6v+4QTUdjnHPMMwOiGgw/GDgZ0KzJSCjk4UasVtYRUjyIIyqj2dwbV4OhIp
|
||||
V6WX/hqya5ifcuLzlHYW5yWha/EB2fZfr017ibHgkX9Jfjk7YnJUfHyT6OYuEhYG
|
||||
TEUrnwKBgHztVFIi0vwELwhVCr37pFzKTTmm7G+SYH2hnvL2o3eCNAxSoE8/+vuP
|
||||
qHxd6MME9OqeuY9s3eimsTuhSxnMN348v3Tr/FnA/VIeEMyDZyPqso1pFylpUnHn
|
||||
67hv/xqXT3+/MHq6AgVWXjwTgn8XNRDfXmHrBIztq6Kzo/kLmthY
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`;
|
||||
|
||||
const privateRsaKeyBase64 = Buffer.from(privateRsaKey).toString('base64');
|
||||
|
||||
const publicRsaKey = `
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArcch89X6VuWj/CQtVfaC
|
||||
XUl0Pcv8IJRgICN8X+3zFNrbiTdhkTtrOhdkbEU5VaW6aQiCXX5+4C1T2sXXXT68
|
||||
2XJhIjKepyX3aY50Fh59pLCciwAKc5wPy3PVMOhup15u9reiQKxps1SNrqVLyZNj
|
||||
ha83qbN9IJvQcMnQghAjjPUeGPMaMMzG1GOnuPOWIiM2kxqRpbugwwTyuepPnakm
|
||||
fkWqVtMIRprPLY6d3liDIUSSRZ7o6vbmgeF+9U4DyaimKVNngrmi+mW0OnyH1eJY
|
||||
LrJEY9tZaRF8xraMZiOcBcyAt6S/TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY
|
||||
8QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
privateRsaKey,
|
||||
privateRsaKeyBase64,
|
||||
publicRsaKey
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
const core = require('@actions/core');
|
||||
|
||||
const got = require('got');
|
||||
const { when } = require('jest-when');
|
||||
|
||||
const { exportSecrets } = require('../../src/action');
|
||||
|
||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||
|
||||
describe('authenticate with userpass', () => {
|
||||
const username = `testUsername`;
|
||||
const password = `testPassword`;
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
await got(`${vaultUrl}/v1/secret/data/userpass-test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
secret: 'SUPERSECRET_WITH_USERPASS',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Enable userpass
|
||||
try {
|
||||
await got(`${vaultUrl}/v1/sys/auth/userpass`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
type: 'userpass'
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const {response} = error;
|
||||
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
|
||||
// Userpass might already be enabled from previous test runs
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create policies
|
||||
await got(`${vaultUrl}/v1/sys/policies/acl/userpass-test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
"name":"userpass-test",
|
||||
"policy":`path \"auth/userpass/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/userpass/users/${username}\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\n\npath \"secret/data/*\" {\n capabilities = [\"list\"]\n}\npath \"secret/metadata/*\" {\n capabilities = [\"list\"]\n}\n\npath \"secret/data/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\n`
|
||||
},
|
||||
});
|
||||
|
||||
// Create user
|
||||
await got(`${vaultUrl}/v1/auth/userpass/users/${username}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken
|
||||
},
|
||||
json: {
|
||||
password: `${password}`,
|
||||
policies: 'userpass-test'
|
||||
},
|
||||
});
|
||||
} catch(err) {
|
||||
console.warn('Create user in userpass', err.response.body);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('method', expect.anything())
|
||||
.mockReturnValueOnce('userpass');
|
||||
when(core.getInput)
|
||||
.calledWith('username', expect.anything())
|
||||
.mockReturnValueOnce(username);
|
||||
when(core.getInput)
|
||||
.calledWith('password', expect.anything())
|
||||
.mockReturnValueOnce(password);
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
});
|
||||
|
||||
function mockInput(secrets) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(secrets);
|
||||
}
|
||||
|
||||
it('authenticate with userpass', async() => {
|
||||
mockInput('secret/data/userpass-test secret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_USERPASS');
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
# e2e tests
|
||||
|
||||
This test suite runs `vault-action` as a GitHub Action in the context of a live build, and then verifies that the appropriate environmental variables are set.
|
||||
These tests are intended to mostly be very simple smoke tests to verify that the action is being compiled and run correctly in context.
|
||||
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEFjCCAv6gAwIBAgIUe0i7/HGZKvbDb30L9mC99KXFwj8wDQYJKoZIhvcNAQEL
|
||||
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
|
||||
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
|
||||
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
|
||||
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjAwODA1MTg1MjAwWhcNMjUwODA0
|
||||
MTg1MjAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
|
||||
BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE
|
||||
CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw
|
||||
ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAMrRXuu2+zhBs0pLYEdXIaPc4KoWO3xm2RJdbzy3hfjFybQ8
|
||||
H/Y6Hi7txjGGSb45xSfXT/RF2srNfs235I+sfB8rrEizNpzkXqOgGa8LKvh2tgBT
|
||||
BK/jDWsEdDhxmkpFhE69wEW+D5ub7QGnx9jrqLKfwCmUA0utlzcFBk2nRNhRtsrp
|
||||
CI5YL1VN4coLpgXdvbodzbynPzGHe9R/o9K0Uiz2hgHooyKwhkVYwo0BIAQamLFz
|
||||
TS7lyeLf0thDOxV31NX8SpSucqRf50WHNk8T/YtKZ9EhlBDT4ybZwwvcC/ocxxcg
|
||||
1LvB0YweZNjSeO78S4CMh1TFGXnF/xOtGABlIbcCAwEAAaNCMEAwDgYDVR0PAQH/
|
||||
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFABD/NxvYLpo5zVNeD01
|
||||
r8IIFYlBMA0GCSqGSIb3DQEBCwUAA4IBAQB7TfpIx53gf/oI3mgR6Ciz287WBzFR
|
||||
OzhJXwHk5J3mx8VC1W8tDRXih2lCLd/f9qDy6LyL/hZcoonev6w9oReuOMBiH6l4
|
||||
Pf3yq2aDXX0AoGgm75c1m34kY669JLMsHq5+xuUDeeFUMd60w9zVtZfBSumy/sgN
|
||||
PdjtvThh8sSByocYULs3tuxZDGyQ6GyQcn/xlMrGtmcD5IuX5IXqcKRVlZttykNx
|
||||
S2ltcR00fekw8WZyPSzMJaP+/Kcq3T2viN02MS6qEycQZoYfEAMdj+A0kjbsZG9D
|
||||
6J92z78b2DuLAUvZVpynNk/UbpDeqIDy40V3JDmtvrfGUMkMhMqgK/+J
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIENTCCAx2gAwIBAgIUMu5h1ysA5DlM6lzZFliT2C2n4lEwDQYJKoZIhvcNAQEL
|
||||
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
|
||||
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
|
||||
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
|
||||
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwIBcNMjAwODA1MTg1MjAwWhgPMjEyMDA3
|
||||
MTIxODUyMDBaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
|
||||
MBQGA1UEBxMNU2FuIEZyYW5jaXNjbzESMBAGA1UEChMJSGFzaGlDb3JwMSMwIQYD
|
||||
VQQLExpUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAxMOaGFzaGlj
|
||||
b3JwLnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/zYDKdDGo
|
||||
+Qy7eJUNjOe6jpeGvK1lMu5T1Xka+h2ay6WH5gLyrPw5pi582iYpJdHVbplKMywx
|
||||
LxZv7mAbKNxqdp8UZKy0A3bCuHQqRF8ssXXHufQ8EGxNkLMLJP0e2q39OnrxXekS
|
||||
8Ct3aJm3V8qkcV3CpVdPNgJh4TSuneCXIxVWjFYSiyHi0/5TRd2D+aQPz12szg5F
|
||||
mBW4dLzYKHEMlWcjWG8mxtbLyt+jSR1+tSehQx7KndufdfniOWEDBdbeR3yDnZdn
|
||||
p8DnRWK4oaEI3Sl8tKlDd1Yp+R96aqOEn1tPW6Jy6Vdvk3fCefclbWZ6B9kiJ/1r
|
||||
gxq7AN7iKmHNAgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRn2mwyk+MPChnLg0iWy1r9
|
||||
b3JvwTAfBgNVHSMEGDAWgBQAQ/zcb2C6aOc1TXg9Na/CCBWJQTANBgkqhkiG9w0B
|
||||
AQsFAAOCAQEAOpCy0vHp1Kxgv0VBRrbrwSQLBGP8a1ubVWoeoZQ+EvX9ozqDrHxm
|
||||
gM4XPYUJlUOOEu0ZRgCW60YK33E1zNKnA1F0/3/rmqMkKnm0BBs/5WzMWtsIBPcU
|
||||
e0CeJmaRIXnERQMH/svD+RrFo1dcF8rUDIlWez7+xGqoIGBg7v4jEmkZ3HdckcE+
|
||||
/xvC61YSG8NsJwR/CEcQ8YCyVfgvuS0ukWs4dN15aVDL3Oe61h3bRcGAywOJBrdq
|
||||
9xaq7ezZp/+lUSkYnatWJBuC/aviH9g9s+gMT0I3fWHh8BB0Ne2txwJ15K/qz5he
|
||||
TjxFsumrh50aFqjSiEHndtY5UWuGAFLiSw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAv82AynQxqPkMu3iVDYznuo6XhrytZTLuU9V5Gvodmsulh+YC
|
||||
8qz8OaYufNomKSXR1W6ZSjMsMS8Wb+5gGyjcanafFGSstAN2wrh0KkRfLLF1x7n0
|
||||
PBBsTZCzCyT9Htqt/Tp68V3pEvArd2iZt1fKpHFdwqVXTzYCYeE0rp3glyMVVoxW
|
||||
Eosh4tP+U0Xdg/mkD89drM4ORZgVuHS82ChxDJVnI1hvJsbWy8rfo0kdfrUnoUMe
|
||||
yp3bn3X54jlhAwXW3kd8g52XZ6fA50ViuKGhCN0pfLSpQ3dWKfkfemqjhJ9bT1ui
|
||||
culXb5N3wnn3JW1megfZIif9a4MauwDe4iphzQIDAQABAoIBAQCYHJuidAoaTwGZ
|
||||
ACV9rJzuqD1lvubpFj5KwEcebPPjmtQ5deIqoaQa+D9wBvYyteq3ENKDNRg8HXL2
|
||||
7B7OC1bbHB5HZxxMW17pSK3gA39Ti52z+zbGF+Q8k6BbG0efG6DW7nUoTOkWeuCN
|
||||
/6fA7uAoEDxirQwUJuo2xAsq3MyMLwcs95rke9Bly8ABFNaV1oMZq8YT/w8oSc2b
|
||||
/7WtxXmChHlVYXTcMqzVPqNFqPRixZRWQ+BSHoXmEDviuGd51L4s9D7iXp32TvUx
|
||||
DMHeS1DFA2en7ZF1uc9VXZeplkkDtVhUe4d6qOqCcUwDFEvMonnyVSa6/FkR5jYZ
|
||||
2yujTdfhAoGBAM0hGOnmnDnCjADUt4mZlr+Mf0XmdKzEV+hid4CQUvBoTXgjYMvv
|
||||
c397eNePce7SwSUE1/APERInGUPhRLVFW5q6/34WRtGBbQkT8ByeJANXes4UFZe3
|
||||
wdNLczWUlSl0G3jTf+Kh3+K5/PtmyxSrAS/9GIk+ibs1mlJOPyVnWqUJAoGBAO9e
|
||||
WlP9/ruXluvkQyM5ZlnAnZYMsFGzzPx4tkazUjurtqxQoyZ0z+pPItGQ7lOl+pDA
|
||||
EWiTun66g+Da9uBiBCJUeXiC1ge2p6bT6N194BrYyrWML9hcIL4mqVojUEUmhnSh
|
||||
6b9h1pC7vFmw5ZFMIIkS60cfBMgQMZxMJN8NuaulAoGBAJM5hwURg90c2ZkbEyPK
|
||||
PVz7fLlxnxoEzcc3LOf0LeLoKXnpgma8VJwRxXiJNs+fKgrkwAtG9QyfTU3f1412
|
||||
2zlhr1ASsv9ZMiXKzpHrmpNfbP+NgLXkqFN7mpPBMZGQCMuemPHTFrpGnODfNTB/
|
||||
T5newIZ4gSgBX+Jk0IOK+47pAoGAeKo6pK6ck9pV5TIbOg18b/AuQG7DD1yxD/CW
|
||||
CkvpP1VPb8vygrdN/FLKPZRu39IC3qdD31DhKXNCeb5Hx1MBvICS/1INLLRCDVIz
|
||||
yDvlFgOFJEG3+LxwcQqyQlMc6s8B5pecarKaZDmPODN5dmZG3HKiEicr1OJ878pe
|
||||
p+aWW1UCgYBmGFbCc1qqlqp+srYGsv3rIgNs5HSfrAjbgY8xh9foMgrYCRAm57gv
|
||||
01yVxMXWmKA6ReVEu8OTVy9fkuOL/vw2o+C6W4IPZYdvSQoPwd5Lf+AqxEQvFF1m
|
||||
tT3SZAM3EhQ7tIXdIQHY27SJ1KlUJMrvUq1CiRWiG/MOKf/87JXPog==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,15 @@
|
||||
ui = false
|
||||
disable_mlock = true
|
||||
|
||||
listener "tcp" {
|
||||
address = "[::]:8200"
|
||||
cluster_address = "[::]:8201"
|
||||
tls_cert_file = "/etc/vault/server.crt"
|
||||
tls_key_file = "/etc/vault/server.key"
|
||||
tls_client_ca_file = "/etc/vault/ca.crt"
|
||||
tls_require_and_verify_client_cert = "true"
|
||||
}
|
||||
|
||||
storage "file" {
|
||||
path = "/var/lib/vault"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEXjCCA0agAwIBAgIUAswquazrfsyDRvXZwn5718DUhU4wDQYJKoZIhvcNAQEL
|
||||
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
|
||||
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
|
||||
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
|
||||
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwIBcNMjAwODA1MTg1MjAwWhgPMjEyMDA3
|
||||
MTIxODUyMDBaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
|
||||
MBQGA1UEBxMNU2FuIEZyYW5jaXNjbzESMBAGA1UEChMJSGFzaGlDb3JwMSMwIQYD
|
||||
VQQLExpUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAxMOaGFzaGlj
|
||||
b3JwLnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0azdZsrPC
|
||||
5Rv8nRxVJnLi+oZgCJYgzhCDiEbYqt1QK1gqNXp0ml5ck6ycj0drwzHzrrX+xcPV
|
||||
5FcNKH3RFyon9XkzjwaXkMv6IkgvH6/jQ1dDW9kWBf3Io3Y59wnD/YaIzNK0CYJS
|
||||
fRNdsZb4InH8gh+RL33+FeysgJwXG1TVA4tTUj7DQxDE0cDd9UD+C9Yx7OWiUjC1
|
||||
IjqdFPusX1nziKYjeI5/UiCmOUGqJJRoMPonuzuGIj9GdmBKmga64OfeZFqn4f6a
|
||||
ay61VnGCwZ24VniUwYElsFbcF2Nv9WqnrOeQlHOsYN68VMqHzaYPqE6SPa6mO5mI
|
||||
/tmpXrDG3Y+RAgMBAAGjgZ0wgZowDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE03/UTs2ZmJpWHSmstt
|
||||
hDngW6F3MB8GA1UdIwQYMBaAFABD/NxvYLpo5zVNeD01r8IIFYlBMCUGA1UdEQQe
|
||||
MByCCWxvY2FsaG9zdIIJdmF1bHQtdGxzhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQCzarBGJium5oZDSSP5GqxpS13QP2onEen6I1k2eRdcOqtbfNdQ20RJrb4dfNkE
|
||||
Dc09KWVlZAn+hYge2KKTXJ+4ltIC9V1LvquyWipNczOT1ve0H9gt3Wm88LdESqI5
|
||||
HOx43pIaa3cWXBlbzrFmT1SASYm1V5Oo1mXzpUukGokHLLmAz36VVuJGbD0BxYke
|
||||
5MefG4tNT1SsMsIqVvGxI9NiVs7YTdJu81MctSYK5snsEKnYdi9N7CHOk3bdDpeC
|
||||
v2Vo7XBk3s4sBMGmnJO+1JOcRFJioooEFkqNyQmg3atfInysVbreKS5KtWNTaCPm
|
||||
yI55plW8ga5ucja2VX3WbwAO
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAtGs3WbKzwuUb/J0cVSZy4vqGYAiWIM4Qg4hG2KrdUCtYKjV6
|
||||
dJpeXJOsnI9Ha8Mx8661/sXD1eRXDSh90RcqJ/V5M48Gl5DL+iJILx+v40NXQ1vZ
|
||||
FgX9yKN2OfcJw/2GiMzStAmCUn0TXbGW+CJx/IIfkS99/hXsrICcFxtU1QOLU1I+
|
||||
w0MQxNHA3fVA/gvWMezlolIwtSI6nRT7rF9Z84imI3iOf1IgpjlBqiSUaDD6J7s7
|
||||
hiI/RnZgSpoGuuDn3mRap+H+mmsutVZxgsGduFZ4lMGBJbBW3Bdjb/Vqp6znkJRz
|
||||
rGDevFTKh82mD6hOkj2upjuZiP7ZqV6wxt2PkQIDAQABAoIBAQCvK0HsVvLtkSCh
|
||||
HbF6gwAcnHyHFQ8d/rRN4KxYhVynD85j/NRODer8G20F/J6tZDFFlSWinUTMkQxr
|
||||
/BpcPg9yCIWKp50Q30cMLujCyBMvphw9jBmzplGG0h5hnRbgMXDDtYoFvw3HJST+
|
||||
XQRlGpxtO7GGdwPvBD5sJdpnHOQ6g7qIYKmlHM99kHU8vr0VghqZAYxEh8RpnYez
|
||||
NLra+7ep+Zp1pFIniU6B8ohyL3OArbQ65qYrZYriAEI5HeEk0RhjewcPsV56LwbS
|
||||
CncTVS/dNYgk1zRIvytmbDVD3v/4lLvnpIWeKVdk7p1aGJeCdpLeWNvDLX0Ws67r
|
||||
QeZQizwhAoGBAMwAIA6+HPsx+8dhNbN3ydX8YU6uUfSeshhshIFZPIYL2vrKyAHU
|
||||
/GAYVzYJH/cU0IvlLJlLdQuiZkOXEX87tgdfmM/o4Qdl12RR0BvU0Cae0txtzNrP
|
||||
yTdfZqDhTz/V8jOAXUNA5oQA45Y3rI7JES8hBd1F9WFOH2WINp344GzlAoGBAOJo
|
||||
SgmRE24VcnfUMqrBpwZBdBrTxDQyTagvd+MuGomIQfcE2Y4rr1eIuJJ0HF5/eYxc
|
||||
DZRO/LVP9tQ8ozXi2tdmgUdKC79O2edmdOCWW5of464R/TLcM5B4SmS36RtdE3qJ
|
||||
ig4fcUmsJ15MAGpkXLMh5YSD/N3TmcnURtx06Fk9AoGATi+mGcBnnybzFuF9EYHR
|
||||
y7/lE6DgLF8+ZvoAdwralY2pqgFaUslsyO/LTRyGMc66d0OoqkAvZfwiMbmOrTMX
|
||||
ew/6o4Tf6lPwD7UDjAcul/67VlyG7T5CIoTf8r0oAJFhOLf0BrizINiuYX6JFlid
|
||||
y3BerQYJG/gzNFjWhglDCrkCgYA+3wUISRAjNrN10ShMwL/3/b8XIA1RDVMBTEU3
|
||||
gfr+jCb9SIx9bWYgoafXi4TBPRbswjdHIvQMCWuankgYU6m/vQhTWp2Of4AFQS9d
|
||||
moNPdmGMWhR8xidPjAfklimWXq9lDMKYj2SvN64rAmHvKXWQjO4mcVyL4RHIuTkA
|
||||
STqoZQKBgQDKHd8F6tjZHEFolmjS5l682g7zVTpBhozezJ/RqYvhJh5ew1pXoD/O
|
||||
Zu9iMfHoDjR4ZUXq6aeLUj/oIt1AsjwaGChOLLAvFbvePgS9XkYkwIlaxS4efAya
|
||||
+CQE/JmY/a1/c2MDLNMCEXvUqX68pv6iDF8pfn+i4tn0omYqgfUlCA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,14 @@
|
||||
describe('e2e-tls', () => {
|
||||
it('verify', () => {
|
||||
expect(process.env.SECRET).toBe("SUPERSECRET");
|
||||
expect(process.env.NAMED_SECRET).toBe("SUPERSECRET");
|
||||
expect(process.env.OTHERSECRET).toBe("OTHERSUPERSECRET");
|
||||
expect(process.env.OTHER_SECRET_OUTPUT).toBe("OTHERSUPERSECRET");
|
||||
expect(process.env.ALTSECRET).toBe("CUSTOMSECRET");
|
||||
expect(process.env.NAMED_ALTSECRET).toBe("CUSTOMSECRET");
|
||||
expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET");
|
||||
expect(process.env.FOO).toBe("bar");
|
||||
expect(process.env.NAMED_CUBBYSECRET).toBe("zap");
|
||||
expect(process.env.SKIP).toBe("true");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
verbose: true
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
const got = require('got');
|
||||
const core = require('@actions/core');
|
||||
|
||||
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
const caCertificateRaw = `${process.env.VAULTCA}`;
|
||||
const clientCertificateRaw = `${process.env.VAULT_CLIENT_CERT}`;
|
||||
const clientKeyRaw = `${process.env.VAULT_CLIENT_KEY}`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
var caCertificate = Buffer.from(caCertificateRaw, 'base64').toString();
|
||||
if (caCertificate == null) {
|
||||
throw Error("VAULTCA env not set.")
|
||||
}
|
||||
|
||||
var clientCertificate = Buffer.from(clientCertificateRaw, 'base64').toString();
|
||||
if (clientCertificate == null) {
|
||||
throw Error("VAULT_CLIENT_CERT env not set.")
|
||||
}
|
||||
|
||||
var clientKey = Buffer.from(clientKeyRaw, 'base64').toString();
|
||||
if (clientKey == null) {
|
||||
throw Error("VAULT_CLIENT_KEY env not set.")
|
||||
}
|
||||
|
||||
// Init
|
||||
const {body} = await got(`https://${vaultUrl}/v1/sys/init`, {
|
||||
method: 'POST',
|
||||
json: {
|
||||
secret_shares: 1,
|
||||
secret_threshold: 1,
|
||||
},
|
||||
responseType: 'json',
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
}
|
||||
});
|
||||
|
||||
if (body.keys_base64.length != 1) {
|
||||
throw Error("No unseal key found after init.")
|
||||
}
|
||||
var unseal = body.keys_base64[0];
|
||||
|
||||
if (body.root_token == "") {
|
||||
throw Error("No root token found after init.")
|
||||
}
|
||||
var rootToken = body.root_token;
|
||||
|
||||
core.exportVariable('VAULT_TOKEN', rootToken);
|
||||
core.setSecret(rootToken)
|
||||
|
||||
// Unseal
|
||||
await got(`https://${vaultUrl}/v1/sys/unseal`, {
|
||||
method: 'POST',
|
||||
json: {
|
||||
key: unseal,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/sys/mounts/secret`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
type: 'kv-v2'
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/secret/data/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
secret: 'SUPERSECRET',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/secret/data/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
otherSecret: 'OTHERSUPERSECRET',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/secret/data/tlsSkipVerify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
skip: 'true',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/sys/mounts/my-secret`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
type: 'kv'
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/my-secret/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
altSecret: 'CUSTOMSECRET',
|
||||
}
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/my-secret/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
otherAltSecret: 'OTHERCUSTOMSECRET',
|
||||
},
|
||||
});
|
||||
|
||||
await got(`https://${vaultUrl}/v1/cubbyhole/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': rootToken,
|
||||
},
|
||||
https: {
|
||||
certificateAuthority: caCertificate,
|
||||
certificate: clientCertificate,
|
||||
key: clientKey,
|
||||
},
|
||||
json: {
|
||||
foo: 'bar',
|
||||
zip: 'zap',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -3,10 +3,12 @@ describe('e2e', () => {
|
||||
expect(process.env.SECRET).toBe("SUPERSECRET");
|
||||
expect(process.env.NAMED_SECRET).toBe("SUPERSECRET");
|
||||
expect(process.env.OTHERSECRET).toBe("OTHERSUPERSECRET");
|
||||
expect(process.env.OTHER_SECRET_OUTPUT).toBe("OTHERSUPERSECRET");
|
||||
expect(process.env.ALTSECRET).toBe("CUSTOMSECRET");
|
||||
expect(process.env.NAMED_ALTSECRET).toBe("CUSTOMSECRET");
|
||||
expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET");
|
||||
expect(process.env.FOO).toBe("bar");
|
||||
expect(process.env.NAMED_CUBBYSECRET).toBe("zap");
|
||||
expect(process.env.SUBSEQUENT_TEST_SECRET).toBe("SUBSEQUENT_TEST_SECRET");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
const got = require('got');
|
||||
|
||||
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.VAULT_TOKEN}` : "testtoken";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Verify Connection
|
||||
await got(`http://${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
await got(`http://${vaultUrl}/v1/secret/data/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
@@ -26,7 +27,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
await got(`http://${vaultUrl}/v1/secret/data/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
@@ -38,7 +39,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
await got(`http://${vaultUrl}/v1/sys/mounts/my-secret`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
type: 'kv'
|
||||
@@ -48,7 +49,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
await got(`http://${vaultUrl}/v1/my-secret/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
altSecret: 'CUSTOMSECRET',
|
||||
@@ -58,7 +59,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
await got(`http://${vaultUrl}/v1/my-secret/nested/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
otherAltSecret: 'OTHERCUSTOMSECRET',
|
||||
@@ -68,13 +69,25 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||
await got(`http://${vaultUrl}/v1/cubbyhole/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
foo: 'bar',
|
||||
zip: 'zap',
|
||||
},
|
||||
});
|
||||
|
||||
await got(`http://${vaultUrl}/v1/secret/data/subsequent-test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
json: {
|
||||
data: {
|
||||
secret: 'SUBSEQUENT_TEST_SECRET',
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -5,9 +5,10 @@ const core = require('@actions/core');
|
||||
const got = require('got');
|
||||
const { when } = require('jest-when');
|
||||
|
||||
const { exportSecrets } = require('../../action');
|
||||
const { exportSecrets } = require('../../src/action');
|
||||
|
||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
|
||||
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||
|
||||
describe('integration', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -15,7 +16,7 @@ describe('integration', () => {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,20 +44,20 @@ describe('integration', () => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url')
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token')
|
||||
.mockReturnValueOnce('testtoken');
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce(vaultToken);
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('namespace')
|
||||
.calledWith('namespace', expect.anything())
|
||||
.mockReturnValueOnce('ns1');
|
||||
});
|
||||
|
||||
it('get simple secret', async () => {
|
||||
mockInput('test secret');
|
||||
mockInput('secret/data/test secret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -64,7 +65,7 @@ describe('integration', () => {
|
||||
});
|
||||
|
||||
it('re-map secret', async () => {
|
||||
mockInput('test secret | TEST_KEY');
|
||||
mockInput('secret/data/test secret | TEST_KEY');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -72,7 +73,7 @@ describe('integration', () => {
|
||||
});
|
||||
|
||||
it('get nested secret', async () => {
|
||||
mockInput('nested/test otherSecret');
|
||||
mockInput('secret/data/nested/test otherSecret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -81,9 +82,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 +96,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 +104,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();
|
||||
|
||||
@@ -123,7 +120,7 @@ describe('authenticate with approle', () => {
|
||||
// Verify Connection
|
||||
await got(`${vaultUrl}/v1/secret/config`, {
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -141,7 +138,7 @@ describe('authenticate with approle', () => {
|
||||
await got(`${vaultUrl}/v1/sys/auth/approle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': 'ns2',
|
||||
},
|
||||
json: {
|
||||
@@ -161,7 +158,7 @@ describe('authenticate with approle', () => {
|
||||
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': 'ns2',
|
||||
},
|
||||
json: {
|
||||
@@ -174,7 +171,7 @@ describe('authenticate with approle', () => {
|
||||
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': 'ns2',
|
||||
},
|
||||
json: {
|
||||
@@ -185,7 +182,7 @@ describe('authenticate with approle', () => {
|
||||
// Get role-id
|
||||
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': 'ns2',
|
||||
},
|
||||
responseType: 'json',
|
||||
@@ -196,7 +193,7 @@ describe('authenticate with approle', () => {
|
||||
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': 'ns2',
|
||||
},
|
||||
responseType: 'json',
|
||||
@@ -212,24 +209,24 @@ describe('authenticate with approle', () => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('method')
|
||||
.calledWith('method', expect.anything())
|
||||
.mockReturnValueOnce('approle');
|
||||
when(core.getInput)
|
||||
.calledWith('roleId')
|
||||
.calledWith('roleId', expect.anything())
|
||||
.mockReturnValueOnce(roleId);
|
||||
when(core.getInput)
|
||||
.calledWith('secretId')
|
||||
.calledWith('secretId', expect.anything())
|
||||
.mockReturnValueOnce(secretId);
|
||||
when(core.getInput)
|
||||
.calledWith('url')
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce(`${vaultUrl}`);
|
||||
when(core.getInput)
|
||||
.calledWith('namespace')
|
||||
.calledWith('namespace', expect.anything())
|
||||
.mockReturnValueOnce('ns2');
|
||||
});
|
||||
|
||||
it('authenticate with approle', async()=> {
|
||||
mockInput('test secret');
|
||||
it('authenticate with approle', async() => {
|
||||
mockInput('secret/data/test secret');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
@@ -242,7 +239,7 @@ async function enableNamespace(name) {
|
||||
await got(`${vaultUrl}/v1/sys/namespaces/${name}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -260,7 +257,7 @@ async function enableEngine(path, namespace, version) {
|
||||
await got(`${vaultUrl}/v1/sys/mounts/${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': namespace,
|
||||
},
|
||||
json: { type: 'kv', config: {}, options: { version }, generate_signing_key: true },
|
||||
@@ -281,7 +278,7 @@ async function writeSecret(engine, path, namespace, version, data) {
|
||||
await got(`${vaultUrl}/v1/${secretPath}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Vault-Token': 'testtoken',
|
||||
'X-Vault-Token': vaultToken,
|
||||
'X-Vault-Namespace': namespace,
|
||||
},
|
||||
json: secretPayload
|
||||
@@ -290,18 +287,6 @@ async function writeSecret(engine, path, namespace, version, data) {
|
||||
|
||||
function mockInput(secrets) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets')
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(secrets);
|
||||
}
|
||||
|
||||
function mockEngineName(name) {
|
||||
when(core.getInput)
|
||||
.calledWith('path')
|
||||
.mockReturnValueOnce(name);
|
||||
}
|
||||
|
||||
function mockVersion(version) {
|
||||
when(core.getInput)
|
||||
.calledWith('kv-version')
|
||||
.mockReturnValueOnce(version);
|
||||
}
|
||||
|
||||
Generated
+7465
-13688
File diff suppressed because it is too large
Load Diff
+22
-22
@@ -4,24 +4,20 @@
|
||||
"description": "A Github Action that allows you to consume vault secrets as secure environment variables.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "ncc build index.js -o dist",
|
||||
"build": "ncc build src/entry.js -o dist",
|
||||
"test": "jest",
|
||||
"test:integration:basic": "jest -c integrationTests/basic/jest.config.js",
|
||||
"test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js",
|
||||
"test:e2e": "jest -c integrationTests/e2e/jest.config.js"
|
||||
},
|
||||
"release": {
|
||||
"branch": "master",
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/github"
|
||||
],
|
||||
"ci": false
|
||||
"test:integration:e2e": "jest -c integrationTests/e2e/jest.config.js",
|
||||
"test:integration:e2e-tls": "jest -c integrationTests/e2e-tls/jest.config.js"
|
||||
},
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"dist/**/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/RichiCoder1/vault-action.git"
|
||||
"url": "git+https://github.com/hashicorp/vault-action.git"
|
||||
},
|
||||
"keywords": [
|
||||
"hashicorp",
|
||||
@@ -31,21 +27,25 @@
|
||||
"github-actions",
|
||||
"javascript"
|
||||
],
|
||||
"author": "Richard Simpson <richardsimpson@outlook.com>",
|
||||
"author": "HashiCorp",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/RichiCoder1/vault-action/issues"
|
||||
"url": "https://github.com/hashicorp/vault-action/issues"
|
||||
},
|
||||
"homepage": "https://github.com/RichiCoder1/vault-action#readme",
|
||||
"homepage": "https://github.com/hashicorp/vault-action#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.2",
|
||||
"got": "^10.2.2"
|
||||
"got": "^11.8.5",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsrsasign": "^10.8.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@actions/core": ">=1 <2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.18",
|
||||
"@zeit/ncc": "^0.21.1",
|
||||
"jest": "^25.1.0",
|
||||
"jest-when": "^2.7.0",
|
||||
"semantic-release": "^15.13.24"
|
||||
"@actions/core": "^1.10.0",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-when": "^3.5.2",
|
||||
"mock-http-server": "^1.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// @ts-check
|
||||
const core = require('@actions/core');
|
||||
const command = require('@actions/core/lib/command');
|
||||
const got = require('got').default;
|
||||
const jsonata = require('jsonata');
|
||||
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
|
||||
|
||||
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
|
||||
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
|
||||
|
||||
async function exportSecrets() {
|
||||
const vaultUrl = core.getInput('url', { required: true });
|
||||
const vaultNamespace = core.getInput('namespace', { required: false });
|
||||
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
|
||||
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 secretsInput = core.getInput('secrets', { required: false });
|
||||
const secretRequests = parseSecretsInput(secretsInput);
|
||||
|
||||
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
|
||||
|
||||
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
|
||||
const authPayload = core.getInput('authPayload', { required: false });
|
||||
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
|
||||
throw Error(`Sorry, the provided authentication method ${vaultMethod} is not currently supported and no custom authPayload was provided.`);
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
prefixUrl: vaultUrl,
|
||||
headers: {},
|
||||
https: {},
|
||||
retry: {
|
||||
statusCodes: [
|
||||
...got.defaults.options.retry.statusCodes,
|
||||
// Vault returns 412 when the token in use hasn't yet been replicated
|
||||
// to the performance replica queried. See issue #332.
|
||||
412,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
|
||||
if (tlsSkipVerify === true) {
|
||||
defaultOptions.https.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const caCertificateRaw = core.getInput('caCertificate', { required: false });
|
||||
if (caCertificateRaw != null) {
|
||||
defaultOptions.https.certificateAuthority = Buffer.from(caCertificateRaw, 'base64').toString();
|
||||
}
|
||||
|
||||
const clientCertificateRaw = core.getInput('clientCertificate', { required: false });
|
||||
if (clientCertificateRaw != null) {
|
||||
defaultOptions.https.certificate = Buffer.from(clientCertificateRaw, 'base64').toString();
|
||||
}
|
||||
|
||||
const clientKeyRaw = core.getInput('clientKey', { required: false });
|
||||
if (clientKeyRaw != null) {
|
||||
defaultOptions.https.key = Buffer.from(clientKeyRaw, 'base64').toString();
|
||||
}
|
||||
|
||||
for (const [headerName, headerValue] of extraHeaders) {
|
||||
defaultOptions.headers[headerName] = headerValue;
|
||||
}
|
||||
|
||||
if (vaultNamespace != null) {
|
||||
defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||
}
|
||||
|
||||
const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions));
|
||||
core.setSecret(vaultToken)
|
||||
defaultOptions.headers['X-Vault-Token'] = vaultToken;
|
||||
const client = got.extend(defaultOptions);
|
||||
|
||||
if (outputToken === true) {
|
||||
core.setOutput('vault_token', `${vaultToken}`);
|
||||
}
|
||||
if (exportToken === true) {
|
||||
core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
|
||||
}
|
||||
|
||||
const requests = secretRequests.map(request => {
|
||||
const { path, selector } = request;
|
||||
return request;
|
||||
});
|
||||
|
||||
const results = await getSecrets(requests, client);
|
||||
|
||||
|
||||
for (const result of results) {
|
||||
// Output the result
|
||||
|
||||
var value = result.value;
|
||||
const request = result.request;
|
||||
const cachedResponse = result.cachedResponse;
|
||||
|
||||
if (cachedResponse) {
|
||||
core.debug('ℹ using cached response');
|
||||
}
|
||||
|
||||
// if a secret is encoded, decode it
|
||||
if (ENCODING_TYPES.includes(secretEncodingType)) {
|
||||
value = Buffer.from(value, secretEncodingType).toString();
|
||||
}
|
||||
|
||||
for (const line of value.replace(/\r/g, '').split('\n')) {
|
||||
if (line.length > 0) {
|
||||
core.setSecret(line);
|
||||
}
|
||||
}
|
||||
if (exportEnv) {
|
||||
core.exportVariable(request.envVarName, `${value}`);
|
||||
}
|
||||
core.setOutput(request.outputVarName, `${value}`);
|
||||
core.debug(`✔ ${request.path} => outputs.${request.outputVarName}${exportEnv ? ` | env.${request.envVarName}` : ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
/** @typedef {Object} SecretRequest
|
||||
* @property {string} path
|
||||
* @property {string} envVarName
|
||||
* @property {string} outputVarName
|
||||
* @property {string} selector
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a secrets input string into key paths and their resulting environment variable name.
|
||||
* @param {string} secretsInput
|
||||
*/
|
||||
function parseSecretsInput(secretsInput) {
|
||||
if (!secretsInput) {
|
||||
return []
|
||||
}
|
||||
|
||||
const secrets = secretsInput
|
||||
.split(';')
|
||||
.filter(key => !!key)
|
||||
.map(key => key.trim())
|
||||
.filter(key => key.length !== 0);
|
||||
|
||||
/** @type {SecretRequest[]} */
|
||||
const output = [];
|
||||
for (const secret of secrets) {
|
||||
let pathSpec = secret;
|
||||
let outputVarName = null;
|
||||
|
||||
const renameSigilIndex = secret.lastIndexOf('|');
|
||||
if (renameSigilIndex > -1) {
|
||||
pathSpec = secret.substring(0, renameSigilIndex).trim();
|
||||
outputVarName = secret.substring(renameSigilIndex + 1).trim();
|
||||
|
||||
if (outputVarName.length < 1) {
|
||||
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const pathParts = pathSpec
|
||||
.split(/\s+/)
|
||||
.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}"`);
|
||||
}
|
||||
|
||||
const [path, selectorQuoted] = pathParts;
|
||||
|
||||
/** @type {any} */
|
||||
const selectorAst = jsonata(selectorQuoted).ast();
|
||||
const 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}"`);
|
||||
}
|
||||
|
||||
let envVarName = outputVarName;
|
||||
if (!outputVarName) {
|
||||
outputVarName = normalizeOutputKey(selector);
|
||||
envVarName = normalizeOutputKey(selector, true);
|
||||
}
|
||||
|
||||
output.push({
|
||||
path,
|
||||
envVarName,
|
||||
outputVarName,
|
||||
selector
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any dot chars to __ and removes non-ascii charts
|
||||
* @param {string} dataKey
|
||||
* @param {boolean=} isEnvVar
|
||||
*/
|
||||
function normalizeOutputKey(dataKey, isEnvVar = false) {
|
||||
let outputKey = dataKey
|
||||
.replace('.', '__').replace(new RegExp('-', 'g'), '').replace(/[^\p{L}\p{N}_-]/gu, '');
|
||||
if (isEnvVar) {
|
||||
outputKey = outputKey.toUpperCase();
|
||||
}
|
||||
return outputKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} inputKey
|
||||
* @param {any} inputOptions
|
||||
*/
|
||||
function parseHeadersInput(inputKey, inputOptions) {
|
||||
/** @type {string}*/
|
||||
const rawHeadersString = core.getInput(inputKey, inputOptions) || '';
|
||||
const headerStrings = rawHeadersString
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line !== '');
|
||||
return headerStrings
|
||||
.reduce((map, line) => {
|
||||
const seperator = line.indexOf(':');
|
||||
const key = line.substring(0, seperator).trim().toLowerCase();
|
||||
const value = line.substring(seperator + 1).trim();
|
||||
if (map.has(key)) {
|
||||
map.set(key, [map.get(key), value].join(', '));
|
||||
} else {
|
||||
map.set(key, value);
|
||||
}
|
||||
return map;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exportSecrets,
|
||||
parseSecretsInput,
|
||||
normalizeOutputKey,
|
||||
parseHeadersInput
|
||||
};
|
||||
@@ -0,0 +1,375 @@
|
||||
jest.mock('got');
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
|
||||
const command = require('@actions/core/lib/command');
|
||||
const core = require('@actions/core');
|
||||
const got = require('got');
|
||||
const {
|
||||
exportSecrets,
|
||||
parseSecretsInput,
|
||||
parseHeadersInput
|
||||
} = require('./action');
|
||||
|
||||
const { when } = require('jest-when');
|
||||
|
||||
describe('parseSecretsInput', () => {
|
||||
it('parses simple secret', () => {
|
||||
const output = parseSecretsInput('test key');
|
||||
expect(output).toContainEqual({
|
||||
path: 'test',
|
||||
selector: 'key',
|
||||
outputVarName: 'key',
|
||||
envVarName: 'KEY'
|
||||
});
|
||||
});
|
||||
|
||||
it('parses mapped secret', () => {
|
||||
const output = parseSecretsInput('test key|testName');
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toMatchObject({
|
||||
outputVarName: 'testName',
|
||||
envVarName: 'testName',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on invalid mapped name', () => {
|
||||
expect(() => parseSecretsInput('test key|'))
|
||||
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
|
||||
});
|
||||
|
||||
it('fails on invalid path for mapped', () => {
|
||||
expect(() => parseSecretsInput('|testName'))
|
||||
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
|
||||
});
|
||||
|
||||
it('parses multiple secrets', () => {
|
||||
const output = parseSecretsInput('first a;second b;');
|
||||
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[0]).toMatchObject({
|
||||
path: 'first',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
path: 'second',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses multiple complex secret input', () => {
|
||||
const output = parseSecretsInput('first a;second b|secondName');
|
||||
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[0]).toMatchObject({
|
||||
outputVarName: 'a',
|
||||
envVarName: 'A',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
outputVarName: 'secondName',
|
||||
envVarName: 'secondName'
|
||||
});
|
||||
});
|
||||
|
||||
it('parses multiline input', () => {
|
||||
const output = parseSecretsInput(`
|
||||
first a;
|
||||
second b;
|
||||
third c | SOME_C;`);
|
||||
|
||||
expect(output).toHaveLength(3);
|
||||
expect(output[0]).toMatchObject({
|
||||
path: 'first',
|
||||
});
|
||||
expect(output[1]).toMatchObject({
|
||||
outputVarName: 'b',
|
||||
envVarName: 'B'
|
||||
});
|
||||
expect(output[2]).toMatchObject({
|
||||
outputVarName: 'SOME_C',
|
||||
envVarName: 'SOME_C',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseHeaders', () => {
|
||||
it('parses simple header', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce('TEST: 1');
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||
});
|
||||
|
||||
it('parses simple header with whitespace', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(`
|
||||
TEST: 1
|
||||
`);
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||
});
|
||||
|
||||
it('parses multiple headers', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(`
|
||||
TEST: 1
|
||||
FOO: bAr
|
||||
`);
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||
expect(Array.from(result)).toContainEqual(['foo', 'bAr']);
|
||||
});
|
||||
|
||||
it('parses null response', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(null);
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportSecrets', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce('http://vault:8200');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce('EXAMPLE');
|
||||
});
|
||||
|
||||
function mockInput(key) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(key);
|
||||
}
|
||||
|
||||
function mockVersion(version) {
|
||||
when(core.getInput)
|
||||
.calledWith('kv-version', expect.anything())
|
||||
.mockReturnValueOnce(version);
|
||||
}
|
||||
|
||||
function mockExtraHeaders(headerString) {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders', expect.anything())
|
||||
.mockReturnValueOnce(headerString);
|
||||
}
|
||||
|
||||
function mockVaultData(data, version='2') {
|
||||
switch(version) {
|
||||
case '1':
|
||||
got.extend.mockReturnValue({
|
||||
get: async () => ({ body: JSON.stringify({ data }) })
|
||||
});
|
||||
break;
|
||||
case '2':
|
||||
got.extend.mockReturnValue({
|
||||
get: async () => ({ body: JSON.stringify({ data: {
|
||||
data
|
||||
} }) })
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function mockExportToken(doExport) {
|
||||
when(core.getInput)
|
||||
.calledWith('exportToken', expect.anything())
|
||||
.mockReturnValueOnce(doExport);
|
||||
}
|
||||
|
||||
function mockOutputToken(doOutput) {
|
||||
when(core.getInput)
|
||||
.calledWith('outputToken', expect.anything())
|
||||
.mockReturnValueOnce(doOutput);
|
||||
}
|
||||
function mockEncodeType(doEncode) {
|
||||
when(core.getInput)
|
||||
.calledWith('secretEncodingType', expect.anything())
|
||||
.mockReturnValueOnce(doEncode);
|
||||
}
|
||||
|
||||
it('simple secret retrieval', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('encoded secret retrieval', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 'MQ=='
|
||||
});
|
||||
mockEncodeType('base64');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('intl secret retrieval', async () => {
|
||||
mockInput('测试 测试');
|
||||
mockVaultData({
|
||||
测试: 1
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('测试', '1');
|
||||
expect(core.setOutput).toBeCalledWith('测试', '1');
|
||||
});
|
||||
|
||||
it('mapped secret retrieval', async () => {
|
||||
mockInput('test key|TEST_NAME');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('TEST_NAME', '1');
|
||||
expect(core.setOutput).toBeCalledWith('TEST_NAME', '1');
|
||||
});
|
||||
|
||||
it('simple secret retrieval from K/V v1', async () => {
|
||||
const version = '1';
|
||||
|
||||
mockInput('test key');
|
||||
mockExtraHeaders(`
|
||||
TEST: 1
|
||||
`);
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('simple secret retrieval with extra headers', async () => {
|
||||
const version = '1';
|
||||
|
||||
mockInput('test key');
|
||||
mockVersion(version);
|
||||
mockVaultData({
|
||||
key: 1
|
||||
}, version);
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('nested secret retrieval', async () => {
|
||||
mockInput('test key.value');
|
||||
mockVaultData({
|
||||
key: { value: 1 }
|
||||
});
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY__VALUE', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key__value', '1');
|
||||
});
|
||||
|
||||
it('export Vault token', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
mockExportToken("true")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledTimes(2);
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('VAULT_TOKEN', 'EXAMPLE');
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('not export Vault token', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 1
|
||||
});
|
||||
mockExportToken("false")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledTimes(1);
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('single-line secret gets masked', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 'secret'
|
||||
});
|
||||
mockExportToken("false")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.setSecret).toBeCalledTimes(2);
|
||||
|
||||
expect(core.setSecret).toBeCalledWith('secret');
|
||||
expect(core.setOutput).toBeCalledWith('key', 'secret');
|
||||
})
|
||||
|
||||
it('multi-line secret gets masked for each line', async () => {
|
||||
const multiLineString = `a multi-line string
|
||||
|
||||
with blank lines
|
||||
|
||||
`
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: multiLineString
|
||||
});
|
||||
mockExportToken("false")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.setSecret).toBeCalledTimes(3); // 1 for each non-empty line.
|
||||
|
||||
expect(core.setSecret).toBeCalledWith('a multi-line string');
|
||||
expect(core.setSecret).toBeCalledWith('with blank lines');
|
||||
expect(core.setOutput).toBeCalledWith('key', multiLineString);
|
||||
})
|
||||
|
||||
it('export only Vault token, no secrets', async () => {
|
||||
mockExportToken("true")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledTimes(1);
|
||||
expect(core.exportVariable).toBeCalledWith('VAULT_TOKEN', 'EXAMPLE');
|
||||
})
|
||||
|
||||
it('output only Vault token, no secrets', async () => {
|
||||
mockOutputToken("true")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.setOutput).toBeCalledTimes(1);
|
||||
expect(core.setOutput).toBeCalledWith('vault_token', 'EXAMPLE');
|
||||
})
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
// @ts-check
|
||||
const core = require('@actions/core');
|
||||
const rsasign = require('jsrsasign');
|
||||
const fs = require('fs');
|
||||
const { default: got } = require('got');
|
||||
|
||||
const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||
/***
|
||||
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
|
||||
* @param {string} method
|
||||
* @param {import('got').Got} client
|
||||
*/
|
||||
async function retrieveToken(method, client) {
|
||||
let path = core.getInput('path', { required: false }) || method;
|
||||
path = `v1/auth/${path}/login`
|
||||
|
||||
switch (method) {
|
||||
case 'approle': {
|
||||
const vaultRoleId = core.getInput('roleId', { required: true });
|
||||
const vaultSecretId = core.getInput('secretId', { required: true });
|
||||
return await getClientToken(client, method, path, { role_id: vaultRoleId, secret_id: vaultSecretId });
|
||||
}
|
||||
case 'github': {
|
||||
const githubToken = core.getInput('githubToken', { required: true });
|
||||
return await getClientToken(client, method, path, { token: githubToken });
|
||||
}
|
||||
case 'jwt': {
|
||||
/** @type {string} */
|
||||
let jwt;
|
||||
const role = core.getInput('role', { required: false });
|
||||
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false });
|
||||
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
|
||||
const keyPassword = core.getInput('jwtKeyPassword', { required: false });
|
||||
const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour
|
||||
const githubAudience = core.getInput('jwtGithubAudience', { required: false });
|
||||
|
||||
if (!privateKey) {
|
||||
jwt = await core.getIDToken(githubAudience)
|
||||
} else {
|
||||
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
|
||||
}
|
||||
|
||||
return await getClientToken(client, method, path, { jwt: jwt, role: role });
|
||||
}
|
||||
case 'kubernetes': {
|
||||
const role = core.getInput('role', { required: true })
|
||||
const tokenPath = core.getInput('kubernetesTokenPath', { required: false }) || defaultKubernetesTokenPath
|
||||
const data = fs.readFileSync(tokenPath, 'utf8')
|
||||
if (!(role && data) && data != "") {
|
||||
throw new Error("Role Name must be set and a kubernetes token must set")
|
||||
}
|
||||
return await getClientToken(client, method, path, { jwt: data, role: role })
|
||||
}
|
||||
case 'userpass':
|
||||
case 'ldap': {
|
||||
const username = core.getInput('username', { required: true });
|
||||
const password = core.getInput('password', { required: true });
|
||||
path = path + `/${username}`
|
||||
return await getClientToken(client, method, path, { password: password })
|
||||
}
|
||||
|
||||
default: {
|
||||
if (!method || method === 'token') {
|
||||
return core.getInput('token', { required: true });
|
||||
} else {
|
||||
/** @type {string} */
|
||||
const payload = core.getInput('authPayload', { required: true });
|
||||
if (!payload) {
|
||||
throw Error('When using a custom authentication method, you must provide the payload');
|
||||
}
|
||||
return await getClientToken(client, method, path, JSON.parse(payload.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Generates signed Json Web Token with specified private key and ttl
|
||||
* @param {string} privateKey
|
||||
* @param {string} keyPassword
|
||||
* @param {number} ttl
|
||||
*/
|
||||
function generateJwt(privateKey, keyPassword, ttl) {
|
||||
const alg = 'RS256';
|
||||
const header = { alg: alg, typ: 'JWT' };
|
||||
const now = rsasign.KJUR.jws.IntDate.getNow();
|
||||
const payload = {
|
||||
iss: 'vault-action',
|
||||
iat: now,
|
||||
nbf: now,
|
||||
exp: now + ttl,
|
||||
event: process.env.GITHUB_EVENT_NAME,
|
||||
workflow: process.env.GITHUB_WORKFLOW,
|
||||
sha: process.env.GITHUB_SHA,
|
||||
actor: process.env.GITHUB_ACTOR,
|
||||
repository: process.env.GITHUB_REPOSITORY,
|
||||
ref: process.env.GITHUB_REF
|
||||
};
|
||||
const decryptedKey = rsasign.KEYUTIL.getKey(privateKey, keyPassword);
|
||||
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
|
||||
}
|
||||
|
||||
/***
|
||||
* Call the appropriate login endpoint and parse out the token in the response.
|
||||
* @param {import('got').Got} client
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
* @param {any} payload
|
||||
*/
|
||||
async function getClientToken(client, method, path, payload) {
|
||||
/** @type {'json'} */
|
||||
const responseType = 'json';
|
||||
var options = {
|
||||
json: payload,
|
||||
responseType,
|
||||
};
|
||||
|
||||
core.debug(`Retrieving Vault Token from ${path} endpoint`);
|
||||
|
||||
/** @type {import('got').Response<VaultLoginResponse>} */
|
||||
let response;
|
||||
try {
|
||||
response = await client.post(`${path}`, options);
|
||||
} catch (err) {
|
||||
if (err instanceof got.HTTPError) {
|
||||
throw Error(`failed to retrieve vault token. code: ${err.code}, message: ${err.message}, vaultResponse: ${JSON.stringify(err.response.body)}`)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (response && response.body && response.body.auth && response.body.auth.client_token) {
|
||||
core.debug('✔ Vault Token successfully retrieved');
|
||||
|
||||
core.startGroup('Token Info');
|
||||
core.debug(`Operating under policies: ${JSON.stringify(response.body.auth.policies)}`);
|
||||
core.debug(`Token Metadata: ${JSON.stringify(response.body.auth.metadata)}`);
|
||||
core.endGroup();
|
||||
|
||||
return response.body.auth.client_token;
|
||||
} else {
|
||||
throw Error(`Unable to retrieve token from ${method}'s login endpoint.`);
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* @typedef {Object} VaultLoginResponse
|
||||
* @property {{
|
||||
* client_token: string;
|
||||
* accessor: string;
|
||||
* policies: string[];
|
||||
* metadata: unknown;
|
||||
* lease_duration: number;
|
||||
* renewable: boolean;
|
||||
* }} auth
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
retrieveToken,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
jest.mock('got');
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
jest.mock('fs', () => ({
|
||||
stat: jest.fn().mockResolvedValue(null),
|
||||
promises: {
|
||||
access: jest.fn().mockResolvedValue(null),
|
||||
}
|
||||
}));
|
||||
|
||||
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, expect.anything())
|
||||
.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 testRole = "testRole"
|
||||
const testTokenPath = "testTokenPath"
|
||||
const testPath = 'differentK8sPath'
|
||||
mockApiResponse()
|
||||
mockInput("kubernetesTokenPath", testTokenPath)
|
||||
mockInput("role", testRole)
|
||||
mockInput("path", testPath)
|
||||
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: testRole })
|
||||
const url = got.post.mock.calls[0][0]
|
||||
expect(url).toContain('differentK8sPath')
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ const { exportSecrets } = require('./action');
|
||||
try {
|
||||
await core.group('Get Vault Secrets', exportSecrets);
|
||||
} catch (error) {
|
||||
core.setOutput("errorMessage", error.message);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
const auth = require('./auth');
|
||||
const secrets = require('./secrets');
|
||||
|
||||
module.exports = {
|
||||
auth,
|
||||
secrets
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
jest.mock('@actions/core');
|
||||
|
||||
const core = require('@actions/core');
|
||||
const ServerMock = require("mock-http-server");
|
||||
const { exportSecrets } = require("./action");
|
||||
const { when } = require('jest-when');
|
||||
|
||||
describe('exportSecrets retries', () => {
|
||||
var server = new ServerMock({ host: "127.0.0.1", port: 0 });
|
||||
var calls = 0;
|
||||
|
||||
beforeEach((done) => {
|
||||
calls = 0;
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce('EXAMPLE');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce("kv/mysecret key");
|
||||
|
||||
server.start(() => {
|
||||
expect(server.getHttpPort()).not.toBeNull();
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce('http://127.0.0.1:' + server.getHttpPort());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
server.stop(done);
|
||||
});
|
||||
|
||||
function mockStatusCodes(statusCodes) {
|
||||
server.on({
|
||||
path: '/v1/kv/mysecret',
|
||||
reply: {
|
||||
status: function() {
|
||||
let status = statusCodes[calls];
|
||||
calls += 1;
|
||||
return status;
|
||||
},
|
||||
headers: { "content-type": "application/json" },
|
||||
body: function() {
|
||||
return JSON.stringify({ data: {"key": "value"} })
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('retries on 412 status code', (done) => {
|
||||
mockStatusCodes([412, 200])
|
||||
exportSecrets().then(() => {
|
||||
expect(calls).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('retries on 500 status code', (done) => {
|
||||
mockStatusCodes([500, 200])
|
||||
exportSecrets().then(() => {
|
||||
expect(calls).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} SecretRequest
|
||||
* @property {string} path
|
||||
* @property {string} selector
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {SecretRequest} TRequest
|
||||
* @typedef {Object} SecretResponse
|
||||
* @property {TRequest} request
|
||||
* @property {string} value
|
||||
* @property {boolean} cachedResponse
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template TRequest
|
||||
* @param {Array<TRequest>} secretRequests
|
||||
* @param {import('got').Got} client
|
||||
* @return {Promise<SecretResponse<TRequest>[]>}
|
||||
*/
|
||||
async function getSecrets(secretRequests, client) {
|
||||
const responseCache = new Map();
|
||||
const results = [];
|
||||
for (const secretRequest of secretRequests) {
|
||||
let { path, selector } = secretRequest;
|
||||
|
||||
const requestPath = `v1/${path}`;
|
||||
let body;
|
||||
let cachedResponse = false;
|
||||
if (responseCache.has(requestPath)) {
|
||||
body = responseCache.get(requestPath);
|
||||
cachedResponse = true;
|
||||
} else {
|
||||
try {
|
||||
const result = await client.get(requestPath);
|
||||
body = result.body;
|
||||
responseCache.set(requestPath, body);
|
||||
} catch (error) {
|
||||
const {response} = error;
|
||||
if (response?.statusCode === 404) {
|
||||
throw Error(`Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if (!selector.match(/.*[\.].*/)) {
|
||||
selector = '"' + selector + '"'
|
||||
}
|
||||
selector = "data." + selector
|
||||
body = JSON.parse(body)
|
||||
if (body.data["data"] != undefined) {
|
||||
selector = "data." + selector
|
||||
}
|
||||
|
||||
const value = await selectData(body, selector);
|
||||
results.push({
|
||||
request: secretRequest,
|
||||
value,
|
||||
cachedResponse
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a Jsonata selector retrieve a bit of data from the result
|
||||
* @param {object} data
|
||||
* @param {string} selector
|
||||
*/
|
||||
async function selectData(data, selector) {
|
||||
const ata = jsonata(selector);
|
||||
let result = JSON.stringify(await ata.evaluate(data));
|
||||
// Compat for custom engines
|
||||
if (!result && ((ata.ast().type === "path" && ata.ast()['steps'].length === 1) || ata.ast().type === "string") && selector !== 'data' && 'data' in data) {
|
||||
result = JSON.stringify(await jsonata(`data.${selector}`).evaluate(data));
|
||||
} else if (!result) {
|
||||
throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`);
|
||||
}
|
||||
|
||||
if (result.startsWith(`"`)) {
|
||||
result = JSON.parse(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSecrets,
|
||||
selectData
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user