fix(auth): repair persistence layer and stop storing plaintext keys

- Store SHA-256 digests (key_hash + key_preview) instead of raw keys, in
  both the in-memory store and Postgres; migrate legacy plaintext rows in
  place and drop the plaintext column.
- Fix TEXT[] scans that failed at runtime (missing pq.Array) in
  GetKey/ListKeys/LoadAllKeys.
- Load persisted keys at startup (InitializeFromDatabase was never called)
  and fall back to the database on cache miss, so issued keys survive
  restarts.
- Make HybridKeyStore.CreateKey genuinely write-through: cache is only
  updated after a successful DB write.
- Guard nil database backend (auth enabled without DB previously panicked
  on key creation) and degrade to in-memory operation.
- Persist bootstrap admin keys when a database is configured.
- Record real audit-log details as JSON and log audit failures instead of
  silently dropping them; add DB pool limits and ping timeout.
- Sentinel errors matched with errors.Is; unit tests for hashing,
  write-through ordering, DB fallback, and nil-DB operation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 20:27:49 -07:00
parent d38c700949
commit 172c240d1b
7 changed files with 822 additions and 399 deletions
+25 -4
View File
@@ -99,7 +99,7 @@ func main() {
// Create database backend if configured
var dbStore *auth.KeyStoreDB
if cfg.Auth.Database.URL != "" {
dbStore, err = auth.NewKeyStoreDB(cfg.Auth.Database.URL)
dbStore, err = auth.NewKeyStoreDB(cfg.Auth.Database.URL, logger)
if err != nil {
logger.Fatalf("Failed to create database key store: %v", err)
}
@@ -112,6 +112,16 @@ func main() {
hybridKeyStore = auth.NewHybridKeyStore(authStore, dbStore)
logger.Debugf("Initialized hybrid key store for API key management")
// Load persisted keys into the cache so previously issued keys
// keep authenticating across restarts.
if hybridKeyStore.HasDatabase() {
if loaded, err := hybridKeyStore.InitializeFromDatabase(ctx); err != nil {
logger.Errorf("Failed to load API keys from database: %v", err)
} else {
logger.Infof("Loaded %d API key(s) from database into cache", loaded)
}
}
// Bootstrap admin key if configured
if cfg.Auth.Bootstrap.Enabled {
bootstrapCfg := &auth.BootstrapConfig{
@@ -133,12 +143,23 @@ func main() {
// If we have an existing key, use it
if existingKey != "" {
if _, err := auth.RegisterAdminKeyInMemory(authStore, existingKey, logger); err != nil {
apiKey, err := auth.RegisterAdminKeyInMemory(authStore, existingKey, logger)
if err != nil {
logger.Warnf("Failed to register existing admin key: %v", err)
} else if err := hybridKeyStore.EnsurePersisted(ctx, apiKey, "bootstrap"); err != nil {
logger.Warnf("Failed to persist existing admin key: %v", err)
}
} else {
// Generate new key
if apiKey, err := auth.BootstrapAdminKeyInMemory(authStore, bootstrapCfg, logger); err != nil {
// Generate a new key — through the hybrid store when a
// database is configured so the admin key survives restarts.
var apiKey *auth.APIKey
var err error
if hybridKeyStore.HasDatabase() {
apiKey, err = auth.BootstrapAdminKey(ctx, hybridKeyStore, bootstrapCfg, logger)
} else {
apiKey, err = auth.BootstrapAdminKeyInMemory(authStore, bootstrapCfg, logger)
}
if err != nil {
logger.Warnf("Bootstrap admin key creation failed: %v", err)
} else if apiKey != nil {
// Store in Kubernetes secret if configured