Skip to content

namecheap/node-vault-client

Node.js Vault Client

Build Status Download Status NPM Version License Dependency Status

A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.

Install

npm install --save node-vault-client

Requirements

Node.js >= 18 — the client uses the native fetch API.

Example

const VaultClient = require('node-vault-client');

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: { 
        type: 'appRole', // one of: 'appRole' | 'token' | 'iam' | 'kubernetes'
        config: { role_id: '637c065f-c644-5e12-d3d1-e9fa4363af61' } 
    },
});

vaultClient.read('secret/tst').then(lease => {
    console.log(lease.getData()); // read() resolves to a Lease; use getData()/getValue(key)
}).catch(e => console.error(e));

Supported Auth Backends

AWS IAM auth

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'iam',
        mount: 'aws',                                  // Optional. Vault AWS auth mount point ("aws" by default)
        config: {
            role: 'my_iam_role',
            iam_server_id_header_value: 'https://vault.example.com:8200/', // Optional. X-Vault-AWS-IAM-Server-ID header
            namespace: 'some_namespace',               // Optional. X-Vault-Namespace header
            region: 'eu-central-1',                     // Optional. AWS STS region (see below)
            credentials: {                             // Optional. Resolved from the AWS provider chain when omitted
                accessKeyId: process.env.AWS_ACCESS_KEY_ID,
                secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
            },
        },
    },
});

region

By default the signed GetCallerIdentity request targets the global STS endpoint sts.amazonaws.com and the SigV4 credential scope is bound to us-east-1. Set config.region to sign against a regional STS endpoint instead — the request is then sent to sts.<region>.amazonaws.com and the signature scope is bound to that region. This is required when Vault's sts_region / sts_endpoint is configured for a non-us-east-1 region (e.g. eu-central-1); otherwise STS rejects the replayed request with SignatureDoesNotMatch — Credential should be scoped to a valid region. Omitting region preserves the previous (global-endpoint) behavior.

AppRole auth

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'appRole',
        mount: 'approle',                              // Optional. Vault AppRole auth mount point ("approle" by default)
        config: {
            role_id: '637c065f-c644-5e12-d3d1-e9fa4363af61', // Required. RoleID of the AppRole
            secret_id: '...',                          // Optional. Required when bind_secret_id is enabled
        },
    },
});

Token auth

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'token',
        mount: 'token',                                // Optional. Vault token auth mount point ("token" by default)
        config: {
            token: 's.xxxxxxxxxxxxxxxxxxxxxxxx',       // Required. Vault token
        },
    },
});

Kubernetes auth

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'kubernetes',
        mount: 'kubernetes',                           // Optional. Vault Kubernetes auth mount point ("kubernetes" by default)
        config: {
            role: 'my_k8s_role',                       // Required. Role configured in the Vault Kubernetes auth backend
            tokenPath: '/var/run/secrets/kubernetes.io/serviceaccount/token', // Optional. Defaults to the in-pod service-account token path
        },
    },
});

API

VaultClient

new VaultClient(options)

Client constructor function.

Param Type Default Description
options Object
options.api Object
options.api.url String the url of the vault server
[options.api.apiVersion] String v1
[options.api.requestOptions] Object extra options merged into every HTTP request (see Custom transport)
options.auth Object
options.auth.type String
[options.auth.mount] String Vault auth backend mount point; default varies per method (e.g. "aws" for iam, "approle", "token", "kubernetes")
options.auth.config Object auth configuration variables
[options.auth.config.namespace] String Optional. Vault namespace, sent as the X-Vault-Namespace header on all secret read/list/write requests. Applies to every auth type, not just IAM.
options.logger Object false
Custom transport (proxy / self-signed TLS)

options.api.requestOptions is shallow-merged into every underlying fetch() call, so you can route traffic through a proxy/SOCKS agent or trust a self-signed / internal-CA Vault. Pass an undici dispatcher (request semantics like method and body always win; headers are merged with per-request headers taking precedence):

const { Agent, ProxyAgent } = require('undici');

