Development of auth plugin for HashiCorp Vault Enterprise
By: Ming Zheng
Tags: Plugin Development, Hashicorp Vault, Golang

Vault Overview

HashiCorp Vault (Vault) can secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API. In general, Vault provides a comprehensive solution for users to manage their secrets. A typical Vault system consists of auth methods, secret engines, storage, auditing device and other system backend. In this article, we are going to demonstrate how to develop a custom auth method and deploy it to be part of the Vault system.

Vault Plugin System

All Vault auth methods and secret engines are considered plugins, which are completely separate, standalone applications that Vault executes and communicates with over RPC.

There are two ways to build a custom plugin - you can either customize existing built-in one, such as Approle, User-Pass auth methods, or build brand new one by following Vault plugin’s development guidelines. In this blog, I will focus on the latter.

Vault Auth Plugin Development

Prerequisite

Assuming you already know how Vault works, to start building an auth plugin there are some additional prerequisites you will need to get going:

Golang programming language. Knowledge about common Vault response attributes, such as “lease_duration”, “policies”, “entity_id” etc. Auth plugin development guideline.

Now we have everything ready. Let’s go through the steps needed to build our Vault auth plugin.

Objective

In this example, we will build a simple custom auth plugin which uses a system environment variable as the auth provider. The plugin can also assign proper policies to the user by querying the entitlement database.

Build Plugin

Project structure

Now it’s time for us to create new directory to nest our Plugin project, following will be project structure we use:

pluginProject

├- auth

│ ├- auth.go

├- main.go

As you can see, we will create two packages inside the project: the main and auth packages.

2. Configure project as plugin

package main



import (

"log"

"os"



"citihub.com/vault/plugin/auth"

"github.com/hashicorp/vault/api"

"github.com/hashicorp/vault/sdk/plugin"

)



//vault auth plugin entry point

func main() {

apiClientMeta := &api.PluginAPIClientMeta{}

flags := apiClientMeta.FlagSet()

flags.Parse(os.Args[1:])

tlsConfig := apiClientMeta.GetTLSConfig()

tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)

if err := plugin.Serve(&plugin.ServeOpts{

BackendFactoryFunc: auth.Factory,

TLSProviderFunc: tlsProviderFunc,

}); err != nil {

log.Fatal(err)

}

}

The first thing we do here is import all necessary package dependencies provided by HashiCorp. Inside the main function, we setup the configuration for plugin, which includes plugin backend functions and TLS handshake function used for communication between the Vault core and the plugin. In next step, we will need to implement functions for auth.Factory .

3. Implement auth backend

There are several methods in Vault backend interface that need to be implemented. To do this we will add following code to auth.go file.

package auth



import (

"context"

"net/http"

"time"



"github.com/hashicorp/vault/sdk/framework"

"github.com/hashicorp/vault/sdk/logical"

)



const (

PATH_NAME = "login"

ID_NAME = "id"

PASSWORD_NAME = "password"

NS_KEY = "X-Vault-Namespace"

NS_NAME = "namespace"

)



//custom auth interface

type auth interface {

GetAuthentication() (bool, error)

GetAuthorization() ([]string, error)

}



func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) {

b := Backend(c)

if err := b.Setup(ctx, c); err != nil {

return nil, err

}

return b, nil

}



type backend struct {

*framework.Backend

}



//config backend

func Backend(c *logical.BackendConfig) *backend {

var b backend

b.Backend = &framework.Backend{

BackendType: logical.TypeCredential,

AuthRenew: b.pathAuthRenew,

PathsSpecial: &logical.Paths{

Unauthenticated: []string{PATH_NAME},

},

Paths: []*framework.Path{

&framework.Path{

Pattern: PATH_NAME,

Fields: map[string]*framework.FieldSchema{

ID_NAME: &framework.FieldSchema{

Type: framework.TypeString,

},

PASSWORD_NAME: &framework.FieldSchema{

Type: framework.TypeString,

},

},

Callbacks: map[logical.Operation]framework.OperationFunc{

logical.UpdateOperation: b.pathAuthLogin,

},

},

},

}

return &b

}

The Backend function is for configuring plugin details. Here are comments for each attribute:

b.Backend = &framework.Backend{

// indicate this plugin is auth plugin

BackendType: logical.TypeCredential,

// implementation of renew functions

AuthRenew: b.pathAuthRenew,

//define auth path, for instance '/login'

PathsSpecial: &logical.Paths{

Unauthenticated: []string{PATH_NAME},

},

//define attributes for login payload, we define two attributes here: 'id' and 'password'

Paths: []*framework.Path{

&framework.Path{

Pattern: PATH_NAME,

Fields: map[string]*framework.FieldSchema{

ID_NAME: &framework.FieldSchema{

Type: framework.TypeString,

},

PASSWORD_NAME: &framework.FieldSchema{

Type: framework.TypeString,

},

},

//callback function

Callbacks: map[logical.Operation]framework.OperationFunc{

logical.UpdateOperation: b.pathAuthLogin,

},

},

},

}

Now we have the backend configured.

We will continue to implement two functions used in above code: b.pathAuthRenew and b.pathAuthLogin .

Add those code to auth.go file:

// raise custom error response

func raiseErrorResponse(msg string, respCode int) (*logical.Response, error) {

errResp := logical.Response{

Data: map[string]interface{}{

"error": msg,

},

}

return logical.RespondWithStatusCode(&errResp, nil, respCode)

}

// implement pathAuthLogin

