Does anyone know if there is a good NodeJS library to connect to hashicorp vault from an AWS Lambda using IAM Authentication.
Something similar to HVAC for Python would be good.
I have tried using node-vault-client but there aren't any good examples of IAM Authentication and it doesn't seem to have had an update since 2019 so I am not sure if its actively being maintained.
I managed to get it working using node-vault-client but I had to make changes to the library because it doesnt allow you to pass a namespace header. I have raised a PR that adds a new field to resolve the issue.
Here is a sample of my code:
const VaultClient = require('node-vault-client');
const vaultClient = VaultClient.boot('main', {
api: { url: 'https://my-vault-url.com' },
auth: {
type: 'iam',
config: {
role: 'my-role',
iam_server_id_header_value: 'my-vault-url.com',
namespace: 'my-namespace', // new option added in my pull request
credentials: new AWS.Credentials({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
}),
},
},
});
vaultClient
.read('secrets/data/path/to/secret')
.then((secrets: any) => {
console.log(`MY SECRET IS ${secrets.__data.data['MY_SECRET_KEY']}`);
})
.catch((e: Error) => {
console.error('Error connecting to vault .....');
console.error(e);
});
************ UPDATE **************
I have since decided to write the code to retrieve the secrets from vault myself and remove the dependency on node-vault-client.
First, I need to call AWS Secrets Manager to get the Vault CA Certificate
// aws-secrets-manager.client.ts
import AWS from 'aws-sdk';
import HttpException from '../errors/http.exception';
export default class AWSSecretsManagerClient {
private static readonly client: AWS.SecretsManager = new AWS.SecretsManager({
region: process.env.AWS_REGION,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
static async getSecret(secretName: string): Promise<string> {
console.log(`Reading secret from aws secrets manager`);
try {
const response = await AWSSecretsManagerClient.client.getSecretValue({ SecretId: secretName }).promise();
console.log(`Successfully read secret from aws secrets manager`);
return response.SecretString;
} catch (e) {
console.error(`Failed to read secret from aws secrets manager`);
console.error(e);
throw new HttpException('Internal Server Error', 500);
}
}
}
Then I authenticate with Vault to get an auth token:
// vault-auth.client.ts
import * as https from 'https';
import axios, { AxiosResponse } from 'axios';
import AWS from 'aws-sdk';
import { Request } from 'aws4';
import * as aws4 from 'aws4';
import HttpException from '../errors/http.exception';
import VaultAuthResponse from '../models/vault/vault-auth-response.model';
import VaultAuthRequest from '../models/vault/vault-auth-request.model';
export default class VaultAuthClient {
private readonly GET_CALLER_IDENTITY: string = 'Action=GetCallerIdentity&Version=2011-06-15';
async authenticate(caCert: string): Promise<string> {
console.log(`Attempting to authenticate with vault`);
const axiosConfig = {
timeout: 5000,
httpsAgent: new https.Agent({
ca: caCert,
}),
headers: {
'X-Vault-Namespace': process.env.VAULT_NAMESPACE,
},
};
const url: string = `${process.env.VAULT_URL}/v1/auth/aws/login`;
const request = this.createVaultRequest();
try {
const vaultResponse: AxiosResponse<VaultAuthResponse> = await axios.post(url, request, axiosConfig);
console.log(`Successfully authenticated with vault`);
return vaultResponse.data.auth.client_token;
} catch (e) {
console.error(`Failed to authenticate with vault`);
console.error(e.stack);
throw new HttpException('Internal Server Error', 500);
}
}
private createSTSRequest(): Request {
const credentials: AWS.Credentials = new AWS.Credentials({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
});
const request = aws4.sign(
{
service: 'sts',
method: 'POST',
body: this.GET_CALLER_IDENTITY,
headers: process.env.VAULT_HOST
? {
'X-Vault-AWS-IAM-Server-ID': process.env.VAULT_HOST,
}
: {},
},
credentials,
);
return request;
}
private createVaultRequest() {
const stsRequest: Request = this.createSTSRequest();
const vaultAuthRequest: VaultAuthRequest = new VaultAuthRequest(
stsRequest.method,
encode(JSON.stringify(stsRequest.headers), 'base64'),
encode(this.GET_CALLER_IDENTITY, 'base64'),
encode(`https://${stsRequest.hostname}${stsRequest.path}`, 'base64'),
vault.vaultRole,
);
return vaultAuthRequest;
}
}
Then I call vault to get the Secret:
import * as https from 'https';
import axios from 'axios';
import HttpException from '../errors/http.exception';
export default class VaultClient {
private readonly VAULT_URL: string = `${process.env.VAULT_URL}/v1/secrets/data/${process.env.VAULT_ENV}`;
async getVaultSecrets(token: string, caCert: string, path: string, key: string): Promise<string> {
console.log(`Attempting to read secrets from vault`);
const axiosConfig = {
timeout: 5000,
httpsAgent: new https.Agent({
ca: caCert,
}),
headers: {
'X-Vault-Namespace': process.env.VAULT_NAMESPACE,
'X-Vault-Token': token,
},
};
try {
const url: string = `${this.VAULT_URL}/${path}`;
const {
data: {
data: { data },
},
} = await axios.get(url, axiosConfig);
const secret = data[`${key}`];
console.log(`Successfully read secret from vault`);
return secret;
} catch (e) {
console.error(`Failed to read secret from vault`);
console.error(e.stack);
throw new HttpException('Internal Server Error', 500);
}
}
}
// Get the Vault CA Cert
const caCert: string = await AWSSecretsManagerClient.getSecret(process.env.CA_CERT_SECRET_NAME);
// Get the Vault Auth Token
const token: string = await new VaultAuthClient().authenticate(caCert);
// Get the Secret
const secret: string = await new VaultClient().getVaultSecrets(token, caCert, 'path-to-secret', 'secret-name');