Vault: NodeJS Express web app using node-vault to fetch secrets

HashiCorp Vault is a secret and encryption management system that allows your organization to secure sensitive information such as API keys, certificates, and passwords.

In this article, I will show how a NodeJS Express web application deployed into a Kubernetes cluster can fetch a secret directly from the Vault server using the node-vault module.

This means that sensitive values no longer need to be baked into the image, set in kubernetes yaml manifest files, placed into base64-encoded Kubernetes secrets, or volume mounted to the filesystem of the container.

Unlike many of the examples found online which use the AppRole auth method for NodeJS, we will use Kubernetes auth mode since it is deployed into a Kubernetes cluster and has access to the Kubernetes Service Account JWT.

Prerequisite

I am assuming you already have a Vault server installed, with the Kubernetes auth method enabled.  If you do not, see my previous article on installing a development Vault server on a Kubernetes cluster.

Deploy Vault enabled NodeJS Express app to Kubernetes

We will go through implementation details in later sections, but first let’s just deploy the NodeJS Express web app into your Kubernetes cluster so we can see the end result of fetching a Vault secret.

Example Vault secret and details

This NodeJS app was created generic enough that you can specify your own secret path and role, pointing at any remote Vault server.  But for this article, I will be using the values setup in my previous article and you should feel free to replace it with your environment specific details.

Based on the previous article, there exists a kv2 Vault secret at “secret/webapp/config” with the following keys:

  • foo=bar
  • username=static-user
  • password=static-password

There is a Vault role named “myrole” and policy named “mypolicy” which provides ‘read’ permission to the secret path above.

Kubernetes auth is enabled on the Vault server, using the Kubernetes Service Account “vault-auth” in the Kubernetes namespace “vault”.

Apply yaml manifest to Kubernetes cluster

# fetch source code for this article
cd ~
git clone https://github.com/fabianlee/nodejs-web-with-node-vault.git && cd $(basename $_ .git)

# Kubernetes deployment name, namespace, and service account
export app=nodejs-express-vault
export namespace_k8s=vault
export service_account_k8s=vault-auth
echo "Deployment '$app' going into k8s namespace '$namespace_k8s' running under the service account '$service_account_k8s'"

# Vault server location
export vault_uri=http://vault.vault.svc.cluster.local:8200

# Your specific Vault role and secret path
export vault_role=myrole
export vault_backend=secret
export vault_context=webapp
export vault_profile=config
echo "going to fetch kv2 secret using role '$vault_role' at path: $vault_backend/data/$vault_context/$vault_profile"

# generate from template and deploy to cluster
cat deployment.yaml | DOLLAR_SIGN='$' envsubst | kubectl apply -f -

# deployment should now exist
kubectl get deployment $app -n $namespace_k8s

Validate fetch of secret from Vault server

Test an explicit fetch of the secret from the Vault server using the node-vault module.  This will return the keys of the secret in JSON format.

# fetch secret using node-vault module
$ kubectl exec -it deployment/$app -n $vault_ns -- curl http://localhost:4000/secret
{"foo":"bar","password":"static-password","username":"static-user"}

Implementation Details

Now that you’ve seen the NodeJS app successfully fetch the Vault secret directly and show its keys, let’s look at some critical sections of configuration and code in index.js.

node-vault module

Require the node-vault module and use the VAULT_URI environment variable specified in the kubernetes manifest at ‘spec.template.spec.containers[main].env’.

const vault = require("node-vault")({
  apiVersion: "v1",
  endpoint: process.env.VAULT_URI || "http://vault.vault.svc.cluster.local:8200"
});

Vault login using Kubernetes auth mode

The Vault role is specified in the kubernetes manifest at ‘spec.template.spec.containers[main].env’.

The JWT for the Kubernetes Service Account configured for Vault can be found in a fixed file location.  Use this token and role to establish a login to the Vault Server.

const vaultRole = process.env.VAULT_ROLE || "myrole";
const JWT_TOKEN_FILE="/var/run/secrets/kubernetes.io/serviceaccount/token";

async function doVaultLogin() {
  var fs = require('fs');
  const jwt = fs.readFileSync(JWT_TOKEN_FILE);

  const loginResult = await vault.kubernetesLogin({
    "role": vaultRole,
    "jwt": jwt.toString()
  });
}

Fetching Vault Secret

The values for constructing the full secret path come from the Kubernetes manifest at ‘spec.template.spec.containers[main].env’.

Fetching the secret is then just a matter of calling the asnc method with an await so it returns the secrets keys located in the map ‘spec.data.data’.

// full path to kv2 secret
const fullSecretPath = (process.env.VAULT_BACKEND || "secret") + "/" +
    "data/" +
    (process.env.VAULT_CONTEXT || "webapp") + "/" +
    (process.env.VAULT_PROFILE || "config");
console.log("secret path: " + fullSecretPath);
...

// async function for fetching Vault secret
let fetchSecretPromise = function(secretPath) {
    return vault.read(secretPath);
}

app.get('/secret', async (req,res) => {
  let secret = await vault.read(fullSecretPath);
  res.json(secret.data.data);
})

Note the Vault token acquired at application startup will expire based on the “ttl” set in the Vault role.  When the Vault token expires, you will start to get 403 permissions errors from vault.read().  This is addressed in my final index.js by surrounding it with a try/catch and refreshing the token with doVaultLogin().

Express startup

The NodeJS Express app starts a listener on port 4000, and does an initial Vault login.

// startup main listener
app.listen(4000, () => {
  doVaultLogin();
  console.log('Server running on port 4000')
})

 

 

 

REFERENCES

Spring docs, Spring Cloud Vault intro

HashiCorp, why vault?

node-vault module for NodeJS

Github fabianlee, source for the code in this article

Kentaro Wayama, managing secrets in NodeJS with HashiCorp Vault

Stackoverflow.com, login and get secret from HashiCorp Vault in NodeJS

Mozilla developer, Promise

Mozilla developer, await

Github Actions, building for NodeJS

Github action for ‘setup-node’

ServerLab.ca, how to deploy NodeJS apps in k8s

sweetcode.io, deploying NodeJS app to K8s using helm

HashiCorp, compare sidecar to CSI to Operator

Baeldung, Spring Vault and Kubernetes showing different methods of access (explicit, semi-explicit, sidecar, Secret CSI, Secret Operator)