secrets

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package secrets resolves secrets by logical path, independently of where they are stored.

Consumers ask for a path such as "service-keys/billing" through the Store interface; the backend — environment variables, Vault, or Kubernetes Secrets — is chosen at startup from SECRETS_BACKEND. Swapping backends is a deployment decision, not a code change, and the env backend behaves exactly like reading the variable directly, so a service can start there and move later.

Index

Constants

View Source
const (
	BackendEnv        = "env"
	BackendVault      = "vault"
	BackendKubernetes = "kubernetes"
)

Well-known backend names accepted in SECRETS_BACKEND.

View Source
const (
	KMSMasterKeyPath    = "kms/master-key"
	EventSigningKeyPath = "kms/event-signing-key"
	JWTSecretPath       = "platform/jwt-secret"
)

Well-known logical paths for the key material an infrastructure deployment usually shares. They are conventions, not requirements: any string is a valid path, and a deployment that names things differently just passes its own.

A seeder should adopt these values from the environment rather than generate them. Generating a master key would orphan everything already encrypted under the real one.

View Source
const DefaultCacheTTL = 60 * time.Second

DefaultCacheTTL bounds how long a consumer keeps using a rotated-out secret. It must stay below the IAM rotation grace window (10 minutes).

Variables

View Source
var DefaultManagedByLabel = "secrets-seeder"

KubernetesStore reads and writes Kubernetes Secrets through the API server using the pod's ServiceAccount - plain HTTP like the Vault backend, so the vendored service modules stay free of client-go's dependency tree.

Logical paths map onto Secret names and data keys like this:

service-keys/<svc> -> Secret "svc-key-<svc>", key "api-key"
kms/<material>     -> Secret "kms-material",  key "<material>"
anything else      -> Secret "<path with / -> ->" , key "value"

RBAC gives each service `get` on only its own secret and the seeder write access, so a compromised consumer can read nothing but its own key. DefaultManagedByLabel is stamped as app.kubernetes.io/managed-by on Secrets this store creates. Deployments that want their own seeder identity on the label set SECRETS_MANAGED_BY.

View Source
var DefaultVaultPrefix = "app"

DefaultVaultPrefix is the path prefix used when none is configured. Deployments that keep their secrets under a different tree set SECRETS_VAULT_PREFIX or pass the prefix to NewVaultStore.

View Source
var ErrAccessDenied = errors.New("access to secret denied")

ErrAccessDenied is returned when the backend refuses access (401/403). Under least-privilege RBAC this is an EXPECTED outcome for material a service is not entitled to (e.g. only IAM may read platform/jwt-secret), so callers can fall back quietly instead of treating it as breakage. It is terminal for Retrying - more attempts cannot change authorization.

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

ErrNotFound is returned when a secret does not exist at the given path. Chain treats it as "try the next backend"; any other error is terminal.

View Source
var ErrReadOnly = errors.New("secret store is read-only")

ErrReadOnly is returned by Set on backends that cannot persist secrets (the env backend). The IAM seeder uses this to switch to adoption mode: read the pre-provided plaintext and derive the DB hash from it.

View Source
var ErrUnavailable = errors.New("secret store unavailable")

ErrUnavailable wraps transient backend failures (network errors, sealed or overloaded Vault, 5xx). Retrying treats it, like ErrNotFound, as worth retrying; every other error (bad token, permission denied) is terminal.

Functions

func EnvVarName

func EnvVarName(path string) string

EnvVarName returns the environment variable a logical path maps to under the generic transform: "service-keys/billing" -> "SECRET_SERVICE_KEYS_BILLING". Exported so operators can predict variable names from documented paths.

func ServiceKeyPath

func ServiceKeyPath(serviceName string) string

ServiceKeyPath returns the logical path of a service's API key.

Types

type EnvOption

type EnvOption func(*EnvStore)

EnvOption configures an EnvStore.

func WithAlias

func WithAlias(path string, envVars ...string) EnvOption

WithAlias registers environment variable names to try for a logical path before the generic transform. Later calls for the same path append.

type EnvStore

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

EnvStore resolves secrets from environment variables. It is the default backend, the permanent fallback link in the lookup chain, and the backend used by tests and the compose test profile (with fixture keys adopted by the IAM seeder).

func NewEnvStore

func NewEnvStore(opts ...EnvOption) *EnvStore

NewEnvStore creates an environment-variable-backed Store.

func (*EnvStore) Exists

func (s *EnvStore) Exists(_ context.Context, path string) (bool, error)

Exists reports whether any candidate variable for path is set and non-empty.

func (*EnvStore) Get

func (s *EnvStore) Get(_ context.Context, path string) (string, error)

Get resolves path via its aliases first, then the generic transform. Unlike remote backends it fails fast with ErrNotFound: environment variables cannot appear later in the process lifetime, so retrying would only stall startup.

func (*EnvStore) Set

func (s *EnvStore) Set(_ context.Context, path string, _ string) error

Set is not supported: processes cannot persist environment variables.

type KubernetesStore

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

func NewKubernetesStoreFromEnv

func NewKubernetesStoreFromEnv() (*KubernetesStore, error)

NewKubernetesStoreFromEnv builds a KubernetesStore from the in-cluster ServiceAccount, with env overrides for tests and out-of-cluster use: KUBERNETES_API_URL, SECRETS_K8S_NAMESPACE, SECRETS_K8S_TOKEN.

