Commit Graph

15 Commits (94425f93e8557bcb89b93dfccc4e237fa072edfc)

Author SHA1 Message Date
boban 968d8c48c9 feat(auth): feed BruteforceGuard from login + 2FA failures, audit them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:01:54 +02:00
boban 4ea4050c80 fix(auth): R15 review — restore one-time-code autofill, memoise pendingUser in verify
- backup field: autocomplete=off -> one-time-code (backup codes ARE one-time codes;
  matches the main challenge field, the rest of the app, and the spec; restores
  password-manager OTP autofill that the earlier polish broke)
- both verify() use the trait's memoised $this->pendingUser() instead of a fresh
  User::find(), saving a redundant query on the failed-attempt re-render (Pint dropped
  the now-unused User import in TwoFactorBackup)
- spec: align trait backing-state visibility note to the implemented 'private'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:19:40 +02:00
boban 8a8232a355 test(auth): cover TOTP+key challenge branch; clarify mount comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:46:37 +02:00
boban 67ce2de538 feat(auth): split backup code out of the main 2FA challenge view
Mount now redirects key-only users without a secure context straight to
the dedicated backup view. The main challenge form renders only for TOTP
users; a subordinate button links to two-factor.challenge.backup instead
of embedding the field inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 15:41:19 +02:00
boban 1fef9e61fe refactor(auth): extract CompletesTwoFactorChallenge trait 2026-06-20 15:24:09 +02:00
boban f2cbf60c04 fix(security): harden brute-force, rate-limiting and auth-DoS (audit follow-up)
Adversarial audit of the auth surface confirmed several exploitable gaps. Every fix
auto-expires and never permanently locks the control plane (bare-IP recovery host and
the clusev:reset-admin CLI stay open).

