vault

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package vault provides a HashiCorp Vault Transit adapter that implements the kernel/crypto.KeyProvider interface.

Layer

adapters/ layer — depends only on kernel/ and pkg/. Must NOT import runtime/.

Envelope Encryption

This package uses envelope encryption (对标 k8s KMS v2):

  1. Vault Transit `/datakey/plaintext` returns a server-generated 32-byte DEK (HSM-backed in HCP) plus its wrapped EDK in a single round-trip.
  2. The DEK encrypts the plaintext via AES-GCM with the caller-supplied AAD.
  3. The wrapped EDK ("vault:vN:...") is returned alongside the ciphertext for storage; the keyID is parsed from the EDK prefix at encrypt time.
  4. On decrypt the EDK is unwrapped by Vault `/transit/decrypt` and used to decrypt locally. The decrypt endpoint accepts EDKs in the canonical `vault:vN:...` format regardless of whether they were produced by `/datakey/plaintext` (current encrypt path) or `/transit/encrypt` (the previously-used wrap path), so any ciphertext written before the encrypt-path switchover to `/datakey/plaintext` remains decryptable without a migration step.

This pattern eliminates server-side exposure of plaintext and ensures AAD binding is enforced at the local AES-GCM layer (not just as a Vault context hint), fixing the AAD binding bug present in the pre-R1c direct-encrypt path.

References

ref: kubernetes/kubernetes staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go@master ref: hashicorp/vault api-docs/secret/transit POST /transit/datakey/plaintext/:name

Index

Constants

View Source
const ProbeReady healthz.ProbeName = "vault_transit_ready"

ProbeReady is the ops-contract name for the Vault transit readiness probe. healthz.ProbeName-typed, funneled by PROBENAME-SEALED-FUNNEL-01.

Variables

This section is empty.

Functions

func AssertForRealMode

func AssertForRealMode(auth AuthMethod) error

AssertForRealMode returns ErrVaultAuthFailed if auth uses MethodToken. Static tokens are rejected in production (GOCELL_ADAPTER_MODE=real) because they cannot be rotated automatically and carry permanent broad permissions. AppRole and Kubernetes tokens are accepted — they are short-lived and scoped to the application's role.

Call this during construction when realMode is true:

if realMode {
    if err := AssertForRealMode(auth); err != nil {
        return nil, err
    }
}

ref: hashicorp/vault security best practices — avoid long-lived static tokens in prod

func IsErrVaultAuthFailed

func IsErrVaultAuthFailed(err error) bool

IsErrVaultAuthFailed reports whether err (or any error in its chain) carries the ErrVaultAuthFailed code.

Types

type AuthMethod

type AuthMethod interface {
	// Method returns the method identifier for logging and metrics labels.
	Method() Method
	// Login authenticates with Vault and returns the resulting token.
	// On success it also sets the token on the underlying *vaultapi.Client
	// so subsequent VaultClient calls use the fresh token without extra wiring.
	Login(ctx context.Context) (AuthResult, error)
}

AuthMethod is the GoCell abstraction for a Vault authentication strategy. Implementations hold their own credentials and call the appropriate Vault auth endpoint on Login. The TransitKeyProvider stores the AuthMethod so the renewal worker can re-authenticate after a terminal LifetimeWatcher failure.

Implementations must be safe for concurrent calls to Login (the renewal worker may call Login from its goroutine while the main goroutine is idle).

ref: hashicorp/vault api/auth.go#Auth — original SDK interface shape

func NewAppRoleAuth

func NewAppRoleAuth(client *vaultapi.Client, roleID string, secretIDSource SecretIDProvider) (AuthMethod, error)

NewAppRoleAuth creates an AuthMethod that authenticates via Vault AppRole. mountPath defaults to "approle" if empty. secretIDSource is called on each Login to obtain the current secret_id.

NOTE: client must be non-nil; unlike NewStaticTokenAuth, AppRole auth requires a real Vault client for the Login HTTP call. In unit tests, use vaultapi.NewClient(vaultapi.DefaultConfig()) with any address — Login is only called at auth time, not at construction time.

ref: hashicorp/vault api/auth/approle/approle.go#NewAppRoleAuth

func NewAuthMethodFromEnv

func NewAuthMethodFromEnv(ctx context.Context, client *vaultapi.Client) (AuthMethod, error)

NewAuthMethodFromEnv constructs an AuthMethod based on the VAULT_AUTH_METHOD environment variable. The client parameter is used by AppRole and Kubernetes auth to issue the login call and set the resulting token.

ctx is used to bound the wrapping token unwrap call when VAULT_SECRET_ID_TYPE=wrapped (F-3a: unwrap must respect the startup deadline).

Required:

  • VAULT_AUTH_METHOD = token | approle | kubernetes (no default — fail-fast)

Per-method required env:

  • token: VAULT_TOKEN
  • approle: VAULT_ROLE_ID + secret from VAULT_SECRET_ID_TYPE dispatch
  • kubernetes: VAULT_K8S_ROLE (optional: VAULT_K8S_JWT_PATH, VAULT_K8S_MOUNT)

AppRole secret ID types (VAULT_SECRET_ID_TYPE, default: direct):

  • direct: read VAULT_SECRET_ID
  • wrapped: read VAULT_SECRET_ID_WRAPPING_TOKEN, unwrap via Vault sys/unwrap
  • file: read VAULT_SECRET_ID_FILE path, re-read on every Login call

ref: hashicorp/vault api/auth/approle/approle.go — SecretID variants ref: hashicorp/vault api/logical.go#Logical.UnwrapWithContext — wrapping token unwrap

func NewKubernetesAuth

func NewKubernetesAuth(client *vaultapi.Client, role, jwtPath, mountPath string) (AuthMethod, error)

NewKubernetesAuth creates an AuthMethod for Vault Kubernetes auth. jwtPath defaults to the standard K8s projected volume path if empty. mountPath defaults to "kubernetes" if empty.

NOTE: client must be non-nil; unlike NewStaticTokenAuth, Kubernetes auth requires a real Vault client for the Login HTTP call. In unit tests, use vaultapi.NewClient(vaultapi.DefaultConfig()) with any address — Login is only called at auth time, not at construction time.

ref: hashicorp/vault api/auth/kubernetes/kubernetes.go#NewKubernetesAuth

func NewStaticTokenAuth

func NewStaticTokenAuth(client *vaultapi.Client, token string) AuthMethod

NewStaticTokenAuth creates an AuthMethod that presents a pre-configured static token. Pass nil client in unit tests (no real Vault required).

type AuthResult

type AuthResult struct {
	// ClientToken is the short-lived Vault client token to use for subsequent API calls.
	ClientToken string
	// LeaseSeconds is the token TTL in seconds. 0 means no explicit TTL (e.g. root token).
	LeaseSeconds int
	// Renewable indicates whether the token can be renewed via LifetimeWatcher.
	Renewable bool
}

AuthResult is the unified output of AuthMethod.Login. It decouples callers from the Vault SDK's *vaultapi.Secret so fakes and tests need not import it.

ref: hashicorp/vault api/auth/approle/approle.go — Login return type

type Method

type Method string

Method identifies a Vault auth method, used in AssertForRealMode and metrics labels.

const (
	// MethodToken is a static Vault token (VAULT_TOKEN). Only allowed in dev/CI mode.
	MethodToken Method = "token"
	// MethodAppRole uses Vault AppRole auth (role_id + secret_id).
	MethodAppRole Method = "approle"
	// MethodKubernetes uses Vault Kubernetes auth (projected service account JWT).
	MethodKubernetes Method = "kubernetes"
)

type SecretIDProvider

type SecretIDProvider func(ctx context.Context) (string, error)

SecretIDProvider supplies a secret_id on demand. Different sources have different refresh semantics:

  • direct — returns the same value every call (env var / static config)
  • wrapped — returns the unwrapped value from the wrapping token; the wrapping token is single-use, so subsequent calls return the cached value (a new wrapping token requires a restart)
  • file — re-reads the file on every call to pick up orchestrator rotations

