secrets

package module
v0.0.0-...-7493548 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2026 License: MIT Imports: 12 Imported by: 0

README

secrets

A Go library for reading secrets from multiple providers using struct tags.

type Config struct {
    DBHost string              `secret:"awssm://prod/db#host"`
    DBPort int                 `secret:"awssm://prod/db#port"`
    DBPass string              `secret:"awssm://prod/db#password"`
    APIKey string              `secret:"env://API_KEY"`
    Debug  bool                `secret:"debug,optional"`
    Cert   []byte              `secret:"file:///etc/tls/cert.pem"`
    EncKey secrets.Versioned[[]byte] `secret:"awssm://prod/enc-key"`
}

Declare secrets as struct fields, resolve them from any combination of providers, and watch for changes at runtime.

Install

go get github.com/brwse/go-secrets

Usage

r := secrets.NewResolver(
    secrets.WithDefault(awssm.New(awssm.WithRegion("us-west-2"))),
    secrets.WithProvider("env", env.New()),
    secrets.WithProvider("file", file.New()),
)
defer r.Close()

var cfg Config
if err := r.Resolve(ctx, &cfg); err != nil {
    log.Fatal(err)
}

Cloud providers return (*Provider, error) from New():

sm, err := awssm.New(awssm.WithRegion("us-west-2"))
if err != nil {
    log.Fatal(err)
}
r := secrets.NewResolver(secrets.WithDefault(sm))

Tag format

secret:"[scheme://]key[#fragment][,option...]"
Tag Meaning
secret:"db-pass" Bare key, uses default provider
secret:"awssm://prod/db#password" AWS Secrets Manager, extract JSON field
secret:"env://API_KEY" Environment variable
secret:"k8s://prod/db-creds#host" Kubernetes Secret, extract data key
secret:"file:///etc/tls/cert.pem" File contents
secret:"key,optional" Zero value if missing
secret:"key,version=previous" Specific version

Bare keys (no scheme) route to the default provider. Keys with a scheme route to the provider registered for that scheme. The #fragment extracts a field from JSON-encoded secrets. Nested fragments like #db.host are supported.

Supported field types

string, []byte, bool, int/int8-int64, uint/uint8-uint64, float32, float64, time.Duration, pointer variants (*string, etc.), encoding.TextUnmarshaler implementations, Versioned[T], and nested/embedded structs.

Providers

Package Scheme Backend Versioned Default config
secrets/awssm awssm AWS Secrets Manager Yes Standard AWS credential chain
secrets/awsps awsps AWS SSM Parameter Store No Standard AWS credential chain, decrypt: true
secrets/gcpsm gcpsm GCP Secret Manager Yes Application Default Credentials, project from GOOGLE_CLOUD_PROJECT
secrets/azkv azkv Azure Key Vault Yes DefaultAzureCredential, requires WithVaultURL
secrets/vault vault HashiCorp Vault KV v2 Yes VAULT_ADDR/VAULT_TOKEN from env, mount "secret"
secrets/onepassword onepassword 1Password CLI No op CLI auth
secrets/k8s k8s Kubernetes Secrets No Standard kubeconfig chain / in-cluster
secrets/env env Environment variables No
secrets/file file Filesystem No
secrets/literal literal In-memory map Yes For testing

Each provider accepts a WithClient option to inject a custom or pre-configured client implementation.

Key rotation

Use Versioned[T] to fetch both current and previous values. The provider must implement VersionedProvider.

type Config struct {
    EncKey secrets.Versioned[[]byte] `secret:"awssm://prod/enc-key"`
}

// cfg.EncKey.Current  — active key
// cfg.EncKey.Previous — previous key for re-encryption

Use version= to fetch a specific version:

type Config struct {
    OldKey string `secret:"key,version=previous"`
}

Watching for changes

w, err := r.Watch(ctx, &cfg, secrets.WatchInterval(5*time.Minute))
if err != nil {
    log.Fatal(err)
}
defer w.Stop()

