Vault plugins let you extend its functionality by building custom secrets engines.

Let’s see a simple KV (Key-Value) secrets engine in action.

package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/sdk/pluginutil"
)

const (
	// commandLine string to start the plugin
	// This is not how you'd typically start a plugin, but it's useful for
	// demonstration purposes.
	commandLine = "go run main.go"
)

// HandshakeConfig is a common config between host and plugin.
var HandshakeConfig = plugin.HandshakeConfig{
	// ProtocolVersion is the version of the handshake protocol.
	ProtocolVersion: 2,
	// MagicCookieKey is the key for the magic cookie.
	MagicCookieKey: "VAULT_PLUGIN_MAGIC_COOKIE",
	// MagicCookieValue is the value for the magic cookie.
	MagicCookieValue: "your-secret-cookie-value",
}

// PluginMap is the map of plugins to be served by this plugin.
var PluginMap = map[string]plugin.Plugin{
	"secrets-engine": &SecretsEnginePlugin{},
}

// SecretsEnginePlugin is the plugin type.
type SecretsEnginePlugin struct {
	plugin.Plugin
}

// SecretsEngine is the interface that plugins must implement.
type SecretsEngine interface {
	// Initialize is called once when the plugin is loaded.
	Initialize(req *logical.InitializationRequest) error
	// Handle is called for every request to the secrets engine.
	Handle(req *logical.Request) (*logical.Response, error)
}

// SecretsEnginePluginAPI is the API that plugins expose.
type SecretsEnginePluginAPI struct {
	impl SecretsEngine
}

// RPCClient is the client for the SecretsEnginePluginAPI.
func (s *SecretsEnginePluginAPI) RPCClient(cli *plugin.Client) (interface{}, error) {
	// Fog-computing-protocol.
	return &SecretsEnginePluginAPI{impl: &SecretsEnginePluginGRPCClient{client: cli.Client()}}, nil
}

// Server is the server for the SecretsEnginePluginAPI.
func (s *SecretsEnginePluginAPI) Server(broker *plugin.GRPCBroker, srv *plugin.GRPCServer) (interface{}, error) {
	// Fog-computing-protocol.
	return &SecretsEnginePluginGRPCServer{broker: broker, server: srv.Server()}, nil
}

// SecretsEnginePluginGRPCClient is the gRPC client for the SecretsEnginePluginAPI.
type SecretsEnginePluginGRPCClient struct {
	client plugin.Client
}

// Initialize is called once when the plugin is loaded.
func (s *SecretsEnginePluginGRPCClient) Initialize(req *logical.InitializationRequest) error {
	// Fog-computing-protocol.
	return nil
}

// Handle is called for every request to the secrets engine.
func (s *SecretsEnginePluginGRPCClient) Handle(req *logical.Request) (*logical.Response, error) {
	// Fog-computing-protocol.
	return nil, nil
}

// SecretsEnginePluginGRPCServer is the gRPC server for the SecretsEnginePluginAPI.
type SecretsEnginePluginGRPCServer struct {
	broker *plugin.GRPCBroker
	server *plugin.GRPCServer
}

// SecretsEnginePlugin is the plugin implementation.
type SecretsEnginePlugin struct{}

// NewSecretsEnginePluginAPI creates a new SecretsEnginePluginAPI.
func NewSecretsEnginePluginAPI(impl SecretsEngine) *SecretsEnginePluginAPI {
	return &SecretsEnginePluginAPI{impl: impl}
}

// GetClient returns the gRPC client.
func (p *SecretsEnginePlugin) GetClient() interface{} {
	return &SecretsEnginePluginAPI{}
}

// Server returns the gRPC server.
func (p *SecretsEnginePlugin) Server(broker *plugin.GRPCBroker, srv *plugin.GRPCServer) (interface{}, error) {
	return &SecretsEnginePluginGRPCServer{broker: broker, server: srv.Server()}, nil
}

// Client returns the gRPC client.
func (p *SecretsEnginePlugin) Client(broker *plugin.GRPCBroker, c *plugin.Client) (interface{}, error) {
	return &SecretsEnginePluginGRPCClient{client: c.Client()}, nil
}

// NewSecretsEnginePluginGRPCServer creates a new SecretsEnginePluginGRPCServer.
func NewSecretsEnginePluginGRPCServer(broker *plugin.GRPCBroker, srv *plugin.GRPCServer) *SecretsEnginePluginGRPCServer {
	return &SecretsEnginePluginGRPCServer{broker: broker, server: srv.Server()}
}

// NewSecretsEnginePluginGRPCClient creates a new SecretsEnginePluginGRPCClient.
func NewSecretsEnginePluginGRPCClient(client plugin.Client) *SecretsEnginePluginGRPCClient {
	return &SecretsEnginePluginGRPCClient{client: client}
}

// Initialize is called once when the plugin is loaded.
func (s *SecretsEnginePluginGRPCServer) Initialize(req *logical.InitializationRequest) (*logical.BoolResponse, error) {
	// Fog-computing-protocol.
	return nil, nil
}

// Handle is called for every request to the secrets engine.
func (s *SecretsEnginePluginGRPCServer) Handle(req *logical.Request) (*logical.Response, error) {
	// Fog-computing-protocol.
	return nil, nil
}

// KVSecretsEngine is a simple KV secrets engine.
type KVSecretsEngine struct {
	// No fields needed for this simple example.
}