func (*KubernetesStore) Exists

func (k *KubernetesStore) Exists(ctx context.Context, path string) (bool, error)

Exists performs a single presence check without retrying.

func (*KubernetesStore) Get

func (k *KubernetesStore) Get(ctx context.Context, path string) (string, error)

Get performs a single read attempt; compose with Retrying (as NewFromEnv does) to absorb startup ordering.

func (*KubernetesStore) Set

func (k *KubernetesStore) Set(ctx context.Context, path string, value string) error

Set creates or updates the target Secret. Updates use a JSON merge patch so other keys in a shared Secret (kms-material) are preserved.

type Store

type Store interface {
	// Get returns the secret value at path.
	Get(ctx context.Context, path string) (string, error)

	// Set creates or overwrites the secret at path. Seeder (IAM) use only;
	// read-only backends return ErrReadOnly.
	Set(ctx context.Context, path string, value string) error

	// Exists reports whether a secret is present at path, without retrying.
	Exists(ctx context.Context, path string) (bool, error)
}

Store is the backend-agnostic secret access contract.

Remote backends (Vault, Kubernetes) implement Get with retry-and-backoff until the secret exists or ctx is done, which absorbs bootstrap ordering: consumers may start before the IAM seeder has run. The env backend fails fast with ErrNotFound instead - environment variables cannot appear later in a process lifetime.

func Cached

func Cached(inner Store, ttl time.Duration) Store

Cached wraps a Store with a per-path TTL cache so consumers can read secrets at call time instead of once at startup. The TTL is the upper bound on how long a rotated-out value keeps being used, so it must stay below whatever grace period the issuing system allows after a rotation. Misses are not cached: a secret that is not there yet (bootstrap ordering) must stay retryable.

func Chain

func Chain(stores ...Store) Store

Chain combines stores so that Get falls through to the next store only on ErrNotFound - any other error is terminal, so an unreachable or misbehaving primary backend surfaces instead of silently serving stale env values. This is what makes migration non-breaking: the factory chains the selected backend with the env backend, so partially migrated environments keep resolving secrets from .env until those variables are removed.

func NewFromEnv

func NewFromEnv(opts ...EnvOption) (Store, error)

NewFromEnv builds the Store selected by SECRETS_BACKEND (default "env"), wrapped in a TTL cache (SECRETS_CACHE_TTL, default 60s). Options are applied to the env backend, which stays in the chain as the permanent fallback for remote backends.

Composition for remote backends is Cached(Retrying(Chain(remote, env))): the chain consults the env fallback between retry rounds, so a secret provided via .env resolves immediately while the remote store is still being seeded.

Backend "kubernetes" (Phase 3) is recognized but not implemented yet; selecting it returns an error rather than silently degrading to env.

func NewWritableFromEnv

func NewWritableFromEnv() (Store, error)

NewWritableFromEnv returns the raw writable backend selected by SECRETS_BACKEND, or (nil, nil) when the env backend is active - env vars cannot be written, so callers switch to adoption/manual flows. Used by the IAM seeder and rotation, which need direct single-shot access rather than the cached, retrying consumer composition.

func Retrying

func Retrying(inner Store, initialBackoff, maxBackoff time.Duration) Store

Retrying wraps a Store so Get retries with exponential backoff while the secret is missing (ErrNotFound - the seeder may not have run yet) or the backend is transiently down (ErrUnavailable - the Vault container may still be starting). This is what makes compose/K8s startup ordering an optimization instead of a correctness requirement. Any other error is terminal, and the loop always stops when ctx is done.

Set and Exists are single-shot passthroughs: seeding failures and presence checks must surface immediately.

type VaultStore

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

VaultStore reads and writes secrets in a HashiCorp Vault KV v2 mount using plain HTTP - deliberately no Vault SDK, so the 17 vendored service modules do not inherit its dependency tree. Logical paths map to {addr}/v1/{mount}/data/{prefix}/{path} with the value stored under the "value" field.

func NewVaultStore

func NewVaultStore(addr, token, mount, prefix string) *VaultStore

NewVaultStore creates a Vault-backed Store. mount defaults to "secret" and prefix to DefaultVaultPrefix (overridable via SECRETS_VAULT_PREFIX) when empty.

func NewVaultStoreFromEnv

func NewVaultStoreFromEnv() (*VaultStore, error)

NewVaultStoreFromEnv builds a VaultStore from VAULT_ADDR, VAULT_TOKEN, SECRETS_VAULT_MOUNT and SECRETS_VAULT_PREFIX.

func (*VaultStore) Exists

func (v *VaultStore) Exists(ctx context.Context, path string) (bool, error)

Exists performs a single presence check without retrying.

func (*VaultStore) Get

func (v *VaultStore) Get(ctx context.Context, path string) (string, error)

Get performs a single read attempt. Compose with Retrying (as NewFromEnv does) to absorb bootstrap ordering; keeping this single-shot lets Chain consult the env fallback between retry rounds instead of blocking on Vault.

func (*VaultStore) Set

func (v *VaultStore) Set(ctx context.Context, path string, value string) error

Set creates or overwrites the secret at path (a new KV v2 version).

Jump to

Keyboard shortcuts

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