javascriptnode.jsamazon-web-servicesaws-lambdahashicorp-vault

How to call Hashicorp Vault from AWS Lambda in NodeJS with IAM Authentication


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.


Solution

  • 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');