go func() {
    for event := range w.Changes() {
        log.Printf("secret %s changed", event.Field)
        w.RLock()
        // read cfg safely
        w.RUnlock()
    }
}()

The watcher polls at the configured interval (default 1 minute), updates only secret-tagged fields under a write lock, and emits ChangeEvent values on the channel. Use w.RLock()/w.RUnlock() when reading the struct from other goroutines.

Validation

if err := r.Validate(&cfg); err != nil {
    log.Fatal(err) // bad tags, unknown schemes, unsupported types
}

Checks tag syntax and provider registration without making any network calls.

Caching

Wrap a provider with NewCachedProvider to avoid redundant API calls. Cached values are held in memory and reused until the TTL expires. This is especially useful for cloud providers where every Resolve() or Watch poll cycle would otherwise hit the network.

sm, _ := awssm.New(awssm.WithRegion("us-west-2"))
r := secrets.NewResolver(
    secrets.WithDefault(secrets.NewCachedProvider(sm, 5*time.Minute)),
    secrets.WithProvider("env", env.New()), // no cache needed
)

Only successful results are cached — errors always pass through. Call Clear() to evict all entries manually. Close() clears the cache and closes the underlying provider if it implements io.Closer.

Parallel fetching

Secrets are fetched concurrently (default parallelism: 10). Multiple fields referencing the same secret URI with different #fragment values result in a single fetch.

r := secrets.NewResolver(
    secrets.WithDefault(sm),
    secrets.WithParallelism(20),
)

Documentation

Overview

Example
// For testing, use literal + env providers.
if err := os.Setenv("EXAMPLE_API_KEY", "sk-test-123"); err != nil {
	log.Fatal(err)
}
defer func() {
	if err := os.Unsetenv("EXAMPLE_API_KEY"); err != nil {
		log.Fatal(err)
	}
}()

r := secrets.NewResolver(
	secrets.WithDefault(literal.New(map[string][]byte{
		"prod/db": []byte(`{"host":"db.example.com","port":5432,"password":"s3cret"}`),
	})),
	secrets.WithProvider("env", env.New()),
	secrets.WithProvider("file", file.New()),
)
defer func() {
	if err := r.Close(); err != nil {
		log.Fatal(err)
	}
}()

type Config struct {
	DBHost string `secret:"prod/db#host"`
	DBPort int    `secret:"prod/db#port"`
	DBPass string `secret:"prod/db#password"`
	APIKey string `secret:"env://EXAMPLE_API_KEY"`
	Debug  bool   `secret:"debug,optional"`
}

var cfg Config
if err := r.Resolve(context.Background(), &cfg); err != nil {
	log.Fatal(err)
}

fmt.Printf("host=%s port=%d key=%s debug=%v\n", cfg.DBHost, cfg.DBPort, cfg.APIKey, cfg.Debug)
Output:
host=db.example.com port=5432 key=sk-test-123 debug=false

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("secret not found")

ErrNotFound indicates the requested secret does not exist. Providers should wrap this error with context:

fmt.Errorf("awssm: secret %q: %w", key, secrets.ErrNotFound)

Functions

This section is empty.

Types

type CachedProvider

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

CachedProvider wraps a Provider with TTL-based caching. Successful results are stored in memory and reused until they expire. This is useful for cloud providers (AWS SM, GCP SM, Vault, etc.) to avoid redundant API calls and potential rate limiting.

CachedProvider is safe for concurrent use.

func NewCachedProvider

func NewCachedProvider(p Provider, ttl time.Duration) *CachedProvider

NewCachedProvider wraps p with a cache that holds results for ttl. Only successful results (err == nil) are cached.

func (*CachedProvider) Clear

func (c *CachedProvider) Clear()

Clear removes all entries from the cache.

func (*CachedProvider) Close

func (c *CachedProvider) Close() error

