Optional pluggable 2FA (TOTP and/or WebAuthn, or off) + recovery-codes modal,
live capped journal, command-palette server search, settings tabs in URL,
fail2ban lone-box layout, and SSH-key auto-provision with safe password-login
disable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guard the TOTP verifyKey call behind hasTotp() so a key-only user's null
two_factor_secret never reaches Google2FA::verifyKey(); they reset via a
backup code. Also adds the reset_no_2fa_note lang key (EN + DE) rendered
on the forgot-password form explaining the no-2FA recovery options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TwoFactorSetup: move to app layout (hosts wire-elements/modal), guard on hasTotp() not hasTwoFactorEnabled(), redirect to settings and flash open_recovery_modal on first TOTP enrollment
- WebauthnKeys.register(): drop hasTwoFactorEnabled() guard (first key may be the sole factor), generate codes + dispatch openModal on first key enrollment
- Settings\Index: add #[Url] on $tab, add $openRecoveryModal flag, mount() reads the flash and lands on the security tab
- settings/index.blade.php: x-init block dispatches openModal when $openRecoveryModal is set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Journal was a one-time snapshot mislabeled "journalctl -f": it loaded the last
N lines once and never refreshed, so it was neither live nor complete.
- FleetService: `journalctl --show-cursor` + new journalSince() reads only entries
after the stored journald cursor (no gaps, no dup appends), capped to a bounded
live tail; parseJournal split into splitJournalCursor + parseJournalLines.
- Services\Index: pollJournal() (wire:poll.5s) appends new entries since the cursor
and caps retained rows at 300 (fetch ceiling == display ceiling).
- Honest labels: drop the misleading "-f"; badge stays "live" (now true).
Command palette (Strg/⌘ K) can now jump to a server: the fleet feeds the Alpine
cmdk x-data; servers surface only while searching, matched on name OR IP, and
selecting one navigates to /servers/<uuid>.
Cursor is validated (^[a-z0-9;=]+$) and single-quoted before the remote shell —
no injection (Codex-reviewed: no P1, no security findings). Removes stock
ExampleTest (asserted 200 on an auth-gated route → always 302).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2FA becomes optional + pluggable: TOTP, a security key, or both, fully
deactivatable, recommended not forced. hasTwoFactorEnabled = either factor;
onboarding drops the forced-2FA redirect; recovery codes become a modal (drop the
dedicated page/route); challenge adapts to whichever factor(s) exist; last-factor
removal clears codes. WebAuthn E2E stays domain-deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sidebar server-switcher syncs with the viewed server (race-free, mount-only).
Documented bfcache/multi-tab edge. See CHANGELOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revert the hydrate-on-every-request sync — writing the single global active-server
session on every async request (slow load(), polls, parallel tabs) is inherently
last-writer-wins and races. Setting it only at mount (once per deliberate
navigation) is race-free and fixes the visible drift. bfcache restore + multi-tab
remain inherent edges of the single-session model — documented in the handoff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a hydrate() hook so Show re-asserts its server as active on the wire:poll tick
and any action — covering a wire:navigate bfcache restore that skips mount(),
without the earlier racy client-side POST. Idempotent + server-side, no race.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The livewire:navigated activate-POST hook introduced unbounded cross-request
ordering races (the on-screen confusion is already fixed race-free by
Show::mount setting the active server + ServerSwitcher::targetFor navigating on
switch). Remove the JS hook + activate endpoint. The remaining bfcache edge is
invisible (the cached page shows the right server) and a transient stale session
resolves on the next deliberate navigation — documented in the handoff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aborting a fetch can't stop a POST already running server-side, so a stale
activation could still land last. Chain the requests so each runs after the
previous resolves — the latest navigation's activation is always last. A 5s
per-request timeout keeps a hung request from stalling the chain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Abort any in-flight active-server POST on each navigation so a slower stale
request can't overwrite the latest selection. Fix the activate-endpoint test to
use an onboarded user (the route is behind EnsureSecurityOnboarded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wire:navigate restores Back/Forward pages from its client cache without re-running
Show::mount(), which could leave active_server_id pointing at a different server
than the details page on screen. Add a servers.activate endpoint + a global
livewire:navigated hook that re-asserts the on-screen server on every navigation
(including cache restores).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two "current server" notions diverged: the sidebar switcher (session active_server_id)
vs the server-details URL. Now viewing a server's details sets it active (sidebar
always matches the page), and switching the sidebar while on a details page navigates
to the chosen server (instead of reloading the old one). One current server, in sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebAuthn/YubiKey as an optional login 2nd factor (web-auth/webauthn-lib), gated
on domain+HTTPS. TOTP + backup codes stay the required 2FA. Register/use/remove
keys in Settings + at the challenge; 2FA reset (UI + CLI) clears keys. Real
YubiKey end-to-end is browser-verified on a domain host. See CHANGELOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ZeroTolerantCounterChecker only tolerates a 0 counter when the stored counter is
also 0; a 0 after a non-zero counter is a rollback and is rejected.
- clusev:reset-admin --disable-2fa now also deletes the user's security keys, so a
compromised key can't revive when 2FA is re-enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Swap the strict counter checker for a zero-tolerant one so platform
authenticators / synced passkeys (which always report counter 0) can assert,
while non-zero counters still must strictly increase (clone detection).
- Guard registration (options + register) and the UI on hasTwoFactorEnabled, so a
key is never created against a disabled/reset second factor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- credential_id can exceed the unique-index key length; store it in TEXT and
enforce uniqueness on an auto-derived sha256 hash column (lookups use the hash).
- Disabling 2FA now also deletes the user's security keys + backup codes, so
re-enrolling starts clean and old factors never silently revive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
options() now validates the label before returning creation options, so clicking
Add with a blank/invalid name aborts before navigator.credentials.create() and
never leaves an orphaned credential on the authenticator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist the updated PublicKeyCredentialSource (not just the sign_count column)
so the next assertion validates against the latest counter — keeping the
library's cloned-authenticator / counter-rollback detection effective.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A "Security-Keys" panel in Settings → Security: add (name + navigator.credentials
.create via resources/js/webauthn.js), list (name/added/last-used), remove (R5
confirm modal), all audited. Hidden with a hint when domain+HTTPS is absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the panel runs on a domain+HTTPS and the user has a registered key, the 2FA
challenge offers "Mit Security-Key anmelden" alongside the TOTP/backup-code field.
A verified assertion completes login exactly like a TOTP success; rate-limit +
session gate reused. TOTP and backup codes remain fully usable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebauthnService builds creation/request options (rpId = active domain, ES256/RS256,
attestation none, exclude/allow lists) via web-auth/webauthn-lib v5, stores the
challenge in the session, and verifies attestation/assertion responses (allowed
origin = https://<rpId>, sign-count tracked). Option building + serialization are
unit-tested; the cryptographic verify path is browser-verified on a domain host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebAuthn is offered only with an active domain (the rpId) over HTTPS; bare-IP/HTTP
returns false so a ceremony never runs with an invalid IP rpId.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add web-auth/webauthn-lib ^5.3, the webauthn_credentials table (per-user unique
credential_id), the WebauthnCredential model and User hasMany relation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional security-key 2nd factor via web-auth/webauthn-lib, gated on domain +
HTTPS (rpId = active domain). TOTP + backup codes stay the required 2FA and the
only recovery. Build now with unit/feature tests (mocked ceremony) + R12 gating;
real YubiKey end-to-end deferred to a domain+HTTPS host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Account recovery: 2FA backup codes (show-once/download/regenerate, usable at the
login challenge), forgot-password via 2FA or backup code (email link gated on a
configured mailer), and a clusev:reset-admin CLI lockout fallback. Resets rotate
remember_token; codes consumed atomically. WebAuthn/YubiKey remains a Phase-2
spec. See CHANGELOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both reset paths flash a `status` and redirect to login, but it was never
rendered. Show it as a success banner so the user gets confirmation their
password changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex review follow-ups: extract the notify toaster into a shared partial and
include it in the auth layout too, so sendResetLink + recovery-code regenerate
actually surface their confirmation (the auth layout had no toaster). Record an
AuditEvent for password resets via the email-token path, matching the 2FA path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Codex security review:
- ForgotPassword + clusev:reset-admin now rotate remember_token, so a stolen
remember-me cookie cannot survive a password reset (the email-token path
already did this).
- useRecoveryCode reads/checks/removes under a row lock (DB transaction +
lockForUpdate), so two concurrent requests can't both spend the same one-time
code (replay).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shell-only emergency recovery: resets the admin password (prints a random one if
omitted) and, with --disable-2fa, clears the 2FA secret + backup codes so the
operator can re-enroll. Requires server access — the safe last resort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ForgotPassword gains sendResetLink() + a ResetPassword component at
/reset-password/{token} using Laravel's password broker. The email option is
shown only when mail.default is a real mailer (hidden while MAIL_MAILER=log), so
it activates automatically once SMTP is configured. Tested with Notification::fake.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>