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 consistency

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Inspect the wrapped error text in the log for the DB-specific cause and address that directly
  2. Verify filesystem permissions on the SQLite database directory (the WAL files must be writable too) or network connectivity/privileges for PostgreSQL
  3. Stop all other headscale processes before upgrading so migrations run exclusively
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/f88899a1697b92e1. Report an issue: GitHub.