libvault v0.2.0 Vault

The main module for configuring and interacting with HashiCorp’s Vault.

When possible, it tries to emulate the CLI, with read, write, list and delete methods. An additional request method is provided when you need further flexibility.

API Preview

vault =
  Vault.new([
    engine: Vault.Engine.KVV2, 
    auth: Vault.Auth.UserPass 
  ])
  |> Vault.auth(%{username: "username", password: "password"})

{:ok, db_pass} = Vault.read(vault, "secret/path/to/password")
{:ok, %{"version" => 1 }} = Vault.write(vault, "secret/path/to/creds", %{secret: "secrets!"})

Flexibility

Hashicorp’s Vault is highly configurable. Rather than cover every possible option, this library strives to be flexible and adaptable. Auth backends, Secret Engines, and Http clients, and JSON parsers are all replacable, and each behaviour asks for a minimal behaviour contract.

HTTP Adapters

The following HTTP Adapters are provided:

  • Tesla with Vault.HTTP.Tesla

    • Can be configured to use :hackney, :ibrowse, or :httpc (and in turn, plays nice with HTTPoison, and HTTPotion)

JSON Adapters

Most JSON libraries provide the same methods, so no default adapter is needed. You can use Jason, JSX, Poison, or whatever encoder you want.

Defaults to Jason if present, falling back to Poison if present, ultimately falling back to nil.

See Vault.JSON.Adapter for the full behaviour interface.

Auth Adapters

Adapters have been provided for the following auth backends:

In addition to the above, a generic backend is also provided (Vault.Auth.Generic). If support for auth provider is missing, you can still get up and running quickly, without writing a new adapter.

Secret Engines

Most of Vault’s Secret Engines use a replacable API. The Vault.Engine.Generic adapter should handle most use cases for secret fetching. This is also the default value.

Vault’s KV version 2 broke away from the standard REST convention. So KV has been given its own adapter:

Additional request methods

The core library only handles the basics around secret fetching. If you need to access additional API endpoints, this library also provides a Vault.request method. This should allow you to tap into the complete vault REST API, while still benefiting from token control, JSON parsing, and other HTTP client nicities.

Setup and Usage

First, ensure that adapter dependancies are part of your dependancies and applications:

def application do
  [
    extra_applications: [:logger, :tesla, :hackney,] # or :ibrowse
    mod: {MyApp.Application, []}
  ]
end

def deps do
  [
    # tesla, required for Vault.HTTP.Tesla
    {:tesla, "~> 1.0.0", },

    # pick your HTTP client - ibrowse, hackney, or if you're feeling bold, httpc.
    {:ibrowse, "~> 4.4.0", },
    {:hackney, "~> 1.6", },

    # Pick your json parser
    {:jason, ">= 1.0.0", },
    {:poison, "~> 3.0", },
  ]
    
end

Example usage:

vault = 
  Vault.new([
    engine: Vault.Engine.KVV2,
    auth: Vault.Auth.UserPass, 
    credentials: %{username: "username", password: "password"}
  ]) 
  |> Vault.auth()

{:ok, db_pass} = Vault.read(vault, "secret/path/to/password")
{:ok, aws_creds} = Vault.read(vault, "secret/path/to/creds")

You can configure the client up front, or change configuration dynamically.

  vault = 
    Vault.new() # use defaults, or application configuration
    |> Vault.set_auth(Vault.Auth.Approle)
    |> Vault.auth(%{role_id: "role_id", secret_id: "secret_id"})

  {:ok, db_pass} = Vault.read(vault, "secret/path/to/password")

  vault = Vault.set_engine(Vault.Engine.KVV2) // switch to versioned secrets

  {:ok, db_pass} = Vault.write(vault, "kv/path/to/password", %{ password: "db_pass" })

Link to this section Summary

Functions

Authenticate against the configured auth backend

Delete a secret from the configured secret engine

List secret keys available at a certain path

Create a new client. Optionally provide a keyword list or map of options for configuration

Read a secret from the configured secret engine

Make an HTTP request against your vault instance, with the current vault token

Set the backend to use for authenticating the client

Set the path used when logging in with your auth adapter

Sets the login credentials for this client

Set the secret engine for the client

Set the host of your vault instance

Set the http module used to make API calls

Check if the current token is still valid

Get a NaiveDateTime struct, in UTC, for when the current token will expire

Write a secret to the configured secret engine

Link to this section Types