// Trust an internal/self-signed CA (preferred over disabling verification)
const vaultClient = VaultClient.boot('main', {
    api: {
        url: 'https://vault.internal:8200/',
        requestOptions: {
            dispatcher: new Agent({ connect: { ca: require('fs').readFileSync('/etc/ssl/internal-ca.pem') } }),
        },
    },
    auth: { type: 'token', config: { token: '...' } },
});

// Route through an HTTP proxy / SOCKS agent
const proxied = VaultClient.boot('proxied', {
    api: { url: 'https://vault.example.com:8200/', requestOptions: { dispatcher: new ProxyAgent('http://proxy:8080') } },
    auth: { type: 'token', config: { token: '...' } },
});

For the self-signed-CA case you can also use the process-wide NODE_EXTRA_CA_CERTS=/path/ca.pem env var with no code change. Only disable verification (new Agent({ connect: { rejectUnauthorized: false } })) in throwaway/dev setups — it removes MITM protection.

vaultClient.fillNodeConfig() ⇒ Promise

Populates Vault's values to NPM "config" module

Resolves once the npm config module has been populated from Vault.

Kind: instance method of VaultClient

vaultClient.read(path) ⇒ Promise.<Lease>

Read secret from Vault

Kind: instance method of VaultClient

Param Type Description
path string path to the secret

vaultClient.list(path) ⇒ Promise.<Lease>

Retrieves secrets list

Kind: instance method of VaultClient

Param Type Description
path string path to the secret

vaultClient.write(path, data) ⇒ Promise.<Object>

Writes data to Vault

Resolves to the raw parsed Vault response body, which may be empty/undefined for 204 No Content responses.

Kind: instance method of VaultClient

Param Type Description
path path used to write data
data object data to write

vaultClient.close()

Release resources held by this client.

This client performs lease renewal for renewable auth tokens by arming a background timer. That timer keeps the Node.js event loop alive, so a short-lived script (e.g. a one-off read) never exits on its own. Call close() once you are done with the client to cancel the timer and let the process exit. It is null-safe and safe to call multiple times. The client may still be used afterwards — the next operation that fetches a renewable token will arm a new refresh timer.

const vaultClient = VaultClient.boot('main', { /* ... */ });
const secret = await vaultClient.read('secret/tst');
console.log(secret);
vaultClient.close(); // process can now exit

Kind: instance method of VaultClient

Lease

The object returned by read() and list() (they resolve to Promise<Lease>). Use its accessors to extract the secret data:

  • getValue(key)String — value for a single key. Throws Requested key does not exist when the key is absent.
  • getData()Object — a deep-cloned copy of the whole secret data object.
  • isRenewable()boolean — whether the underlying lease is renewable.

VaultClient.boot(name, [options]) ⇒ VaultClient

Boot an instance of Vault

The instance will be stored in a local hash. Calling Vault.boot multiple times with the same name will return the same instance.

Kind: static method of VaultClient
Returns: VaultClient

Param Type Description
name String Vault instance name
[options] Object options for Vault#constructor.

VaultClient.get(name) ⇒ VaultClient

Get an instance of Vault

The instance will be stored in a local hash. Calling Vault.pop multiple times with the same name will return the same instance.

Kind: static method of VaultClient
Returns: VaultClient

Param Type Description
name String Vault instance name

VaultClient.clear([name])

Clear named Vault instance

If no name passed all named instances will be cleared.

Kind: static method of VaultClient

Param Type Description
[name] String Vault instance name, all instances will be cleared if no name were passed

KV v2 & generic backends

Overview

By default the client behaves exactly as before (KV v1 / raw passthrough). To enable transparent KV v2 support set api.kv.autoDetect: true or supply a static api.engines map. Either option activates path-rewriting and response-unwrapping; callers do not need to know the engine version.

Configuration options

Option Type Default Description
api.kv.autoDetect boolean false Auto-detect the KV version of each mount on first use via GET sys/internal/ui/mounts/<path>.
api.engines Object {} Static mount-to-version map, e.g. { secret: 2, legacy: 1 }. Overrides detection; use this when the token lacks permission on sys/internal/ui/mounts.

Both options can be combined: engines acts as an override — matching mounts skip detection while unmatched mounts are auto-detected (when autoDetect: true).