Close clears the cache and, if the underlying provider implements io.Closer, closes it.

func (*CachedProvider) Get

func (c *CachedProvider) Get(ctx context.Context, key string) ([]byte, error)

Get retrieves the secret for key, returning a cached value if fresh.

func (*CachedProvider) GetVersion

func (c *CachedProvider) GetVersion(ctx context.Context, key, version string) ([]byte, error)

GetVersion retrieves a versioned secret, returning a cached value if fresh. The underlying provider must implement VersionedProvider; otherwise an ErrVersioningNotSupported error is returned.

type ChangeEvent

type ChangeEvent struct {
	// Field is the struct field name (e.g. "EncKey").
	Field string
	// Key is the secret key (e.g. "prod/encryption-key").
	Key string
	// Provider is the provider scheme (e.g. "awssm").
	Provider string
	// OldValue is the previous raw value.
	OldValue []byte
	// NewValue is the new raw value.
	NewValue []byte
}

ChangeEvent is emitted by a Watcher when a secret value changes.

type ErrConversion

type ErrConversion struct {
	Field    string // struct field name
	TypeName string // target Go type name
	Raw      string // the raw string value that failed conversion
	Err      error  // the underlying conversion error
}

ErrConversion indicates that a raw secret value could not be converted to the target field type.

func (*ErrConversion) Error

func (e *ErrConversion) Error() string

func (*ErrConversion) Unwrap

func (e *ErrConversion) Unwrap() error

type ErrNoDefaultProvider

type ErrNoDefaultProvider struct {
	Field string // struct field name
	Key   string // the bare key from the tag
}

ErrNoDefaultProvider indicates a bare key was encountered but no default provider is configured.

func (*ErrNoDefaultProvider) Error

func (e *ErrNoDefaultProvider) Error() string

type ErrUnknownProvider

type ErrUnknownProvider struct {
	Field  string // struct field name
	Scheme string // the URI scheme
	URI    string // the full URI
}

ErrUnknownProvider indicates a URI scheme was not registered with the resolver.

func (*ErrUnknownProvider) Error

func (e *ErrUnknownProvider) Error() string

type ErrUnsupportedType

type ErrUnsupportedType struct {
	Field    string // struct field name
	TypeName string // the unsupported Go type name
}

ErrUnsupportedType indicates that the field type is not supported by the resolver.

func (*ErrUnsupportedType) Error

func (e *ErrUnsupportedType) Error() string

type ErrVersioningNotSupported

type ErrVersioningNotSupported struct {
	Field    string // struct field name
	Provider string // the provider scheme or "default"
}

ErrVersioningNotSupported indicates that a version was requested but the provider does not implement VersionedProvider.

func (*ErrVersioningNotSupported) Error

func (e *ErrVersioningNotSupported) Error() string

type Option

type Option func(*resolverConfig)

Option configures a Resolver.

func WithDefault

func WithDefault(p Provider) Option

WithDefault sets the provider used for bare keys (no URI scheme).

func WithParallelism

func WithParallelism(n int) Option

WithParallelism sets the maximum number of concurrent secret fetches. Defaults to 10. Set to 1 for sequential fetching. n must be >= 1.

func WithProvider

func WithProvider(scheme string, p Provider) Option

WithProvider registers a provider for the given URI scheme.

type Provider

type Provider interface {
	// Get retrieves the raw secret bytes for the given key.
	// Returns ErrNotFound (wrapped) if the key does not exist.
	Get(ctx context.Context, key string) ([]byte, error)
}

Provider retrieves secret values by key. Implementations must be safe for concurrent use.

type Resolver

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

Resolver populates struct fields annotated with `secret` tags from configured providers.

func NewResolver

func NewResolver(opts ...Option) *Resolver

NewResolver creates a Resolver with the given options.

func (*Resolver) Close

func (r *Resolver) Close() error