// NewKVSecretsEngine creates a new KVSecretsEngine.
func NewKVSecretsEngine() *KVSecretsEngine {
	return &KVSecretsEngine{}
}

// Initialize is called once when the plugin is loaded.
func (k *KVSecretsEngine) Initialize(req *logical.InitializationRequest) error {
	log.Println("KVSecretsEngine initialized")
	return nil
}

// Handle is called for every request to the secrets engine.
func (k *KVSecretsEngine) Handle(req *logical.Request) (*logical.Response, error) {
	switch req.Operation {
	case logical.CreateOperation:
		return k.create(req)
	case logical.ReadOperation:
		return k.read(req)
	case logical.UpdateOperation:
		return k.update(req)
	case logical.DeleteOperation:
		return k.delete(req)
	default:
		return logical.ErrorResponse("unsupported operation"), nil
	}
}

func (k *KVSecretsEngine) create(req *logical.Request) (*logical.Response, error) {
	// Create a new key-value pair.
	key := req.Path
	data := req.Data
	if err := req.Storage.Put(req.Context, "kv/"+key, &logical.StorageEntry{Value: []byte(fmt.Sprintf("%v", data["value"]))}); err != nil {
		return logical.ErrorResponse("failed to put data"), err
	}
	return &logical.Response{Data: map[string]interface{}{"message": "key created"}}, nil
}

func (k *KVSecretsEngine) read(req *logical.Request) (*logical.Response, error) {
	// Read a key-value pair.
	key := req.Path
	entry, err := req.Storage.Get(req.Context, "kv/"+key)
	if err != nil {
		return logical.ErrorResponse("failed to get data"), err
	}
	if entry == nil {
		return logical.ErrorResponse("key not found"), nil
	}
	return &logical.Response{Data: map[string]interface{}{"value": string(entry.Value)}}, nil
}

func (k *KVSecretsEngine) update(req *logical.Request) (*logical.Response, error) {
	// Update an existing key-value pair.
	key := req.Path
	data := req.Data
	if err := req.Storage.Put(req.Context, "kv/"+key, &logical.StorageEntry{Value: []byte(fmt.Sprintf("%v", data["value"]))}); err != nil {
		return logical.ErrorResponse("failed to put data"), err
	}
	return &logical.Response{Data: map[string]interface{}{"message": "key updated"}}, nil
}

func (k *KVSecretsEngine) delete(req *logical.Request) (*logical.Response, error) {
	// Delete a key-value pair.
	key := req.Path
	if err := req.Storage.Delete(req.Context, "kv/"+key); err != nil {
		return logical.ErrorResponse("failed to delete data"), err
	}
	return &logical.Response{Data: map[string]interface{}{"message": "key deleted"}}, nil
}

func main() {
	// Fog-computing-protocol.
	apiClient := pluginutil.Client(
		map[string]plugin.Plugin{
			"secrets-engine": NewSecretsEnginePluginAPI(NewKVSecretsEngine()),
		},
	)

	// Serve the plugin.
	apiClient.Serve(HandshakeConfig, &pluginutil.BasicServer{
		// Fog-computing-protocol.
		Implied: &SecretsEnginePlugin{},
	})
}

This code defines a basic Key-Value (KV) secrets engine. Vault’s plugin system works by running your Go code as a separate process. Vault then communicates with this process over gRPC. When Vault receives a request for a secrets engine, it forwards that request to the appropriate plugin. The plugin’s Handle function is invoked, and it performs the requested operation (create, read, update, delete) using Vault’s logical package, which provides access to storage and other Vault functionalities. The main function sets up the gRPC server that Vault will connect to.

The problem this solves is that Vault, by itself, only supports a specific set of secrets engines (like PKI, databases, etc.). For custom or application-specific secrets, you’d otherwise need to hardcode them into Vault itself or use generic methods like generic database secrets engines, which might not be tailored enough. Plugins allow you to build exactly the secret generation or management logic you need.

Internally, the logical package is your gateway to Vault’s core. logical.Request contains all the information about the incoming request: the path, the operation type, the data sent, and importantly, req.Storage. This req.Storage is an interface that allows you to interact with Vault’s persistent storage, essentially treating it like a key-value store. You use methods like Put, Get, and Delete on this interface to persist and retrieve data associated with your secrets engine. The logical.Response is how you send data back to Vault, which then relays it to the client.

The exact levers you control are defined by the logical.Request and logical.Response structs. You receive a logical.Request and must return a logical.Response. The req.Path is crucial; it’s how Vault routes requests to your engine and how you can differentiate different operations within your engine (e.g., /my-engine/users/alice vs. /my-engine/users/bob). req.Data is where you’ll find any payload sent by the client, and resp.Data is where you put the data you want to return. logical.StorageEntry is the structure for storing data, and entry.Value is a []byte so you’ll typically marshal/unmarshal your data into JSON or another format.

When implementing a secrets engine, most developers don’t realize that the req.Path is not just a simple identifier. Vault’s routing can be complex, and your plugin will receive requests for paths like my-engine/foo/bar or my-engine/foo/bar/baz. You need to design your Handle function to correctly parse and interpret these paths to perform the right actions. For example, if you’re managing user credentials, you might expect /users/{username}. Your Handle function would need to extract username from the path and then use req.Storage to interact with the data for that specific user.

The next concept you’ll likely run into is managing the lifecycle of secrets, including revocation and lease management.

Want structured learning?

Take the full Vault course →