Custom Vault Integrations: Javascript

Introduction

Hashicorp’s Vault has become the industry standard tool for secrets management. It comes packaged with multiple solutions for engines supporting various authentication methods, secrets integrations, and storage. However, not every application has a first-party, third-party, or community secrets engine integration with Vault. This is especially true for internally developed custom applications and software that your company may utilize internally or as customer-facing products. In these situations, you often need to develop a custom integration with Vault.

These articles will guide you in developing a basic custom secrets engine integration with Vault in each of four common languages. It is assumed that you are already familiar with the fundamentals of administrating a Vault cluster, the common Vault engines, and coding in the language(s) of your choice that are demonstrated in each article.

This article will focus on a custom Vault integration developed in Javascript.

Bindings Installation

Vault has a well documented REST API, and you can certainly interact with it through the REST API bindings in the application language. However, bindings to the Vault API already exist in several languages, and these should be used for their obvious advantages. In this section, we will see how to install those bindings in Javascript.

The Javascript bindings for Vault within the Node runtime are, like HVAC for Python, also community developed and maintained, but also still commonly used across the field. Similar to HVAC, they implement all of the expected functionality in a familiar interface for the language. Simply add the bindings to your package.json:

"dependencies": {
  "node-vault": "^0.9.22"
},

and then install with npm as per usual.

There is no formal documentation (besides the README) for node-vault, so one generally relies upon source code and examples.

Client Construction and Authentication

The first step in interacting with the Vault API through the bindings is to construct a client for the interface. After constructing the client, the member method for authentication with a token can then be invoked. This code is actually remarkably similar across the different language bindings.

Due to subtleties and styles in Javascript, we will use a lambda to return an authenticated client. We initialize a map with the Vault configuration specifying the API version and Vault cluster address. The address is retrieved from the normal environment variable. We then initialize a Vault client from the bindings method while passing the configuration as the input argument. Afterwards, a lambda returns an authenticated client by retrieving the token from the normal environment variable, and assigning it to the previously initialized Vault client. This authenticated client is then returned and assigned to the client variable.

const vaultConfig = {
  apiVersion: 'v1',
  endpoint:   process.env.VAULT_ADDR,
};
const vaultClient = require('node-vault')(vaultConfig);

const client = async () => {
  const vaultToken = process.env.VAULT_TOKEN;

  return vaultClient.token = vaultToken;
}

The authenticated client can now potentially generate more tokens if its associated policy authorizes that action. In the below code, we generate a new token object. The resulting object is then parsed in the client_token in the auth to return the actual token. We perform a token lookup on the new token to determine its capabilities. We can then inspect and use the output accordingly since it was never assigned to any variable.

vaultClient.tokenCreate()
.then((result) => {
  console.log(result);
  return vaultClient.tokenLookup({ token: result.auth.client_token });
})
.then((result) => {
  console.log(result);
})
.catch((err) => console.error(err.message));

Alternate Authentication Engines

You may not want to use the basic Token Authentication engine (and understandably so) for authentication with the Vault client. In that situation, we can utilize a few alternatives.

In the below lambda function, we demonstrate how to use AppRole authentication with the “push” method. Passing in the role id and secret id as input arguments form the environment for security reasons returns a response from the Vault API approle login endpoint. This response contains the token necessary for authentication and authorization according to the associated role and policy. We can use the client_token from auth in the response to easily authenticate the client afterwards.

const client = async () => {
  const response = await vaultClient.approleLogin({
    role_id:   process.env.ROLE_ID,
    secret_id: process.env.SECRET_ID,
  });

  return vaultClient.token = response.auth.client_token;
}

In the below method, we demonstrate how to use AppRole authentication with the “pull” method. This is the newer procedure that is more secure than the “push” procedure, and also requires multiple personas. In the below code, we demonstrate how to accomplish this with a single promise.

The promise begins by using the authenticated and authorized client to retrieve the authentication object from the role name input argument. The returned object is then parsed to assign the values to the respective role id and secret id from the nested data. The role id and secret id are then used to authenticate through an AppRole login. Note that this is the two persona version of the “pull” method where the token is not wrapped (which would be three personas).

const roleName = process.env.ROLE_NAME;

Promise.all([vaultClient.getApproleRoleId({ role_name: roleName }),
  vaultClient.getApproleRoleSecret({ role_name: roleName })])
.then((result) => {
  const roleId = result[0].data.role_id;
  const secretId = result[1].data.secret_id;

  return vaultClient.approleLogin({
    role_id:   roleId,
    secret_id: secretId,
  });
})
.then((result) => {
  console.log(result);
})

We can also easily authenticate a client for an application deployed to Kubernetes by using the Kubernetes authentication engine. We simply input the name of the app corresponding to the role, and the application’s generated JSON web token for the authentication login. The client is then authenticated and authorized. Note that applications deployed to Kubernetes do also have the option of the Vault agent running as a sidecar to inject secrets into workload containers as k8s secrets.

vaultClient.kubernetesLogin({ role: process.env.K8S_APP, jwt: process.env.K8S_JWT }))
.then(console.log)
.catch((err) => console.error(err.message));

Secrets Retrieval (KV2 Engine)

Now that we have a connected, authenticated, and authorized client, we are able to begin interfacing with the KV2 secrets engine and retrieve secrets. Assuming we have a config map with some user input variable values, we can read a secret from a specified KV2 secret engine mount point and secret path. This returns a secret object similar to the Go and Ruby bindings. What is surprisingly different from the other three bindings is that the immediate return is the secret object map accessed within the data, and it does not require a data accessor. This is due to the bindings naturally supporting KV1, and the KV2 support is “enabled” by adding data to the path between the mount and the secret (similar to Ruby with the additional data key).

For demonstration purposes, we show how to print out the key value pairs in the secret using iteration. You would naturally almost never want to actually do this outside of debug logging. We then return the map of secret key value pairs accessed within the secret data.

const secretData = async () => {
  const { secret } = await vaultClient.read(`${config.get('mount_point')}/data/${config.get('secret_path')}`);

  // for demonstration purposes
  for (let [key, value] of secret.data.entries()) {
    console.log(`secret key: ${key}, secret_value: ${key}`)
  }
  secret.data.forEach((key, value) {
    console.log(`secret key: ${key}, secret_value: ${key}`)
  })

  return secret.data;
}

We can also perform multiple actions consecutively in the normal manner within Javascript. Below we can see a quick execution for writing, reading, and deleting a secret with the Vault bindings (ergo a one second lease on the secret).

vaultClient.write('secret/data/foo', { value: 'bar', lease: '1s' })
.then(() => vaultClient.read('secret/data/foo'))
.then(() => vaultClient.delete('secret/data/foo'))
.catch((err) => console.error(err.message));

Ending

Now we have a custom Vault integration for your application developed in Javascript. This code can be used as a package within your application or other software so that it can independently connect, authenticate, authorize, and retrieve secrets with an existing Vault cluster in a self-contained procedure. The application is now capable of direct interfacing to Vault, and does not require workarounds using additional tooling. Once you feel comfortable implementing the functionality in this article, you can then expand to support additional client configs, and other authentication and secrets engines.

If your organization is interested in custom Vault integrations for your applications or other software you develop or otherwise utilize, contact Shadow-Soft below.

  • This field is for validation purposes and should be left unchanged.