Batch of user-reported fixes + two findings from the nav-split audit.
Bugs:
- Alerts never e-mailed. AppServiceProvider::applyMailSettings() ran only
at boot; the long-lived QUEUE worker booted once, so SMTP configured
after worker start (the normal case) meant queued alert/reset mail used
the stale/log mailer. Re-apply the DB SMTP config before every job
(Queue::before) so queued mail always sends with the current settings.
- SMTP could not be fully reset. Add Email::clearConfig() (+ a Reset
button + Setting::forget) that removes every mail_* setting, reverting
the panel to the safe default (log) mailer.
- New cert endpoint / uptime check showed no validity/status until a
manual refresh. Certs\Index::addEndpoint + Health\Index::addCheck now
probe the just-added row immediately.
Audit findings (096ba3c..HEAD):
- HIGH stored/DOM XSS: the per-server Terminal tab rendered the server
name inside an Alpine x-text JS sink (`x-text="title || '{{ name }}'"`);
Blade escaping does not protect that context. Move the fallback to a
data-attribute read as a plain string (server-terminal + the host
terminal for consistency).
- LOW: /terminal is now admin-only, but the command-palette `g t` leader
key still sent every role there → 403. Drop it from the static map;
render it as a role-filtered nav entry (admins only).
UI:
- status-pill is h-8 now (the small-button height) so a status pill and
the action buttons in the same row line up (Services/Certs/Docker/…).
748 tests (new clearConfig test); R12-verified (uniform pill/button
heights). Marketing docs at ~/clusev-site reworded to remove the
contradictory domain/CLUSEV_DOMAIN install note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>