diff --git a/README.md b/README.md index f01a0593..113a7cdd 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 +``` diff --git a/action.yml b/action.yml index 073f5794..e182e430 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 b898005d..256980c2 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); diff --git a/src/retries.test.js b/src/retries.test.js index 132edd52..77113134 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(); + }); + }); +});