HashiCorp Vault resolver

Config resolver that swaps vault: placeholders for live values fetched from a HashiCorp Vault KVv2 mount.

Overview

Piko's config loader resolves <prefix>:<value> placeholder strings against any registered resolver. The package handles the vault: prefix. The loader strips the prefix, splits the body into a mount path and a path-within-mount, then calls KVv2(<mount>).Get(ctx, <path>) against the official Vault Go client. It returns a single field when you supply #<key>, or the whole secret as a JSON object. The implementation targets KVv2, the modern Vault KV engine, not KVv1.

The resolver is pure Go with no build tags or CGO. It runs the same way in interpreted dev mode (dev-i) and in compiled builds. The whole resolver is a two-method type. GetPrefix returns vault:, and Resolve performs the lookup. It carries no transport, retry, or breaker logic of its own. The loader layer wraps every resolve call in a circuit breaker, so a transient Vault outage stays bounded. All methods on the resolver are safe for concurrent use, so a single instance can serve the whole config load.

The official Vault client authenticates from the standard environment variables. VAULT_ADDR selects the server. api.NewClient reads VAULT_TOKEN and sets it as the client token. VAULT_NAMESPACE and VAULT_CACERT apply where set. For non-token auth methods (AppRole, Kubernetes, AWS IAM, OIDC), use Vault Agent or your own bootstrap to obtain a token and place it in VAULT_TOKEN before construction. The resolver does not implement auth methods directly.

Requirements

  • A reachable Vault server with a KVv2 secrets engine mounted (default mount is secret).
  • A token in VAULT_TOKEN (or an alternative auth flow that surfaces a token to the standard env vars), and VAULT_ADDR pointing at the server.
  • Vault policy granting read on the secret paths you reference. The KVv2 engine stores data under a data/ API segment, so the policy path is <mount>/data/<path> (for example secret/data/prod/database). That data/ segment belongs in the Vault policy, not in the placeholder body.

Configuration

NewResolver takes no arguments, it relies entirely on the Vault client's default configuration chain.

import "piko.sh/piko/wdk/config/config_resolver_vault"

resolver, err := config_resolver_vault.NewResolver()
if err != nil {
    return err
}

Reference secrets in your config struct using the vault: prefix. The body is <mount>/<logical-path>#<key>. Use the KVv2 logical path without the data/ segment. The client adds data/ for you when it reads the secret. Putting data/ in the body produces a doubled secret/data/data/... path that returns a not-found error.

database:
  password: vault:secret/prod/database#password
stripe:
  secret_key: vault:secret/prod/api-keys#stripe

The placeholder body secret/prod/database reads the Vault path secret/data/prod/database, which is also the path your Vault policy grants read on. Omit #<key> to receive the whole secret as a JSON object. With #<key>, the resolver extracts that one field and renders it as a string with Go %v formatting, so a numeric or boolean field arrives as its text form.

Bootstrap

Wiring Vault in is two steps. Construct a resolver, then pass it to piko.WithConfigResolvers. There is no custom glue, because the resolver implements the two-method config.Resolver interface that WithConfigResolvers accepts directly.

import (
    "piko.sh/piko"
    "piko.sh/piko/wdk/config/config_resolver_vault"
)

resolver, err := config_resolver_vault.NewResolver()
if err != nil {
    return err
}

ssr := piko.New(
    piko.WithConfigResolvers(resolver),
)

The package also exports a Register() shortcut. It constructs a resolver and registers it in the global resolver registry in one call. This is a one-liner for apps that wire resolvers from init().

Tradeoffs

Vault is the most flexible secret broker on this list and also the most operationally heavy. A Vault cluster needs HA, unsealing, audit logs, and a token-rotation story. This resolver reads KVv2 only. It does not touch Vault's dynamic secrets, PKI, or transit engines. If your needs are "fetch a static API key", a cloud secret manager or Kubernetes Secrets is enough. Vault earns its keep when you run it cloud-neutral, gate PKI, or unify secret access across multiple clouds. It also earns its keep with dynamic secrets, that is database credentials issued on demand and revoked on TTL.

See also

Sibling resolvers:

Companion provider:

Framework docs:

External: