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()
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()