Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add ability to retry vault token retrieval #442

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 30 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
#...
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
```
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
87 changes: 86 additions & 1 deletion src/retries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,89 @@ describe('exportSecrets retries', () => {
done();
});
});
});
});

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();
});
});
});