{"record":{"id":"9eaf9d97fd178b92","repo":"juanfont/headscale","slug":"adding-revoked-to-pre-auth-keys-w","errorCode":null,"errorMessage":"adding revoked to pre_auth_keys: %w","messagePattern":"adding revoked to pre_auth_keys: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":826,"sourceCode":"\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"adding description to pre_auth_keys: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Add a revoked timestamp to pre-auth keys. The v2 API's DELETE\n\t\t\t\t// soft-revokes a key (set revoked = now) rather than destroying\n\t\t\t\t// it; the row is reaped later by the background collector.\n\t\t\t\tID: \"202606201200-pre-auth-key-revoked\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\tif !tx.Migrator().HasColumn(&types.PreAuthKey{}, \"revoked\") {\n\t\t\t\t\t\terr := tx.Migrator().AddColumn(&types.PreAuthKey{}, \"revoked\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"adding revoked to pre_auth_keys: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Add the OAuth client + access token tables backing the v2 API's\n\t\t\t\t// OAuth client-credentials flow. They mirror the api_keys /\n\t\t\t\t// pre_auth_keys security model: a public id/prefix plus an Argon2id\n\t\t\t\t// hash of the secret.\n\t\t\t\t//\n\t\t\t\t// SQLite uses explicit DDL that matches schema.sql byte-for-byte\n\t\t\t\t// (the squibble digest is the SQLite source of truth). Postgres,\n\t\t\t\t// which has no digest and rejects SQLite-isms like AUTOINCREMENT,\n\t\t\t\t// uses dialect-aware AutoMigrate, mirroring InitSchema's fresh-DB\n\t\t\t\t// table creation so an existing Postgres deployment can upgrade.","sourceCodeStart":808,"sourceCodeEnd":844,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L808-L844","documentation":"Migration '202606201200-pre-auth-key-revoked' fails adding the revoked timestamp column to pre_auth_keys, which backs the v2 API's soft-revoke (revoked = now) semantics. Same AddColumn failure class: DDL privileges, locks, disk space, or a read-only database.","triggerScenarios":"Schema lacking pre_auth_keys.revoked while the guarded AddColumn cannot execute due to environment problems rather than the guard logic itself.","commonSituations":"Version upgrades introducing soft-revocation; locked SQLite files; read-only database mounts (e.g. container filesystem mounted ro).","solutions":["Fix the wrapped cause: make the DB writable, unlock it, grant ALTER, free space","Manual fallback for drifted schemas: ALTER TABLE pre_auth_keys ADD COLUMN revoked datetime NULL; restart - HasColumn then skips AddColumn","Verify soft-revoke works after upgrade by revoking a test key via the v2 API and checking revoked is set"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Confirm the database accepts DDL before the upgrade window\nif _, err := db.Exec(\"CREATE TABLE IF NOT EXISTS _probe(id integer)\"); err != nil {\n\tlog.Fatalf(\"cannot execute DDL (locked/readonly/no privilege): %v\", err)\n}\ndb.Exec(\"DROP TABLE IF EXISTS _probe\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Back up before upgrading to soft-revoke-capable versions","After upgrade, smoke-test: create and revoke a pre-auth key via the v2 API"],"tags":["database","migration","preauthkey","soft-delete","ddl"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}