Link to this type auth()
auth() :: Vault.Auth.Adapter.t() | nil
Link to this type auth_path()
auth_path() :: String.t()
Link to this type credentials()
credentials() :: map()
Link to this type engine()
engine() :: Vault.Engine.Adapter.t() | nil
Link to this type host()
host() :: String.t()
Link to this type http()
http() :: Vault.HTTP.Adapter.t() | nil
Link to this type json()
json() :: Vault.Json.Adapter.t() | nil
Link to this type method()
method() :: :get | :put | :post | :patch | :head | :delete
Link to this type options()
options() :: map() | Keyword.t()
Link to this type t()
t() :: %Vault{
  auth: auth(),
  auth_path: auth_path(),
  credentials: credentials(),
  engine: engine(),
  host: host(),
  http: http(),
  http_options: term(),
  json: json(),
  token: token(),
  token_expires_at: token_expires_at()
}
Link to this type token()
token() :: String.t() | nil
Link to this type token_expires_at()
token_expires_at() :: NaiveDateTime.t()

Link to this section Functions

Link to this function auth(vault, params \\ %{})
auth(t(), map()) :: {:ok, t()} | {:error, [term()]}

Authenticate against the configured auth backend.

Examples

A successful authentication returns a client containing a valid token, as well as the expiration time for the token. Perform this operation before reading or writing secrets.

Errors from vault are returned as a list of strings.

Uses pre-configured credentials if provided. Passed in credentials will override existing credentials.

{:ok, vault} = Vault.set_credentials(vault, %{username: "UserN4me", password: "P@55w0rd"})

{:error, ["Missing Credentials, username and password are required"]} = Vault.set_credentials(vault, %{username: "whoops"})
Link to this function delete(vault, path, options \\ [])
delete(t(), String.t(), list()) :: {:ok, map()} | {:error, term()}

Delete a secret from the configured secret engine.

Examples

Returns the response from vault, which is typically an empty map. See Secret Engine Adapter options for further configuration.

{:ok, %{} }} = Vault.delete(vault,"secret/path/to/write")

{:error, ["Key not found"]} = Vault.list(vault,"secret/bad/path/")
Link to this function list(vault, path, options \\ [])
list(t(), String.t(), list()) :: {:ok, map()} | {:error, term()}

List secret keys available at a certain path.

Examples

Path should end with a trailing slash. Provided adapters returns the values on the data key from vault, if present. See Secret Engine adapter details for additional configuration, such as returning the full response.

Errors from vault are returned as a list of strings.

{:ok, %{ "keys" => ["some/", "paths", "returned"] }} = Vault.list(vault,"secret/path/to/write")

{:error, ["Unauthorized"]} = Vault.list(vault,"secret/bad/path/")
Link to this function new(params \\ %{})
new(options()) :: t()

Create a new client. Optionally provide a keyword list or map of options for configuration.

Examples

Return a default Vault client:

vault = Vault.new()

Return a fully initialized Vault Client:

  vault = Vault.new(%{
    http: Vault.HTTP.Tesla,
    host: myvault.instance.com,
    auth: Vault.Auth.JWT,
    auth_path: 'jwt',
    engine: Vault.Engine.Generic,
    token: "abc123",
    token_expires_at: NaiveDateTime.utc_now() |> NaiveDateTime.add(30, :second),
    credentials: %{role_id: "dev-role", jwt: "averylongstringoflettersandnumbers..."}
  })

Options

The following options can be provided as part of the :vault application config, or as a Keyword List or Map of options. Runtime configuration will always take precedence.

  • :auth - Module for your Auth adapter.
  • :auth_path - Path to use for your auth adapter. Provided adapters have their own default paths. Check your adapter for details.
  • :engine Module for your Secret Engine adapter. Defaults to Vault.Engine.Generic.
  • :host - host of your vault instance. Should contain the port, if needed. Should not contain a trailing slash. Defaults to System.get_env("VAULT_ADDR").
  • :http - Module for your http adapter. Defaults to Vault.HTTP.Tesla when :tesla is present.
  • :http_options - A keyword list of options to your HTTP adapter.
  • :token - A vault token.
  • :token_expires_at A NaiveDateTime instance that represents when the token expires, in utc.
  • :credentials - The credentials to use when authenticating with your Auth adapter.
Link to this function read(vault, path, options \\ [])
read(t(), String.t(), list()) :: {:ok, map()} | {:error, term()}

Read a secret from the configured secret engine.

Examples

Provided adapters return the values on the data key from vault, if present. See Secret Engine adapter details for additional configuration, such as returning the full response.

Errors from vault are returned as a list of strings.

{:ok, %{ password: "value" }} = Vault.write(vault,"secret/path/to/read")