ref: external-secrets/external-secrets — resolves credential references at Login time ref: HashiCorp Vault Agent — re-reads AppRole file on each auto-auth cycle

type TokenRenewer

type TokenRenewer interface {
	// LookupSelfToken returns the token's current metadata (TTL, renewable flag)
	// by calling auth/token/lookup-self. Required to seed the LifetimeWatcher
	// with an accurate initial LeaseDuration.
	LookupSelfToken(ctx context.Context) (*vaultapi.Secret, error)
	// NewLifetimeWatcher creates a LifetimeWatcher that automatically renews
	// the token at ~2/3 of its TTL.
	NewLifetimeWatcher(i *vaultapi.LifetimeWatcherInput) (*vaultapi.LifetimeWatcher, error)
}

TokenRenewer is an optional capability for VaultClient implementations that support token lifecycle management. The production vaultAPIClient implements this; test fakes do not (static tokens need no renewal).

Type-assert the VaultClient to TokenRenewer to enable background renewal:

if r, ok := client.(TokenRenewer); ok { ... }

ref: hashicorp/vault api/lifetime_watcher.go@main

type TransitKeyProvider

type TransitKeyProvider struct {
	// contains filtered or unexported fields
}

TransitKeyProvider implements kernel/crypto.KeyProvider using HashiCorp Vault Transit. It is the adapters/-layer replacement for runtime/crypto VaultTransitKeyProvider (R1c layering correction A1).

Envelope model: local AES-GCM with per-value DEK; Vault only wraps the DEK. AAD is bound in the local AEAD layer — NOT sent to Vault — fixing the S1 P0 security bug where AAD was silently ignored by Vault for non-derived keys.

auth is a required AuthMethod. Construction calls auth.Login to obtain the initial token, then optionally starts a background LifetimeWatcher + re-auth loop (when the token is renewable and the client implements TokenRenewer).

Environment variables (standard Vault SDK env vars):

  • VAULT_ADDR: Vault server address
  • VAULT_NAMESPACE: (HCP / Vault Enterprise) namespace; applied via SetNamespace before all I/O. Empty = root namespace.
  • VAULT_AUTH_METHOD: auth method (token|approle|kubernetes) — REQUIRED
  • VAULT_TOKEN: (token method) Vault token
  • VAULT_ROLE_ID: (approle method)
  • VAULT_SECRET_ID: (approle + VAULT_SECRET_ID_TYPE=direct)
  • GOCELL_VAULT_TRANSIT_MOUNT: transit mount path (default: "transit")
  • GOCELL_VAULT_TRANSIT_KEY: key name (default: "gocell-config")

ref: hashicorp/vault builtin/logical/transit/path_rewrap.go@main ref: kubernetes/kubernetes kmsv2/envelope.go@master

func NewTransitKeyProvider

func NewTransitKeyProvider(
	ctx context.Context, client VaultClient, mountPath, keyName string, auth AuthMethod, clk clock.Clock, metrics *TransitMetrics,
) (*TransitKeyProvider, error)

NewTransitKeyProvider creates a TransitKeyProvider with the given VaultClient and AuthMethod. auth is REQUIRED — pass NewStaticTokenAuth(nil, "test-token") in unit tests that do not need a real Vault connection.

ctx governs the initial Login and key existence check. If Vault is unreachable, the call blocks until ctx is canceled (or the call times out). Callers that do not have a deadline should use context.WithTimeout to avoid blocking startup indefinitely. NewTransitKeyProviderFromEnv uses a 30-second timeout by default.

Construction calls auth.Login to acquire the initial token, then performs a fail-fast key existence check (readLatestVersion). The check doubles as the initial cachedLatestVersion seed, so the first Current() call after construction is served lock-free without a Vault round-trip. If the token is renewable and the client implements TokenRenewer, initTokenRenewal configures the background renewal + re-auth worker (not started — call Worker().Start).

