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
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
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