Login:
- per-IP (20/min) + per-account (30/15min) buckets on top of the existing email+IP 5/min,
  so a distributed multi-IP brute-force of the single admin account is capped instead of
  scaling linearly with IPs. ip/acct counters decay on their own (a valid login doesn't
  reset a flood's budget).
- constant-time: the unknown-email branch now runs one bcrypt against a fixed DUMMY_HASH
  (evaluated before the `! $user ||` short-circuit), closing the account-enumeration
  timing oracle.

2FA:
- TOTP replay protection — verifyTotp uses verifyKeyNewer with a persisted last-used step
  (two_factor_last_used_step migration), so each 30s code is single-use. Passes 0 (never
  null) as the old step, since null makes verifyKeyNewer return bare `true` not the step.
- challenge + 2FA-proof reset gain an IP-independent per-account backstop, capping the
  distributed TOTP/backup brute-force surface.

Defense-in-depth / DoS:
- global throttle on /livewire/update (180/min per user-id-or-IP) via setUpdateRoute,
  keeping 'web' + the persistent EnsureSecurityOnboarded gate intact.
- reauth throttle (5/min per user) on password change/profile; SMTP test-send capped
  (3/10min per user) so the panel can't relay spam.
- SshClient gets a short, separate connect timeout (~5s) so a dead/tarpit host can't hold
  a worker across several sequential 15s reads (the observed 8.3s /livewire/update DoS).
- profile password policy raised to 12 chars + mixed case + numbers (was 10).

Tests: BruteForceHardeningTest covers TOTP replay, per-email + per-IP login caps,
identical unknown/known rejection, and reauth throttling. Full suite 187 green; Codex
clean; verified in a real browser (login/polls/Livewire updates unaffected by the throttle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:59:45 +02:00
boban 5b1e7e5168 fix(2fa): TwoFactorSetup back to auth layout; exception-safe User::verifyTotp at all call sites
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:40:15 +02:00
boban aa43947dcf fix(2fa): memoize challenge pending-user lookup; assert key-only view; tidy hint wording
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:17:18 +02:00
boban 55d513f241 feat(2fa): challenge adapts to factor — TOTP field gated, key-only leads with the key + backup code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:10:25 +02:00
boban 7afa50d253 feat(webauthn): use a security key at the login challenge
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>
2026-06-14 18:26:58 +02:00
boban b2cb94bb32 feat(auth): accept a 2FA backup code at the login challenge
The challenge now falls back to consuming a one-time recovery code when the TOTP
check fails. Relaxed the code input (was numeric, maxlength 6) so a backup code
is typable, and added a hint. Bilingual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:50:36 +02:00
boban 5691051ad8 feat(i18n): bilingual foundation (DE/EN) + localize auth/dashboard/servers/fleet/account
Adds user-selectable German (default) + English, per new rule R16.

Foundation:
- config locale=de + fallback=de; User.locale column (+migration); SetLocale middleware
  (user → session → default; SUPPORTED=[de,en]); public /locale/{code} route; DE/EN
  switcher (x-lang-switch) in topbar + auth layout; <html lang> dynamic.
- Translation structure: lang/{de,en}/<group>.php (one group per feature + shared common),
  identical keys across both languages, :placeholder interpolation. Documented as R16 in
  rules.md + CLAUDE.md (R9 reconciled).
- Removed the "Warum Caddy" rationale block from the System page (per feedback).

Localized this pass (views + Livewire components, keys de↔en at parity):
- common, auth, dashboard, servers, services, files, audit, settings, system, versions.
- #[Title] attributes → dynamic title() (attributes can't call __()).

Remaining (next commit): modals components + a few modal views, shell (command palette,
nav-item, server-item, toaster), and backend service messages (app/Services/*, OsProfile).
App is fully functional in German throughout; English covers the groups above.

Boot-verified: /login HTTP 200 in DE + EN, titles + <html lang> localized, locale switch works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:36:16 +02:00
boban 0f57074e6f chore(audit): full code sweep — fix open-redirect, drop dead code, clean docs
Full-codebase audit + automatic cleanup (no behavioural change for users).

Security
- ServerSwitcher: post-switch redirect is now reduced to a SAME-ORIGIN
  relative path. Rejects off-host referers, non-http(s) schemes
  (javascript:/data:), different ports (https://host:444/x -> /x), and
  protocol-relative tricks (//host, /\host). Closes a Referer open-redirect
  (Codex P2).
- EditCredential: a key passphrase is stored only for key auth, never for
  password auth.

Removed dead code
- FirewallService::status/allow/deny (+ orphaned clampPort)
- Server::auditEvents(), SshCredential::scopeActive(), unused 'bell' icon

Cleanups / improvements
- Files list + editor use a dedicated 'file' icon (was the audit icon)
- Import RuntimeException/Throwable instead of FQN; fix stale docblocks
  (FirewallService, HardeningService) and the Files delete comment
- Quote base64 in the sudo command path (defensive, consistent)
- Null-safe Auth::user()?->email in Settings; single domain() read in hasTls()

Release
- Bump 0.1.0 -> 0.1.1 + CHANGELOG entry

Verified: Pint clean, Codex review clean (no actionable regressions),
R12 browser check — 9 routes HTTP 200, 0 console errors, lazy pages loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:54:26 +02:00
boban a64bd39c6c feat(security): dashboard hardening, credential mgmt, system domain/TLS + channel, self-hardening
Security pivot — make server hardening, SSH access, firewall, dashboard domain/TLS
and the release channel controllable from the dashboard (no SSH needed), with guards
so a remote change can never lock the operator out.

Foundation
- ssh_credentials gains name + disabled_at + last_used_at; CredentialVault refuses a
  disabled credential. New key/value Setting model. FleetService::runPrivileged() /
  runPlain() — central sudo-aware exec (base64-wrapped sh -c), live-verified as root.

A · control-plane self-hardening
- SecurityHeaders middleware: env-aware CSP (allows the Vite dev origin in dev, strict
  same-origin in prod), X-Frame-Options DENY, nosniff, Referrer/Permissions-Policy,
  HSTS when secure. 2FA brute-force throttle (5/60s). install.sh sets
  SESSION_SAME_SITE=strict + EXPIRE_ON_CLOSE + SECURE_COOKIE (only behind TLS).

B · SSH credential management
- name/label on the access; a credential card on the server page with Bearbeiten /
  Sperren-Entsperren (kill-switch) / Löschen (R5), all audited.

C · server hardening from the dashboard (guards + confirmation)
- HardeningService (PermitRootLogin no, PasswordAuthentication no, fail2ban,
  unattended-upgrades) + FirewallService (UFW). HardeningAction modal previews the
  exact root commands before applying. GUARDS: refuse to disable password-login when
  Clusev itself logs in by password or no key exists; UFW opens the real sshd port +
  80/443 before enabling. Live-verified non-destructively (previews, the password
  guard refusing, ufw status read).

D+E · System page (/system)
- Dashboard Domain + Let's-Encrypt email (Setting) -> DeploymentService renders the
  matching Caddy site block (honest: stages the file + shows the reload command, never
  fakes TLS). Release channel (stable|beta|dev) configurable; Versions reads it.

Built largely by 4 parallel agents into disjoint files; shared files integrated + the
security-critical bits hardened by hand (the password-auth lock-out guard + env-aware CSP).

Verified (R12): /system + server detail + all 8 routes 200 / 0 console errors (CSP does
not break Livewire/Alpine/Vite); credential card + 5 hardening "Anwenden" buttons render;
the hardening modal opens with the command preview; System persists domain/channel +
renders valid Caddy config; runPrivileged runs as root + the vault refuses disabled creds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 02:25:23 +02:00
boban b3e5d3a024 feat: auth + 2FA (login wall + TOTP + forced onboarding)
- Custom Livewire auth (full-page, R1/R2): Login (rate-limited), TwoFactorChallenge,
  TwoFactorSetup (QR via pragmarx/google2fa-qrcode), PasswordChange.
- Guard: panel routes behind auth + EnsureSecurityOnboarded middleware — forces
  password rotation, then 2FA enrolment, before the panel unlocks.
- User: two_factor_secret (encrypted), two_factor_confirmed_at, must_change_password.
- Centered auth layout, POST logout, dynamic sidebar user + 2FA status.
- Seeded admin with a one-time password + must_change_password.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:57:02 +02:00