Returns an error if auth is nil, Login fails, or the key existence check fails.

func NewTransitKeyProviderFromEnv

func NewTransitKeyProviderFromEnv(realMode bool, clk clock.Clock, metrics *TransitMetrics) (*TransitKeyProvider, error)

NewTransitKeyProviderFromEnv constructs a TransitKeyProvider from environment variables using the real HashiCorp vault/api client.

Required env vars:

  • VAULT_ADDR — Vault server address
  • VAULT_AUTH_METHOD — auth method: token | approle | kubernetes (REQUIRED, no default)

Optional env vars (default values shown):

  • GOCELL_VAULT_TRANSIT_MOUNT (default: "transit")
  • GOCELL_VAULT_TRANSIT_KEY (default: "gocell-config")

When realMode is true, static VAULT_TOKEN is rejected (ErrVaultAuthFailed). Operators must use approle or kubernetes in production.

Ordering constraint — the real-mode guard (AssertForRealMode) runs BEFORE NewTransitKeyProvider so that a rejected token configuration fails fast without spending the Vault round-trip for Login + key metadata read. Do not reorder without understanding the security/perf implication: a misconfig that would be rejected in real mode should never perform Vault I/O.

ref: hashicorp/vault api/client.go@main — DefaultConfig + NewClient ref: hashicorp/vault api/auth/approle/approle.go — AppRole auth

func (*TransitKeyProvider) ByID

ByID returns the KeyHandle identified by keyID. Validates the "vault-transit:" prefix; wrong prefix → ErrKeyProviderKeyNotFound. Lock-free: handle fields are immutable after construction.

func (*TransitKeyProvider) Close

func (p *TransitKeyProvider) Close(ctx context.Context) error

Close stops the token renewal worker (if configured) and satisfies the lifecycle.ManagedResource contract. The underlying VaultClient manages HTTP connection pooling via net/http.Client, which requires no explicit close.

func (*TransitKeyProvider) Current

Current returns the active KeyHandle for encrypting new values. Lock-free: serves cached latest_version when available, falls back to a Vault read on cache miss / invalidation.

func (*TransitKeyProvider) Probes

func (p *TransitKeyProvider) Probes() []healthz.Probe

Probes returns the typed readiness probes for TransitKeyProvider. The single probe ProbeReady ("vault_transit_ready") reads transit/keys/{keyName} metadata (the same path used by readLatestVersion) to verify that:

  • The Vault token is valid and not revoked.
  • The transit mount is enabled.
  • The named key exists.

The probe accepts a ctx from the /readyz handler; it further caps its own wait at transitReadinessTimeout (3 s) so a slow Vault does not hold the /readyz response indefinitely.

