Skip to content

REST API: every Bearer API-key request returns HTTP 500 "attempt to write a readonly database" (TokenValidationService::syncLastUsedToDatabase) #1120

Description

@okacyzzz-oss

Summary
Authentication with an API key (Authorization: Bearer ) always fails with HTTP 500 on an LXC installation. TokenValidationService tries to save the key's last-used time to the main database from the php-fpm process, which runs as www and cannot write /cf/conf/mikopbx.db. The exception is not caught, so every request with an API key fails, including requests the key should allow.

Evidence
Server: MikoPBX 2026.3.40 (Asterisk 22.8.2) in an LXC container, PHP 8.4
/cf/conf/mikopbx.db: owner root:root, mode 0644 (as created by the installer)
php-fpm pool user: www
API key: full_permissions false, allowed_paths {"/api/v3/employees": "write"}

Request Allowed by the key Expected Actual
GET /pbxcore/api/v3/employees?limit=5 yes 200 500
GET /pbxcore/api/v3/employees/121 yes 200 500
GET /pbxcore/api/v3/extensions no 403 500
GET /pbxcore/api/v3/api-keys no 403 500

System log (on every request):
PDOException: SQLSTATE[HY000]: General error: 8 attempt to write a readonly database in src/PBXCoreREST/Services/TokenValidationService.php on line 204
Phalcon\Mvc\Model->save() TokenValidationService.php:204
TokenValidationService->syncLastUsedToDatabase() TokenValidationService.php:187
TokenValidationService->updateLastUsedBuffer() TokenValidationService.php:115
TokenValidationService->validatePermissions() TokenValidationService.php:85
TokenValidationService->validate() Middleware/AuthenticationMiddleware.php:188
AuthenticationMiddleware->authenticateWithBearerToken() Middleware/AuthenticationMiddleware.php:98

Expected Behavior
A valid API key is accepted (200), and a path that is not in allowed_paths is rejected (403). The last-used time is informational and should never break authentication.

Potential Root Cause
validatePermissions() calls updateLastUsedBuffer(), which calls syncLastUsedToDatabase() when LAST_USED_SYNC_INTERVAL (60 s) has passed since the last sync. syncLastUsedToDatabase() runs ApiKeys::findFirst($keyId)->save() directly inside php-fpm. The main database is read-only for www, so save() throws, and nothing catches the exception.

The ":sync" cache key is written only after syncLastUsedToDatabase() returns. Because the save throws, the key is never written, so the sync is attempted (and fails) on every request and the 60-second throttle never takes effect. API-key authentication is therefore completely unusable on this installation.

The same code exists on the develop branch.

Requests from 127.0.0.1 without a key are not affected: they do not reach this code path, and data writes are performed by the backend worker running as root.

Possible fixes:
Wrap the last-used update in try/catch and log the error instead of failing authentication, and write the ":sync" key before attempting the save
Hand the last-used update to a backend worker (as other writes are handled) instead of saving from php-fpm

Reproduction
Install MikoPBX 2026.3.40 in an LXC container (database owned by root:root 0644, php-fpm running as www)
Create an API key: description "test", full_permissions false, allowed_paths {"/api/v3/employees": "write"}
From another host: curl -k -H "Authorization: Bearer " "https:///pbxcore/api/v3/employees?limit=5"
The response is HTTP 500, and the system log shows the PDOException above

Files to Investigate
src/PBXCoreREST/Services/TokenValidationService.php — updateLastUsedBuffer(), syncLastUsedToDatabase()
src/PBXCoreREST/Middleware/AuthenticationMiddleware.php — authenticateWithBearerToken()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions