守り · 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

Four 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.

03

Atomic & validated

An update that fails validation is rejected. Get() keeps returning the last good config. Your process never observes a half-applied or invalid state.

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.

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
provider ecosystem

Read from where your secrets already live.

Providers register with the database/sql pattern. Core has zero cloud-SDK dependencies - each provider is its own module. All pass theprovidertest conformance kit.

env:built-in

Environment

poll
dotenv://built-in

Dotenv files

fsnotify
file://built-in

Files

fsnotify
aws-sm://

AWS Secrets Manager

poll
aws-ps://

AWS Parameter Store

poll
vault://

HashiCorp Vault

lease-aware
k8s-secret://

Kubernetes Secret

native watch
k8s-cm://

Kubernetes ConfigMap

native watch
consul://

Consul KV

native watch
gcp-sm://

GCP Secret Manager

poll
azure-kv://

Azure Key Vault

poll
doppler://

Doppler

poll
op://

1Password

poll
sops://

SOPS (age/KMS)

fsnotify
postgres://

PostgreSQL

native watch
mysql://

MySQL

poll
sqlite://

SQLite

fsnotify
mongodb://

MongoDB

native watch
redis://

Redis

native watch
dynamodb://

DynamoDB

poll
etcd://

etcd

native watch
firestore://

Firestore

native watch
firebase-rtdb://

Firebase RTDB

native watch
firebase-rc://

Firebase Remote Config

poll
s3://

Amazon S3

poll
gcs://

Google GCS

poll
azblob://

Azure Blob

poll
cosmos://

Azure Cosmos DB

poll
launchdarkly://

LaunchDarkly

native watch
unleash://

Unleash

poll
flagsmith://

Flagsmith

poll
configcat://

ConfigCat

poll
split://

Split

poll
growthbook://

GrowthBook

poll
flipt://

Flipt

poll
goff://

GO Feature Flag

poll
yours://

Write your own

Provider SPI →
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.

Redacted everywhere

secret.String and secret.Bytes render as [REDACTED] in String(), fmt, JSON, and slog. Only Reveal() exposes the value - and it's greppable in review.

A vet analyzer

reconcilevet flags any secret-bearing ref assigned to a plain string or []byte, so a leak can't slip past code review.

No injection chains

The exec: provider is off by default. Refs are never interpolated from other resolved values - one secret can't build another's command.

Honest about limits

Zero() is best-effort and documented as such - no false promises about Go's GC. No payloads are ever logged; the conformance kit asserts it.

Give your config a guardian.

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