Auto-detect example

const client = VaultClient.boot('main', {
    api: {
        url: 'https://vault.example.com:8200/',
        kv: { autoDetect: true },
    },
    auth: { type: 'token', config: { token: '...' } },
});

// Works transparently on both KV v1 and KV v2 mounts
const lease = await client.read('secret/my-app/config');
console.log(lease.getData());         // the secret object
console.log(lease.getMetadata());     // KV v2 version metadata (undefined on v1)

Static engines override example

const client = VaultClient.boot('main', {
    api: {
        url: 'https://vault.example.com:8200/',
        engines: { secret: 2, legacy: 1 },
    },
    auth: { type: 'token', config: { token: '...' } },
});

KV v2-specific methods

These methods require a KV v2 mount and throw UnsupportedOperationError on v1 / non-KV mounts.

// Soft-delete specific versions
await client.deleteVersions('secret/foo', [1, 2]);

// Restore soft-deleted versions
await client.undeleteVersions('secret/foo', [1]);

// Permanently destroy versions
await client.destroyVersions('secret/foo', [1, 2]);

// Read version metadata (current_version, versions map, etc.)
const meta = await client.readMetadata('secret/foo');

// Delete all metadata and version history (permanent)
await client.deleteMetadata('secret/foo');

update() — merge-patch

// PATCH a subset of keys without overwriting others (KV v2)
await client.update('secret/foo', { password: 'new-value' });
// Sends PATCH secret/data/foo with Content-Type: application/merge-patch+json

delete()

// Soft-delete the latest version on KV v2; DELETE on v1/passthrough
await client.delete('secret/foo');

request() — raw escape hatch

For any Vault backend that does not benefit from KV path rewriting use request(). It sends the literal path with no rewriting or response normalisation and returns the parsed body directly.

// Encrypt with Transit engine — path must not be rewritten
const result = await client.request('POST', 'transit/encrypt/my-key', {
    plaintext: Buffer.from('hello').toString('base64'),
});
console.log(result.data.ciphertext);

Lease.getMetadata()

getMetadata() is additive — existing code is unaffected.

const lease = await client.read('secret/my-app/db');
lease.getData();     // the secret values
lease.getMetadata(); // { version, created_time, deletion_time, destroyed, custom_metadata }
                     // undefined on KV v1 / passthrough mounts

Path requirements when autoDetect / engines are active

When autoDetect: true or api.engines is set, the client rewrites logical paths to the correct KV v2 API paths automatically (e.g. secret/foosecret/data/foo for reads). Callers must pass logical paths — do not include the internal KV v2 segments (data/, metadata/, delete/, undelete/, destroy/) in the path argument:

// Correct — logical path only
await client.read('secret/my-app/config');

// Wrong — double-rewrite: 'secret/data/foo' becomes 'secret/data/data/foo' on the wire
await client.read('secret/data/foo');

If you need to send a fully-literal Vault API path (e.g. when working with non-KV backends or when you have already constructed the complete path), use request() which bypasses all path rewriting:

// Literal path, no rewriting
await client.request('GET', 'secret/data/foo');

Mount detection caching

  • Each canonical mount is detected at most once per VaultClient instance.
  • Concurrent first-touch requests for the same mount share a single in-flight detection promise.
  • The detection endpoint used is GET sys/internal/ui/mounts/<path> (readable by any authenticated token).
  • When the token lacks permission on that endpoint, set api.engines to skip detection.

Error classes

Class When thrown
UnsupportedOperationError A v2-only method (deleteVersions, undeleteVersions, destroyVersions, readMetadata, deleteMetadata) is called against a non-v2 mount.
VaultError Mount detection fails (e.g. permission denied) and no api.engines override was provided.

Contributing

Contributions are welcome! Please read the contributing guide to get started, and note that this project requires a DCO sign-off on every commit.

Getting help

Not sure where to start? See SUPPORT.md.

Code of Conduct

This project adheres to the Contributor Covenant Code of Conduct.

Security

To report a security vulnerability, please follow our Security Policy.

License

Licensed under the Apache License 2.0.

Packages

 
 
 

Contributors