Files
at-container-registry/pkg/appview/db/crypto_keys.go
T
Evan JarrettandClaude Opus 5 934e4a2a59 appview: stop the crypto key first-boot race
Two instances booting against a fresh database both find no key, both generate
one, and both write. PutCryptoKey was last-writer-wins, so the loser kept its own
key in memory while the database held the other's. It then signed OAuth client
assertions with a key absent from the published JWKS, and issued registry JWTs
that did not match the certificate written to disk. Every one of them fails
verification, and nothing logs why.

PutCryptoKey now keeps the first write, and both loaders re-read afterwards and
use whatever is stored. Nothing in the codebase rotates a key through this
function, so the update arm only ever fired on the race.

Verified against the old behavior: with last-writer-wins restored, three of six
concurrent loaders returned a key that was not the one in the database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:34:42 -05:00

39 lines
1.3 KiB
Go

package db
import "database/sql"
// GetCryptoKey retrieves a key by name from the database.
// Returns nil, nil if no key with that name exists.
func GetCryptoKey(db DBTX, name string) ([]byte, error) {
var data []byte
err := db.QueryRow("SELECT key_data FROM crypto_keys WHERE name = ?", name).Scan(&data)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return data, nil
}
// PutCryptoKey stores a key in the database, keeping any key already stored
// under that name.
//
// First writer wins, deliberately. Two instances booting against a fresh
// database both find no key, both generate one, and both write. With
// last-writer-wins the loser kept its own key in memory while the database held
// the other's, so it signed JWTs with a key absent from the published JWKS and
// produced OAuth client assertions that fail verification. Nothing in the
// codebase rotates a key through this function, so the update arm only ever
// fired on that race.
//
// Callers must re-read after writing and use whatever is stored; see
// loadOAuthKey and loadRSAKey.
func PutCryptoKey(db DBTX, name string, data []byte) error {
_, err := db.Exec(
"INSERT INTO crypto_keys (name, key_data) VALUES (?, ?) ON CONFLICT(name) DO NOTHING",
name, data,
)
return err
}