Skip to main content

Vault Event Notifications

·1149 words·6 mins
Vault Hashicorp Hcp

Vault Enterprise (or HCP Vault Dedicated) support event notifications.

Currently, there are two major classes of events:

  • Database plugin events
  • Key/value plugin events (both version 1 and 2 of the key/value secrets engine)

Each event class has a number of event types. As an example, for the kv-v2 secrets engine there are events for:

  • kv-v2/config-write
  • kv-v2/data-delete
  • kv-v2/data-patch
  • kv-v2/data-write
  • kv-v2/delete
  • kv-v2/destroy
  • kv-v2/metadata-delete
  • kv-v2/metadata-patch
  • kv-v2/metadata-write
  • kv-v2/undelete

Basically there are events for everything related to key/value secrets.

These events allow you to build applications that react to changes to key/value secrets and database secrets in close to realtime.

In the following sections I will briefly outline what a Go application that does this looks like.

Prerequisites
#

To follow along with this example, you need access to a Vault Enterprise cluster or a HCP Vault Dedicated cluster. I will be using a HCP Vault Dedicated cluster and the admin namespace.

$ export VAULT_NAMESPACE=admin
$ export VAULT_ADDR=vault-cluster-public-vault-21cb89ac.41a8a26e.z1.hashicorp.cloud:8200

Note that I left https:// out from the VAULT_ADDR environment variable, the reason for this will be clear later.

You need to configure the AppRole auth method. Below follows a sample configuration:

$ vault auth enable approle
$ vault write auth/approle/role/app \
    token_type=service \
    token_ttl=24h \
    secret_id_ttl=24h \
    token_max_ttl=24h \
    secret_id_num_uses=0 \
    token_policies=app
$ export VAULT_APPROLE_ROLE_ID=$(vault read -field=role_id \
    auth/approle/role/app/role-id)
$ export VAULT_APPROLE_SECRET_ID=$(vault write -f -field=secret_id \
    auth/approle/role/app/secret-id)

The role was configured with the app policy, so let’s create it. Store the following policy document in app.hcl:

path "sys/events/subscribe/kv-v2/data-write" {
  capabilities = ["read"]
}

path "secret/data/app-data" {
  capabilities          = ["list", "subscribe"]
  subscribe_event_types = ["*"]
}

path "secret/data/app-data" {
  capabilities = ["read"]
}

This policy allows the app to set up subscriptions to kv-v2/data-write events and also to read the secret stored at secret/data/app-data. Create the policy:

$ vault policy write app app.hcl

Next we enable version 2 of the key/value secrets engine and add a secret:

$ vault secrets enable -version=2 -path=secret kv
$ vault kv put -mount=secret app-data foo=bar

Subscribe to events using a Go application
#

This section will break down the pieces of the Go application.

First of all, import a number of packages that we will need. The most important ones are for Vault and the Gorilla websocket package that we use to listen for events:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/gorilla/websocket"
	vault "github.com/hashicorp/vault/api"
	auth "github.com/hashicorp/vault/api/auth/approle"
)

Next we set up some global variables for the Vault address, namespace and the secret path:

var (
	vaultAddr      = os.Getenv("VAULT_ADDR")
	vaultNamespace = os.Getenv("VAULT_NAMESPACE")
	secretPath     = "secret/data/app-data"
)

The authenticateVault function handles authentication to Vault using the AppRole auth method. This basically means it uses the role ID and secret ID we have stored as environment variables to authenticate to Vault and obtain a Vault token:

func authenticateVault(ctx context.Context, client *vault.Client) error {
	appRoleAuth, err := auth.NewAppRoleAuth(
		os.Getenv("VAULT_APPROLE_ROLE_ID"),
		&auth.SecretID{
			FromEnv: "VAULT_APPROLE_SECRET_ID",
		},
	)
	if err != nil {
		return fmt.Errorf("unable to initialize AppRole auth method: %w", err)
	}

	authInfo, err := client.Auth().Login(ctx, appRoleAuth)
	if err != nil {
		return fmt.Errorf("unable to login to AppRole auth method: %w", err)
	}

	if authInfo == nil {
		return fmt.Errorf("no auth info was returned after login")
	}

	return nil
}

