Development of auth plugin for HashiCorp Vault Enterprise

Ming Zheng
Apr 1, 2020 · 7 min read

Recently, I helped an organization develop a HashiCorp Vault (Vault) auth plugin. During development I found the documentation around Vault auth plugin for its Enterprise version to be insufficient in places, so here I document an example using the things I discovered during development.

Vault Overview

Vault Plugin System

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

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

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

Objective

Build Plugin

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

# 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

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

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

Useful Reference

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

Citihub Digital, a Synechron Company

Recording the digital DNA of financial services.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store