{:error, ["Unauthorized"]} = Vault.read(vault,"secret/bad/path")
Link to this function request(vault, method, path, options \\ [])
request(t(), method(), String.t(), list()) ::
  {:ok, term()} | {:error, list()}

Make an HTTP request against your vault instance, with the current vault token.

Examples

This library doesn’t cover every vault API, but this can help fill some of the gaps, and removing some boilerplate around token management, and JSON parsing.

It can also be handy for renewing dynamic secrets, if you’re using the AWS Secret backend.

Requests can take the following options a Keyword List.

options:

  • :query_params - a keyword list of query params for the request. Do not include query params on the path.
  • :body - Map. The body for the request
  • :headers - Keyword list. The headers for the request
  • :version - String. The vault api version - defaults to “v1”

General Example

Here’s a genneric example for making a request:

vault = Vault.new(
  http: Vault.HTTP.Tesla, 
  host: "http://localhost", 
  token: "token"
  token_expires_in: NaiveDateTime.utc_now()
)

Vault.request(vault, :post, "path/to/call", [ body: %{ "foo" => "bar"}])
# POST to http://localhost/v1/path/to/call
# with headers: {"X-Vault-Token", "token"}
# and a JSON payload of: "{ 'foo': 'bar'}"

AWS lease renewal

A quick example of renewing a lease.

vault = Vault.new(
  http: Vault.HTTP.Tesla, 
  host: "http://localhost", 
  token: "token"
  token_expires_in: NaiveDateTime.utc_now()
)

body = %{lease_id: lease, increment: increment}
{:ok, response} = Vault.request(vault, request(:put, "sys/leases/renew", [body: body])
Link to this function set_auth(vault, auth)
set_auth(t(), auth()) :: t()

Set the backend to use for authenticating the client.

Examples

The auth backend should be a module that meets the Vault.Auth.Adapter behaviour.

vault = Vault.set_auth(vault, Vault.Auth.Approle)
Link to this function set_auth_path(vault, auth_path)
set_auth_path(t(), auth_path()) :: t()

Set the path used when logging in with your auth adapter.

Examples

Auth backends can be mounted at any path on /auth/. If left unset, the auth adapter may provide a default, eg userpass. See your Auth adapter for details.

vault = Vault.set_auth_path(vault, "auth-path")
Link to this function set_credentials(vault, creds)
set_credentials(t(), map()) :: t()

Sets the login credentials for this client.

Examples

vault = Vault.set_credentials(vault, %{username: "UserN4me", password: "P@55w0rd"})
Link to this function set_engine(vault, engine)
set_engine(t(), engine()) :: t()

Set the secret engine for the client.

Examples

The secret engine should be a module that meets the Vault.Engine.Adapter behaviour.

vault = Vault.set_engine(vault, Vault.Engine.KVV2)
Link to this function set_host(vault, host)
set_host(t(), host()) :: t()

Set the host of your vault instance.

Examples

The host can be fetched from anywhere, as long as it’s a string.

vault = Vault.set_host(vault, System.get_env("VAULT_ADDR"))

The port should be provided if needed, along with the protocol.

vault = Vault.set_host(vault, "https://my-vault.host.com:12345")
Link to this function set_http(vault, http)
set_http(t(), http()) :: t()

Set the http module used to make API calls.

Examples

Should be a module that meets the Vault.HTTP.Adapter behaviour.

vault = Vault.set_http(vault, Vault.HTTP.Tesla)
Link to this function token_expired?(vault)
token_expired?(t()) :: true | false

Check if the current token is still valid.

Examples

Returns true if the current time is later than the expiration date, otherwise false.

true = Vault.token_expired?(vault)
Link to this function token_expires_at(client)

Get a NaiveDateTime struct, in UTC, for when the current token will expire.

Examples

Expiration time is generated from the current time on the current server.

~N[2018-11-25 16:30:30.177731] = Vault.token_expires_at(vault)
Link to this function write(vault, path, value, options \\ [])
write(t(), String.t(), term(), list()) :: {:ok, map()} | {:error, term()}

Write a secret to the configured secret engine.

Examples

Provided adapters returns the values on the data key from vault, if present. See Secret Engine adapter details for additional configuration, such as returning the full response.

Errors from vault are returned as a list of strings.

{:ok, %{ version: 1 }} = Vault.write(vault,"secret/path/to/write", %{ secret: "value"})

{:error, ["Unauthorized"]} = Vault.write(vault,"secret/bad/path", %{ secret: "value"})