The fetchSecret function reaches out to Vault to fetch the key/value secret it has permissions to read. It will do this when the program starts up, and every time there is a new update event arriving.

func fetchSecret(ctx context.Context, client *vault.Client) (map[string]interface{}, error) {
	secret, err := client.Logical().ReadWithContext(ctx, secretPath)
	if err != nil {
		return nil, err
	}

	if secret == nil || secret.Data == nil {
		return nil, fmt.Errorf("secret not found")
	}

	data, ok := secret.Data["data"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("malformed secret data")
	}

	return data, nil
}

The watchSecretUpdates sets up a websocket connection to Vault to listen for kv-v2/data-write events. This application only has access to events for this single specific secret it has read access to, so this demo application does not bother to read the details of the event. Note that the event itself does not contain any secret data, so the app much read the secret after it has been updated.

Note that you must explicitly provide the Vault token and namespace headers for the connection.

When the secret has been fetched from Vault the new value is printed to the terminal.

func watchSecretUpdates(ctx context.Context, client *vault.Client) {
	headers := http.Header{
		"X-Vault-Token":     []string{client.Token()},
		"X-Vault-Namespace": []string{vaultNamespace},
	}

	url := "wss://" + vaultAddr + "/v1/sys/events/subscribe/kv-v2/data-write?json=true"

	conn, res, err := websocket.DefaultDialer.DialContext(ctx, url, headers)
	if err != nil {
		if res != nil && res.Body != nil {
			defer res.Body.Close()
			body, _ := io.ReadAll(res.Body)
			fmt.Printf("Response body: %s\n", string(body))
		}
		log.Fatalf("Websocket connection failed: %v", err)
	}
	defer conn.Close()

	log.Println("Listening for secret update events...")

	for {
		_, _, err := conn.ReadMessage()
		if err != nil {
			log.Printf("Websocket read error: %v", err)
			continue
		}

		log.Println("Detected secret update event. Fetching new value...")
		data, err := fetchSecret(ctx, client)
		if err != nil {
			log.Printf("Error fetching secret: %v", err)
			continue
		}
		log.Printf("Updated secret: %v", data)
	}
}

Finally, the main function creates the Vault client, authenticates it, fetches the initial value of the secret, and then starts watching for new events from Vault.

We see that I add the https:// part to the address here. This is because I reused the same address value here and when I set up the websocket connection (which starts with wss://).

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// create a new Vault client
	client, err := vault.NewClient(&vault.Config{Address: "https://" + vaultAddr})
	if err != nil {
		log.Fatalf("Error creating Vault client: %v", err)
	}

	// authenticate the Vault client using AppRole
	if err := authenticateVault(ctx, client); err != nil {
		log.Fatalf("Vault authentication failed: %v", err)
	}
	log.Println("Authenticated to Vault successfully")

	// fetch the initial value of the secret
	secret, err := fetchSecret(ctx, client)
	if err != nil {
		log.Fatalf("Error fetching initial secret: %v", err)
	}
	log.Printf("Initial secret fetched: %v", secret)

	// watch for secret updates
	watchSecretUpdates(context.Background(), client)
}

With all the code in a main.go file, initialize a go project and run the application:

$ go mod init example
$ go mod tidy
$ go run main.go
2025/03/10 18:05:41 Authenticated to Vault successfully
2025/03/10 18:05:41 Initial secret fetched: map[foo:bar]
2025/03/10 18:05:41 Listening for secret update events...

When the value of the secret is updated the following happens:

$ go run main.go
2025/03/10 18:05:41 Authenticated to Vault successfully
2025/03/10 18:05:41 Initial secret fetched: map[foo:bar]
2025/03/10 18:05:41 Listening for secret update events...
...
2025/03/10 18:07:06 Detected secret update event. Fetching new value...
2025/03/10 18:07:06 Updated secret: map[foo:hello world]

Key takeaways
#

You can build applications that use key/value secrets or database secrets and take actions whenever these are updated. An example of this is if you need to perform some actions on a custom system that is difficult to do with any other means, then you can listen for relevant updates from Vault and then take actions based on these.

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect · Author · HashiCorp Ambassador