Commit Graph

393 Commits (07fe404ce96eb3b52b409efcb373d2a8abcf0f3d)

Author SHA1 Message Date
boban f0210c302b docs: spec — WireGuard panel gate + clusev wg CLI (SP1)
Optional host-level WireGuard gate for the Clusev panel: install.sh installs
wireguard-tools (off), a 'clusev wg setup/up/down/status/add-peer/remove-peer'
host CLI sets up wg0 + gates TCP 80/443 to the WG subnet via a dedicated
CLUSEV-WG-GATE iptables chain off DOCKER-USER, with 'clusev wg down' as the
escape hatch and SSH/WG-UDP never gated. Hardened after an adversarial review:
exact ordered firewall rules, persistence skips if wg0 is down (no reboot
brick), down isolates Clusev's chain, setup idempotency, precise install
wiring (CLUSEV_DIR exec + install_host_watchers). SP1 of 2; dashboard
status/traffic is SP2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:48:47 +02:00
boban 77d50d44e5 chore: release 0.9.27 — Anmeldeschutz (control-plane brute-force IP ban) 2026-06-20 18:58:24 +02:00
boban 6c81ea7fd1 harden(auth): fold IPv4-mapped IPv6, dedupe unbanAll, limit bans render, forged-token test 2026-06-20 18:53:52 +02:00
boban cabad0e770 harden(auth): scope whitelist purge to active bans, cover unbanAll, hoist ip 2026-06-20 18:20:19 +02:00
boban 7b4215d055 feat(auth): Anmeldeschutz settings tab + unban confirm flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:13:12 +02:00
boban 96f86e2761 feat(auth): clusev:unban CLI + host wrapper escape hatch 2026-06-20 18:07:43 +02:00
boban 01867dba09 harden(auth): capture login IP once, assert 2fa-failed audit 2026-06-20 18:06:11 +02:00
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 2936af82bd feat(auth): guests-only BlockBannedIp middleware + 403 page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:45:16 +02:00
boban 7645241a7a feat(auth): ValidIpOrCidr validation rule 2026-06-20 17:42:06 +02:00
boban df32275fd9 test(auth): BruteforceGuard record/ban/unban/cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:40:40 +02:00
boban 49d5c0aed3 harden(auth): BruteforceGuard review fixes (hit count, mask bounds, once-guard, mapped loopback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:39:01 +02:00
boban ce98fbdba0 feat(auth): BruteforceGuard with inet_pton CIDR exemptions 2026-06-20 17:29:00 +02:00
boban 1f7b06c576 feat(auth): BannedIp model + migration 2026-06-20 17:26:28 +02:00
boban 9756f9d463 docs: implementation plan — control-plane brute-force ban (Anmeldeschutz)
9-task TDD plan: BannedIp model, BruteforceGuard (inet_pton CIDR + cache),
ValidIpOrCidr rule, guests-only BlockBannedIp middleware + 403 page, Login/2FA
failure hooks + audit, clusev:unban CLI, Anmeldeschutz settings tab with R5
confirm-modal unban. Spec erratum: middleware is appended (not prepended) so
Auth::guest() resolves for the guests-only check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:20:52 +02:00
boban fe8c25c824 docs: spec — control-plane brute-force IP ban (Anmeldeschutz)
App-level persistent IP ban for Clusev's own login (distinct from the remote-fleet
Fail2banService): BannedIp model, BruteforceGuard service, guests-only BlockBannedIp
middleware, Login/2FA hooks + audit, a new Anmeldeschutz settings tab, clusev:unban CLI.
Hardened after an adversarial multi-lens review: documented IP trust model (Caddy sole
proxy, prod-only trustProxies), guests-only enforcement so an authed operator is never
locked out and can self-unban, current-IP self-whitelist, concrete cache strategy,
inet_pton CIDR matching, R5 confirm modals, errors.php for the 403 strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:05:52 +02:00
boban d58073086c chore: release 0.9.26 — 2FA backup code as its own view 2026-06-20 16:42:30 +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 7c1a32518d i18n(auth): drop orphaned 2FA recovery hint strings 2026-06-20 15:47:26 +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 4e32f76d1b polish(auth): distinct backup page title, autocomplete off, empty-code test
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:38:38 +02:00
boban e8bbe21b78 feat(auth): dedicated backup-code 2FA view + route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 15:31:32 +02:00
boban 0765293123 refactor(auth): tighten trait visibility, return type, @mixin
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:28:36 +02:00
boban 1fef9e61fe refactor(auth): extract CompletesTwoFactorChallenge trait 2026-06-20 15:24:09 +02:00
boban f581529164 i18n(auth): add 2FA backup-view strings 2026-06-20 15:21:52 +02:00
boban 8ec3b0b69a docs: implementation plan — 2FA backup-code own button + view
Task-by-task TDD plan for Spec 1: shared CompletesTwoFactorChallenge trait,
new TwoFactorBackup component + guest route + view, main-view redesign
(form gated to TOTP users, backup button, mount->backup redirect for
key-only-no-secure-context), i18n keys, and full R12/R15 verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:12:04 +02:00
boban 0f5d14a252 docs: spec — split 2FA backup code into its own button + view
Spec 1 of 2 for the 2FA challenge rework. Moves the backup (recovery) code
off the combined challenge screen into a dedicated /two-factor-challenge/backup
route + view, behind a 'Backup-Code verwenden' button; the main screen shows
only the primary factor. A key-only user without secure context (http + bare IP)
is redirected straight to the backup view. Shared rate-limit/login logic is
extracted into a CompletesTwoFactorChallenge trait so both views hit one bucket
set. IP-vs-domain factor routing is already correct (WebauthnService::available)
and unchanged. Spec 2 (auth-failure -> fail2ban hard IP ban) is out of scope here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:01:39 +02:00
boban b619b320c2 chore: release 0.9.25 — clusev host CLI + short commands + help tab-URL 2026-06-20 14:01:45 +02:00
boban 5967b7b7e5 fix: shell-escape the install dir when generating the clusev CLI (R15 Codex)
A checkout path containing shell syntax ($(), quotes) could otherwise inject
code into /usr/local/bin/clusev, which runs as root via 'sudo clusev update'.
Render the path through printf %q into an unquoted assignment; add a test that
a hostile path does not execute injected code.
2026-06-20 13:50:19 +02:00
boban 8d2b7087a9 feat: short clusev commands in versions panel, lang strings, MOTD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 23:18:25 +02:00
boban 437a22592b feat: help tab-URL + Commands/CLI topic + short command copy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 23:06:45 +02:00
boban 580c72a59d feat: clusev host CLI wrapper for short stack commands 2026-06-19 22:56:54 +02:00
boban 503f4d7fdf docs: implementation plan for clusev host CLI + short commands + help tab-URL 2026-06-19 22:45:05 +02:00
boban 83bd42f16a docs: spec for clusev host CLI, short commands, help tab-URL 2026-06-19 22:37:37 +02:00
boban 1d9dbdfc9b chore(release): v0.9.24 - in-panel help page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:22:35 +02:00
boban d1fb93ffe3 feat(help): remaining topics (updates, servers, sessions, email, audit, recovery)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:20:58 +02:00
boban d60f97413f feat(help): security topic - 2FA access paths + extension note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:16:37 +02:00
boban d0275db308 feat(help): domain-tls topic with the reverse-proxy setup guide
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:15:39 +02:00
boban 28c28df868 feat(help): in-panel help page scaffold + overview topic
New /help full-page Livewire component with left topic nav (like Settings),
bilingual content via per-locale Blade partials, sidebar + command-palette
entries (g h). First topic: Überblick / Overview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:14:15 +02:00
boban ca693219bf docs(plan): implementation plan for the in-panel help page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:06:08 +02:00
boban 60295a0aea docs(spec): in-panel Help page design + drop product name from external-proxy hint
Spec for a bilingual /help page (left topic nav like Settings) covering all
settings, a generic reverse-proxy setup guide, and the 2FA access-path note
(security key only over the HTTPS domain; backup code on the bare-IP/HTTP path;
TOTP works everywhere). Also generalize the external-TLS hint copy (no product
name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:02:25 +02:00
boban a709ef772c fix(assets): drop Vite preload hints (silence 'preloaded but not used')
With one CSS + one JS entry the <link rel=preload> sits next to the matching
stylesheet/script and adds no value, but it triggers Chrome's 'preloaded but
not used' console warning (amplified by DevTools 'Disable cache', which
double-fetches the CSS). Disable preload tag generation; stylesheet and script
still load normally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:50:54 +02:00
boban c020af4c09 fix(webauthn): add security-key hint so passkey managers defer
Even with authenticatorAttachment=cross-platform, Bitwarden's extension
intercepted navigator.credentials.create and offered to save a passkey. Add the
WebAuthn L3 'hints: [security-key]' to both registration and assertion options;
browsers and passkey managers that honor hints step aside and let the hardware
key prompt through. Injected into the client payload only (the session copy used
for server validation is unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:30:52 +02:00
boban 5ebee9b4a5 fix(webauthn): target a hardware security key (YubiKey), one-touch second factor
Registration set no authenticatorSelection, so the browser offered the
passkey-manager 'save passkey' flow (Bitwarden) instead of prompting for the
YubiKey. Now request a cross-platform, non-resident, presence-only credential:
authenticatorAttachment=cross-platform (roaming key, not a platform/manager
passkey), residentKey=discouraged (non-discoverable second factor — what keeps
passkey managers out), userVerification=discouraged (single touch, no PIN).
Login assertion also uses discouraged so it is: insert key, one tap, done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:23:01 +02:00
boban 5abf984df2 fix(webauthn): enable security keys behind an external TLS proxy
WebauthnService::available() keyed on request()->isSecure(), which is false in
external-proxy mode (the forwarded https scheme isn't trusted without
TRUSTED_PROXY_CIDR) — so hardware keys were locked even though the panel runs
over HTTPS on the domain. Gate on being reached via the active domain instead
(always HTTPS through the front door); the bare-IP recovery host stays locked.
A core security feature no longer depends on an optional proxy setting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:11:20 +02:00
boban a61feb1bef fix(deploy): apply Caddyfile changes on update (dir mount + recreate)
A single-file bind mount of the Caddyfile kept the original inode, so a
git pull that replaces the file never reached a running caddy, and up -d does
not recreate it for a content-only change — Caddy fixes (e.g. the 0.9.18
WebSocket fix) silently never deployed. Mount the docker/caddy directory
instead (reflects the live file) and force-recreate the caddy container on
every install/update so Caddyfile changes reliably take effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:57:42 +02:00
boban 92969eb0dc fix(tls): reverb WebSocket + honest TLS status behind an external proxy
- reverbClient() now returns wss://<domain>:443 when the active domain is being
  served (the HTTPS front door), instead of deriving wss://host:80 from a request
  that looks like plain HTTP:80 behind an external TLS proxy. Bare-IP recovery
  stays plain ws.
- Caddy no longer redirects the /app/* /apps/* WS paths to https when the upstream
  already terminated TLS (X-Forwarded-Proto: https) — that redirect broke the
  WebSocket in external-proxy mode.
- The 'TLS active' status card no longer claims Let's Encrypt issuance in
  external-proxy mode; it states TLS is provided by the upstream proxy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:49:25 +02:00
boban 4bcdafbeff fix(tls): force https URL generation for the active domain (external-proxy mode)
Behind an external TLS proxy, Clusev's own Caddy only honors the upstream's
X-Forwarded-Proto when TRUSTED_PROXY_CIDR is set. Without it the app saw http,
asset() emitted http:// URLs, and the HTTPS page's CSP ('self') blocked them —
the panel loaded with no CSS/JS over the domain. PanelScheme now forces the URL
root + scheme to https://<domain> whenever the active domain is being served,
regardless of the (possibly untrusted) request scheme. The bare-IP recovery
path returns earlier and stays on HTTP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:35:06 +02:00
boban 7d35f3abac fix(system): surface a failed restart sentinel write instead of a stuck state
Mirror the update button (0.9.15): requestRestart() now returns bool and
restartNow() shows an error toast (pointing at sudo ./update.sh) when the
sentinel can't be written, instead of a 'restarting' state that never resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:10:28 +02:00