守り · config & secrets for go

Secrets that
reconcile themselves.

mamori loads configuration and secrets from anywhere into typed, validated Go structs - then watches every source and reconciles changes at runtime. Rotate a database password upstream; your pool rotates too. No restart.

go get github.com/xavidop/mamori
principles

Five ideas, composed.

Each does one thing, through a well-defined interface, testable on its own.

01

Typed loading

One struct, tag-driven, many sources. A generics API - Load[T] and Watch[T] - decodes and validates into your own types. No stringly-typed lookups.

02

Runtime reconciliation

Native watch where the backend supports it - Kubernetes informers, Consul blocking queries, fsnotify - polling with jitter everywhere else, and lease-aware refresh for Vault. Values you assemble yourself keep up too: WithDerive rebuilds a DSN from its parts on every update, so a rotated password never leaves a stale connection string behind.

03

Atomic & gated

A bad update never goes live. Validation rejects it, and you can add your own check on top: try the new credential, and say no if it fails. Until then your app keeps serving the last good config.

04

Secret hygiene

secret.String redacts itself in logs, fmt, and JSON. Only Reveal() exposes the value - and a shipped go vet analyzer flags sensitive refs stored in plain strings.

05

Boots through an outage

A restart while the backend is down still starts. WithBootstrapCache keeps an encrypted snapshot of the last known-good values on disk and boots from it when a cold start cannot reach the backend, then returns to live values as soon as it can. Opt-in, bounded by a max age you set, and reported in Status so a stale start is never silent.

quickstart

Declare it. Watch it.

Tag a struct with where each value lives. mamori resolves, validates, and - withWatch - keeps it reconciled, handing you a diff-aware callback.

// One struct, many sources.
type Config struct {
  DBPassword secret.String `source:"aws-sm://prod/db#password"`
  LogLevel   string        `source:"env:LOG_LEVEL" default:"info"`
  Workers    int           `source:"env:WORKERS" validate:"gte=1,lte=256"`
  TLSCert    []byte        `source:"file:///etc/tls/tls.crt"`
}
// Watch: reconcile at runtime, react without restarting.
w, _ := mamori.Watch[Config](ctx,
  mamori.OnChange(func(ev mamori.Change[Config]) {
    if ev.Changed("DBPassword") {
      pool.Rotate(ev.New.DBPassword.Reveal())
    }
  }),
)
defer w.Close()

cfg := w.Get() // lock-free; always the last valid config
middleware

One interface, so it all composes.

Because every provider is a Provider, decorators nest freely. Cache to cut API cost, fail over to a replica, rate-limit a twitchy backend, audit every resolve, or rewrite refs per tenant.

  • Cache - memoize resolves for a TTL
  • Failover - primary, then replicas
  • RateLimit - protect rate-limited backends
  • Audit - log every access, never the payload
  • Prefix - multi-tenant namespace rewriting
mamori.WithProvider(
  middleware.Cache(5*time.Minute,
    middleware.Audit(logger,
      middleware.Failover(
        primarySM,
        replicaSM,
      ),
    ),
  ),
)
security

Secret-safe by construction.

Every secret passes through your config code. That code also ends up in logs, errors, and bug reports. So these are on by default.

Secrets stay out of your logs

Secrets have their own type. Print one and you get [REDACTED]. That holds in logs, in JSON, in errors, in a panic. Getting the real value takes an explicit call, so it is one word to grep for.

A leak fails the build

mamori ships a go vet analyzer. Put a secret in a plain string and CI fails. Nobody has to catch it in review.

One secret cannot unlock another

Running commands is off by default. A secret mamori resolved can never build the next lookup. Neither can your environment. So a leaked secret cannot point mamori somewhere new.

Honest about what it cannot do

Wiping a secret from memory is best effort. Go's runtime cannot promise more, and we do not pretend otherwise. What we do promise is tested: no secret value ever reaches a log.

Give your config a guardian.

Typed. Watched. Reconciled. Runs in a Lambda, a systemd unit, or a Pod.