Close closes all providers that implement io.Closer.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, dst any) error

Resolve walks dst (which must be a non-nil pointer to a struct) and populates fields annotated with `secret` struct tags from the configured providers.

Secrets are fetched concurrently with a configurable parallelism limit. Secrets are deduplicated by URI so the same secret is only fetched once. All errors are collected and returned via errors.Join.

func (*Resolver) Validate

func (r *Resolver) Validate(dst any) error

Validate checks that dst is a valid target for Resolve without contacting any provider. It verifies:

  • dst is a non-nil pointer to a struct
  • all `secret` tags are syntactically valid
  • all referenced schemes have registered providers (or a default exists for bare keys)
  • all field types are supported for conversion

func (*Resolver) Watch

func (r *Resolver) Watch(ctx context.Context, dst any, opts ...WatchOption) (*Watcher, error)

Watch starts a Watcher that periodically re-resolves secrets into dst. It performs an initial Resolve and then polls at the configured interval. The returned Watcher must be stopped via Stop() or context cancellation.

type Versioned

type Versioned[T any] struct {
	Current  T
	Previous T
}

Versioned holds current and previous values for key rotation. When used as a field type, the resolver fetches both versions. Requires the provider to implement VersionedProvider.

type VersionedProvider

type VersionedProvider interface {
	Provider
	GetVersion(ctx context.Context, key, version string) ([]byte, error)
}

VersionedProvider is implemented by providers that support secret versioning. The resolver uses this for Versioned[T] fields and version= tag options.

type WatchOption

type WatchOption func(*watcherConfig)

WatchOption configures a Watcher.

func WatchInterval

func WatchInterval(d time.Duration) WatchOption

WatchInterval sets the polling interval for the Watcher. Defaults to 1 minute.

type Watcher

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

Watcher periodically re-resolves secrets and detects changes. It provides thread-safe read access via RLock/RUnlock.

func (*Watcher) Changes

func (w *Watcher) Changes() <-chan ChangeEvent

Changes returns a channel that receives ChangeEvents when secret values change. The channel is closed when the Watcher is stopped or the context is cancelled.

func (*Watcher) RLock

func (w *Watcher) RLock()

RLock acquires a read lock on the watched struct. Use this before reading the struct to ensure consistency.

func (*Watcher) RUnlock

func (w *Watcher) RUnlock()

RUnlock releases the read lock.

func (*Watcher) Stop

func (w *Watcher) Stop()

Stop stops the Watcher and closes the Changes channel.

Directories

Path Synopsis
Package awsps provides a secret provider that reads from AWS Systems Manager Parameter Store.
Package awsps provides a secret provider that reads from AWS Systems Manager Parameter Store.
Package awssm provides a secret provider that reads from AWS Secrets Manager.
Package awssm provides a secret provider that reads from AWS Secrets Manager.
Package azkv provides a secret provider that reads from Azure Key Vault.
Package azkv provides a secret provider that reads from Azure Key Vault.
Package env provides a secret provider that reads from environment variables.
Package env provides a secret provider that reads from environment variables.
Package file provides a secret provider that reads from filesystem files.
Package file provides a secret provider that reads from filesystem files.
Package gcpsm provides a secret provider that reads from GCP Secret Manager.
Package gcpsm provides a secret provider that reads from GCP Secret Manager.
Package k8s provides a secret provider that reads from Kubernetes Secrets.
Package k8s provides a secret provider that reads from Kubernetes Secrets.
Package literal provides a map-based secret provider for testing.
Package literal provides a map-based secret provider for testing.
Package onepassword provides a secret provider that reads from 1Password using the 1Password CLI (op).
Package onepassword provides a secret provider that reads from 1Password using the 1Password CLI (op).
Package vault provides a secret provider that reads from HashiCorp Vault (KV v2).
Package vault provides a secret provider that reads from HashiCorp Vault (KV v2).

Jump to

Keyboard shortcuts

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