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 ¶
const ( BackendEnv = "env" BackendVault = "vault" BackendKubernetes = "kubernetes" )
Well-known backend names accepted in SECRETS_BACKEND.
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.
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 ¶
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.
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.
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.
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.
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.
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 ¶
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 ¶
ServiceKeyPath returns the logical path of a service's API key.
Types ¶
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 ¶
NewEnvStore creates an environment-variable-backed Store.
func (*EnvStore) Exists ¶
Exists reports whether any candidate variable for path is set and non-empty.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.