This is intentionally NOT sys/health — sys/health only reports whether the Vault process is running and unsealed; it does NOT verify that the transit mount or the specific key are accessible. (vault#28846)

ref: external-secrets/external-secrets pkg/provider/vault — ValidateStore

uses auth/token/lookup-self + business-path probe, not sys/health

func (*TransitKeyProvider) Renewable

func (p *TransitKeyProvider) Renewable() bool

Renewable reports whether the initial auth produced a renewable token. False for MethodToken or for misconfigured AppRole/K8s roles that return non-renewable tokens. When false, the background renewal worker is not started and operators will not be alerted when the token approaches expiry.

In real mode, NewTransitKeyProviderFromEnv rejects a non-renewable result (F-4a): operators must configure the Vault role to issue renewable tokens.

func (*TransitKeyProvider) Rotate

func (p *TransitKeyProvider) Rotate(ctx context.Context) (string, error)

Rotate generates a new key version via Vault Transit rotate API and refreshes the local version cache. Lock-free: Vault rotate is server-side atomic, and the version cache is an atomic.Int64.

Failure semantics:

  • If the rotate POST fails the cache is untouched and ErrKeyProviderRotateFailed is returned; subsequent Current() continues to serve the prior version.
  • If the post-rotate readLatestVersion fails the cache is left at 0 (already invalidated); subsequent Current() will degrade to a Vault read on the next call and refill the cache automatically — no further intervention needed.

ref: hashicorp/vault builtin/logical/transit/path_keys.go@main ref: hashicorp/vault api-docs/secret/transit POST /transit/keys/:name/rotate

func (*TransitKeyProvider) Worker

func (p *TransitKeyProvider) Worker() worker.Worker

Worker returns the token renewal worker when one has been configured, or nil when the VaultClient does not implement TokenRenewer (e.g. test fakes with static tokens). The bootstrap layer skips WithWorkers registration for nil.

ref: kernel/lifecycle.ManagedResource — "Returning nil means no background

goroutine is needed"

type TransitMetrics

type TransitMetrics struct {
	// contains filtered or unexported fields
}

TransitMetrics owns every Prometheus collector exposed by the Vault transit key provider. Construct once per *prom.Registerer (composition-root scope); share the pointer across every TransitKeyProvider built against the same SharedDeps. Replacing the provider does NOT re-register collectors.

Concurrency: all fields are safe for concurrent read/write from worker goroutines (Counter / Gauge / CounterVec are sync; cachedVersion is atomic).

Single-mount invariant: cached_key_version exposes the active provider's cache value with no labels (one process owns one Vault Transit mount + key today). Multi-key support would need a GaugeVec keyed by mount_path / key_name; that's an explicit schema bump, not an implicit migration.

func NewTransitMetrics

func NewTransitMetrics(reg prom.Registerer) (*TransitMetrics, error)

NewTransitMetrics constructs and registers all five vault-transit collectors with reg. Returns a *TransitMetrics for callers to pass into NewTransitKeyProvider / NewTransitKeyProviderFromEnv.

authHealthy starts at 0 (not 1) — the renewal worker transitions it to 1 after a successful initial Start. Non-renewable token deployments never start the worker, so the gauge correctly remains 0 (a true "no healthy renewer" signal, replacing the prior false-green where construction set 1 unconditionally).

Returns an error if any collector fails to register (typically AlreadyRegisteredError when the same metric name was previously registered on reg by another path).

func (*TransitMetrics) LoadCachedVersion

func (m *TransitMetrics) LoadCachedVersion() int64

LoadCachedVersion returns the current value of the cached-version atomic. Provider read sites use this in place of a separate per-instance cache.

func (*TransitMetrics) StoreCachedVersion

func (m *TransitMetrics) StoreCachedVersion(v int64)

StoreCachedVersion writes v to the shared cached-version atomic that the gocell_vault_cached_key_version GaugeFunc reads. Provider write sites for the cache go through this funnel to keep "version cache" and "metric read" as a single physical variable — no double-write to mirror is needed.

type VaultClient

type VaultClient interface {
	// Write sends a PUT/POST to the given Vault path with the provided data
	// and returns the raw secret map or an error.
	Write(ctx context.Context, path string, data map[string]any) (map[string]any, error)
	// Read sends a GET to the given Vault path and returns the data map.
	Read(ctx context.Context, path string) (map[string]any, error)
}

VaultClient is the minimal subset of the Vault SDK client that TransitKeyProvider requires. Using an exported interface allows external packages (e.g. S14a rotation service, integration test helpers) to inject a fake or mock without importing github.com/hashicorp/vault/api directly.

Migrated from runtime/crypto.vaultClient (R1c Phase 0-c); exported in R1c reviewer FID-005 to unblock S14a key-rotation path.

ref: hashicorp/vault builtin/logical/transit/path_rewrap.go@main

func NewVaultAPIClient

func NewVaultAPIClient(c *vaultapi.Client) VaultClient

NewVaultAPIClient wraps the provided *vaultapi.Client in the VaultClient adapter used by TransitKeyProvider. Use this when constructing the provider from a pre-configured *vaultapi.Client (e.g. in tests or when the caller manages Vault authentication separately from NewTransitKeyProviderFromEnv).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL