juanfont/headscale · critical
adding hash column: %w
Error message
adding hash column: %w
What it means
Thrown by migration '202511011637-preauthkey-bcrypt' when GORM cannot add the 'hash' column to pre_auth_keys (bcrypt hash storage for pre-auth keys). It is the second guarded AddColumn in the same migration; the wrapped error carries the true database-level cause. Because migrations execute in a startup transaction, this error prevents headscale from starting.
Source
Thrown at hscontrol/db/db.go:529
{
// Add columns for prefix and hash for pre auth keys, implementing
// them with the same security model as api keys.
ID: "202511011637-preauthkey-bcrypt",
Migrate: func(tx *gorm.DB) error {
// Check and add prefix column if it doesn't exist
if !tx.Migrator().HasColumn(&types.PreAuthKey{}, "prefix") {
err := tx.Migrator().AddColumn(&types.PreAuthKey{}, "prefix")
if err != nil {
return fmt.Errorf("adding prefix column: %w", err)
}
}
// Check and add hash column if it doesn't exist
if !tx.Migrator().HasColumn(&types.PreAuthKey{}, "hash") {
err := tx.Migrator().AddColumn(&types.PreAuthKey{}, "hash")
if err != nil {
return fmt.Errorf("adding hash column: %w", err)
}
}
// Create partial unique index to allow multiple legacy keys (NULL/empty prefix)
// while enforcing uniqueness for new bcrypt-based keys
err := tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_pre_auth_keys_prefix ON pre_auth_keys(prefix) WHERE prefix IS NOT NULL AND prefix != ''").Error
if err != nil {
return fmt.Errorf("creating prefix index: %w", err)
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
ID: "202511122344-remove-newline-index",
Migrate: func(tx *gorm.DB) error {
// Reformat multi-line indexes to single-line for consistencyView on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the wrapped error text in the log for the DB-specific cause and address that directly
- Verify filesystem permissions on the SQLite database directory (the WAL files must be writable too) or network connectivity/privileges for PostgreSQL
- Stop all other headscale processes before upgrading so migrations run exclusively
- If the error persists, restore the pre-migration backup and retry the upgrade in a controlled window
Defensive patterns
Strategy: validation
Validate before calling
// Ensure schema state is consistent before startup (SQLite example)
db, _ := sql.Open("sqlite", file)
var n int
db.QueryRow("SELECT count(*) FROM pragma_table_info('pre_auth_keys') WHERE name IN ('prefix','hash')").Scan(&n)
if n != 0 && n != 2 {
log.Fatal("pre_auth_keys schema is half-migrated (prefix without hash or vice versa); restore from backup")
} Prevention
- Never kill headscale mid-startup; let migrations finish or restore the backup
- Keep the SQLite file and its directory writable by the headscale process user (WAL files included)
- Pin one headscale version across the fleet; mixed versions sharing a DB cause half-applied states
When it happens
Trigger: Same startup migration path as the prefix column: pre_auth_keys.hash missing and AddColumn fails due to missing ALTER privilege, SQLite 'database is locked', full disk, or an unreadable/corrupt database file.
Common situations: Upgrading an old headscale database to the bcrypt pre-auth-key model; concurrent headscale instances; container running as a user that cannot write the SQLite file or its -wal/-shm siblings; PostgreSQL connection dropped mid-migration.
Related errors
- automigrating types.Route: %w
- adding prefix column: %w
- migration failed: %w
- saving API key to database: %w
- version check: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/f88899a1697b92e1.
Report an issue: GitHub.