auth: fehlgeschlagene Anmeldungen mit Einstieg (livewire/fortify/two-factor); unbekannte Adressen als Tageszähler statt Verlaufseintrag, Kachel im Systemstatus, Löschfrist 90 Tage

Auf Live kamen seit dem 05.10. rund 250 Versuche am Tag aus über 50 Quellen,
alle mit unbekannter Adresse — verteiltes Durchprobieren fremder
Zugangsdaten, das im Verlauf die relevanten Einträge verdeckte. Neue Tabelle
failed_login_tallies (Tag × Einstieg × IP-Hash), model:prune täglich 04:05.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-06 15:30:29 +00:00
parent 9f806de1af
commit 05535aac37
9 changed files with 303 additions and 21 deletions

View file

@ -4,6 +4,7 @@ namespace App\Services\Admin;
use App\Authorization\Permission as PermissionCatalog;
use App\Models\ArchiveEntry;
use App\Models\FailedLoginTally;
use App\Models\PressReleaseAttachment;
use App\Models\PressReleaseImage;
use App\Services\Archive\ArchivePendingQueue;
@ -145,6 +146,35 @@ class AdminSystemHealth
];
}
/**
* @return array{key: string, label: string, status: string, detail: string}
*/
private function failedLoginCheck(): array
{
$summary = fn (string $day): object => FailedLoginTally::query()
->whereDate('day', $day)
->selectRaw('coalesce(sum(attempts), 0) as attempts, count(distinct ip_hash) as sources')
->first() ?? (object) ['attempts' => 0, 'sources' => 0];
$today = $summary(now()->toDateString());
$yesterday = $summary(now()->subDay()->toDateString());
$warnPerDay = (int) config('monitoring.failed_logins.warn_per_day', 1000);
return [
'key' => 'failed_logins',
'label' => 'Anmeldeversuche mit unbekannter Adresse',
'status' => (int) $today->attempts >= $warnPerDay ? self::StatusWarn : self::StatusOk,
'detail' => sprintf(
'Heute %d Versuche aus %d Quellen, gestern %d aus %d.%s',
(int) $today->attempts,
(int) $today->sources,
(int) $yesterday->attempts,
(int) $yesterday->sources,
(int) $today->attempts >= $warnPerDay ? ' Ungewöhnlich viele — Einstieg in failed_login_tallies prüfen.' : '',
),
];
}
/**
* @param array<string, mixed> $queue
* @return list<array{key: string, label: string, status: string, detail: string}>
@ -167,6 +197,11 @@ class AdminSystemHealth
$checks[] = $probe->scheduler();
$checks[] = $probe->disk();
// Anmeldeversuche mit unbekannter Adresse (Nachtrag 1.7): Tageszähler
// statt Verlaufseinträgen. Viel Grundrauschen ist normal; warnen erst
// ab der konfigurierten Tagesmenge.
$checks[] = $this->failedLoginCheck();
// Neue Permissions ausgerollt?
$expected = PermissionCatalog::adminOnly();
$existing = Permission::query()->whereIn('name', $expected)->pluck('name')->all();