From 05074af56353aca8ccc37db9e69d31c8e8934418 Mon Sep 17 00:00:00 2001 From: Max Wagner <3364111+wagnerm@users.noreply.github.com> Date: Wed, 22 Mar 2023 15:34:31 -0600 Subject: [PATCH 1/3] Add option to retry Vault Token retrieval Sometimes we might encounter errors when retrieving the Vault token using a method like JWT. In those cases, the action does not retry the request today because the got package does not try POST requests by default. This change adds an option called retryVaultTokenRetrieval that will add the POST method to the retriable methods got uses. The post method is not used in any other place in this action, so having the POST method added to the defaultOptions seems okay for now. --- action.yml | 3 +++ src/action.js | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/action.yml b/action.yml index 073f579..e182e43 100644 --- a/action.yml +++ b/action.yml @@ -79,6 +79,9 @@ inputs: 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 + retryVaultTokenRetrieval: + description: 'Enable retrying retrieval of Vault server tokens. If not specified the token request to the Vault server will only be tried once.' + required: false runs: using: 'node16' main: 'dist/index.js' diff --git a/src/action.js b/src/action.js index b898005..256980c 100644 --- a/src/action.js +++ b/src/action.js @@ -31,6 +31,7 @@ async function exportSecrets() { headers: {}, https: {}, retry: { + methods: [...got.defaults.options.retry.methods], statusCodes: [ ...got.defaults.options.retry.statusCodes, // Vault returns 412 when the token in use hasn't yet been replicated @@ -68,6 +69,11 @@ async function exportSecrets() { defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; } + const retryVaultTokenRetrieval = (core.getInput('retryVaultTokenRetrieval', { required: false }) || 'false').toLowerCase() != 'false'; + if (retryVaultTokenRetrieval === true) { + defaultOptions.retry.methods.push('POST'); + } + const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions)); defaultOptions.headers['X-Vault-Token'] = vaultToken; const client = got.extend(defaultOptions); From 5fc6aa310bda268b6f87ce9b2a6aa1c0304dd16d Mon Sep 17 00:00:00 2001 From: Max Wagner <3364111+wagnerm@users.noreply.github.com> Date: Mon, 3 Apr 2023 21:06:32 +0000 Subject: [PATCH 2/3] Add a test for Vault token retrieval retries When the retryVaultTokenRetrieval option is set in the action we will now see HTTP errors when retrieving the Vault token retried. This adds a test block to test the client.post that is performed during the token retrieval is retried on an HTTP error, like a 500. --- src/retries.test.js | 87 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/retries.test.js b/src/retries.test.js index 132edd5..7711313 100644 --- a/src/retries.test.js +++ b/src/retries.test.js @@ -66,4 +66,89 @@ describe('exportSecrets retries', () => { done(); }); }); -}); \ No newline at end of file +}); + +describe('exportSecrets retrieve token 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"); + + 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('retryVaultTokenRetrieval', expect.anything()) + .mockReturnValueOnce('true') + + 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 mockKvRetrieval() { + server.on({ + path: '/v1/kv/mysecret', + reply: { + status: 200, + headers: { "content-type": "application/json" }, + body: function() { + return JSON.stringify({ data: {"key": "value"} }) + } + } + }); + } + + function mockStatusCodes(statusCodes) { + server.on({ + method: 'POST', + path: '/v1/auth/approle/login', + reply: { + status: function() { + let status = statusCodes[calls]; + calls += 1; + return status; + }, + body: function() { + return JSON.stringify({ auth: {"client_token": "token"} }); + } + } + }); + } + + it('retries on 500 status code', (done) => { + mockKvRetrieval() + mockStatusCodes([500, 201]) + exportSecrets().then(() => { + expect(calls).toEqual(2); + done(); + }); + }); +}); From c1b8c73d07321e4ba74dc011ce12e5ae577fdf0b Mon Sep 17 00:00:00 2001 From: Max Wagner <3364111+wagnerm@users.noreply.github.com> Date: Mon, 3 Apr 2023 21:19:26 +0000 Subject: [PATCH 3/3] Add retryVaultTokenRetrieval input to README --- README.md | 58 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f01a059..113a7cd 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Retrieved secrets are available as environment variables or outputs for subseque # ... ``` -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. +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 #... @@ -420,31 +420,33 @@ steps: 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` | | -| `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` | | -| `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` | | +| 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` | | +| `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` | | +| `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` | | +| `retryVaultTokenRetrieval` | When set to true, attempts to authenticate with Vault will be retried when an HTTP error occurs | `false` | | + ## Masking - Hiding Secrets from Logs @@ -473,7 +475,7 @@ $ npm install && npm run build ### Vault test instance -The Github Action needs access to a working Vault instance to function. +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 @@ -520,4 +522,4 @@ Edit the ./.github/workflows/local-test.yaml file to use your new feature branch Run your feature branch locally. ```sh $ act local-test -``` \ No newline at end of file +```