func (b *backend) pathAuthLogin(_ context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

id := d.Get(ID_NAME).(string)

password := d.Get(PASSWORD_NAME).(string)

nameSpace := ""

ns := req.Headers

if val, hasVal := ns[NS_KEY]; hasVal {

nameSpace = val[0]

}

cauth := New(id, password)

if isAuth, err := cauth.GetAuthentication(); err != nil || !isAuth {

return raiseErrorResponse(err.Error(), http.StatusForbidden)

}

policies, err := cauth.GetAuthorization()

if err != nil {

return raiseErrorResponse(err.Error(), http.StatusForbidden)

}

// Compose the response

return &logical.Response{

Auth: &logical.Auth{

InternalData: map[string]interface{}{

PASSWORD_NAME: cauth.GetPassword(),

},

Policies: policies,

Metadata: map[string]string{

ID_NAME: cauth.GetId(),

NS_NAME: nameSpace,

},

LeaseOptions: logical.LeaseOptions{

TTL: 30 * time.Minute,

MaxTTL: 60 * time.Minute,

Renewable: true,

},

EntityID: id,

},

}, nil

}

//implement pathAuthRenew

func (b *backend) pathAuthRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

if req.Auth == nil {

return nil, EmptyAuth

}

secretValue := req.Auth.InternalData[PASSWORD_NAME].(string)

password := d.Get(PASSWORD_NAME).(string)

if secretValue != password {

return nil, InternalDataNotMatch

}

return framework.LeaseExtend(30*time.Second, 60*time.Minute, b.System())(ctx, req, d)

}

You may notice that in the code, we catch X-Vault-Namespace header. This header is supported only by Vault Enterprise and gives us the ability to implement secure multi-tenancy within Vault, in order to provide isolation and ensure teams can self-manage their own environments. In this example we write the namespace value to be part of token meta information, so users will know which namespace they have authenticated.

pathAuthLogin will return the final response to users. We can custom the response string per our need. In the above example, if login fails we will raise error response with 403 status code. If login succeeds, a standard successful response message will return to users.

Here are explanations for each attribute in the response, check comments below:

logical.Response{

// contains auth info detail

Auth: &logical.Auth{

// internalData used to renew the lease

InternalData: map[string]interface{}{

PASSWORD_NAME: cauth.GetPassword(),

},

//policy names assigned to login token

Policies: policies,

//metadata attached to login token

Metadata: map[string]string{

ID_NAME: cauth.GetId(),

NS_NAME: nameSpace,

},

// lease info

LeaseOptions: logical.LeaseOptions{

// initial time to live

TTL: 30 * time.Minute,

// max time to live

MaxTTL: 60 * time.Minute,

Renewable: true,

},

// entity id connected to this login

EntityID: id,

},

}

As far as now, we have finished building our Vault auth plugin. Now we will need to build the code into binary.

Build plugin project

In this section, we are going to build the project, before doing so, we need to make sure all Vault library dependencies are installed in our development environment. Run the following commands to install them and build our plugin:

# cd to your project

$ cd /path/to/project

# init module path for your project

$ go mod init citihub.com/vault/plugin

#download all dependencies

$ go mod vendor

#build plugin 'ch' binary and save under 'plugins/' folder

$ go build -o plugins/ch

At this point, our plugin binary ready to test. In next section, I will show you how to register the plugin with the Vault plugin catalog, enable it to a specific path and then login.

Deploy and test the plugin

To test the plugin binary, we need a running Vault server. You can download latest Vault binary from Here . Save it to your $GOPATH/bin directory.

Run the command below in the console to start the Vault server:

#setup test user's profile, for linux and Mac OS

$ export vaultUserId=tester1

$ export vaultPassword=123123

#start vault in dev mode

$ vault server -dev -dev-root-token-id=root -dev-plugin-dir=/path/to/project/plugins

You will see Vault status in the console. Vault will stay active if there is no error during startup. You can also try to open: http://127.0.0.1:8200 in your browser to visit the Vault web UI.

Now, open another console and try following commands to register and enable our auth plugin:

$ export VAULT_ADDR=http://127.0.0.1:8200

#make sure you have execute permission

$ chmod u+x /path/to/project/plugins/ch

#calculate shasum 256 vaule

$ SHASUM=$(shasum -a 256 /path/to/project/plugins/ch | cut -d " " -f1)

#register to vault plugin catalog

$ vault write sys/plugins/catalog/ch sha_256=$SHASUM command="ch"

#enable vault plugin at the path 'auth/ch/login', for vault enterprise we can white list the vault namespace header

$ vault auth enable -passthrough-request-headers=X-Vault-Namespace -path=ch -plugin-name=ch plugin

You will see a success message if no error occurs during enabling the plugin.

Now we can test login using following command:

$ vault write auth/ch/login id=tester1 password=123123

If login is successful Vault will return the access token with policies, meta, lease duration info attached.

Conclusion

In this article we demonstrated how to build a simple custom Vault auth method which supports features in Hashicorp Vault Enterprise. For authentication we used a simple environment variable for demonstration purposes — in a real-world scenario your organization with have a much more complex auth provider to integrate with, such as oAuth2 or database backed auth provider and this example can be modified accordingly.

Lastly, feel free to give feedback if you see errors or something doesn’t make sense to you. Happy coding!

Useful Reference

https://www.vaultproject.io/docs/internals/plugins/

https://learn.hashicorp.com/vault/developer/plugin-backends

https://www.hashicorp.com/blog/building-a-vault-secure-plugin/

https://github.com/hashicorp/vault

https://github.com/hashicorp/vault-auth-plugin-example