fix(sentinel): auto-regenerate invalid or undecryptable tokens

Replace hard validation error with self-healing token logic. Tokens that
are null, empty, or fail decryption are now regenerated automatically
rather than crashing sentinel startup or metrics reads.

Token format changed from encrypted JSON payload to a plain 64-char
random string (Str::random), eliminating double-encryption issues and
simplifying the validation regex to cover the new character set.

New `ensureValidSentinelToken()` method on ServerSetting centralises
the get-or-regenerate contract; both StartSentinel and HasMetrics now
delegate to it. HasMetrics logs a warning when regeneration occurs so
operators know a sentinel container restart is required.

`isValidSentinelToken()` now accepts `?string` (null → false).

Adds feature tests covering: null/empty/undecryptable stored values,
idempotent return of valid tokens, RuntimeException only when
regeneration itself produces an invalid token, no double-encryption of
newly generated tokens, and cast round-trip consistency.
This commit is contained in:
Andras Bacsai committed 2026-04-29 16:44:12 +02:00
1 parent 3e76390194
commit 00d6e83e7f
4 files changed
+142 -19

No files matched your search

+14 -6
View File
@@ -2,7 +2,9 @@
namespace App\Traits;
use App\Models\ServerSetting;
use App\Models\Server;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Log;
trait HasMetrics
{
@@ -28,9 +30,15 @@ trait HasMetrics
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$endpoint = $this->getMetricsEndpoint($type, $from);
$token = $server->settings->sentinel_token;
if (! ServerSetting::isValidSentinelToken($token)) {
throw new \Exception('Invalid sentinel token format. Please regenerate the token.');
$previousToken = null;
try {
$previousToken = $server->settings->sentinel_token;
} catch (DecryptException) {
// fall through to ensureValidSentinelToken which will regenerate
}
$token = $server->settings->ensureValidSentinelToken();
if ($token !== $previousToken) {
Log::warning('Regenerated sentinel token during metrics read; sentinel container restart required', ['server_id' => $server->id]);
}
$response = instant_remote_process(
@@ -61,10 +69,10 @@ trait HasMetrics
private function isServerMetrics(): bool
{
return $this instanceof \App\Models\Server;
return $this instanceof Server;
}
private function getMetricsServer(): \App\Models\Server
private function getMetricsServer(): Server
{
return $this->isServerMetrics() ? $this : $this->destination->server;
}