Go to file
boban efecd26e31 feat(firewall): full UFW rule management + read-only firewalld view
The server-detail page gains a "Firewall-Regeln" panel — addressing "ich kann nur
aktivieren, sonst nichts": list rules, add, and delete, with hard SSH-lockout guards.

FirewallService:
- status(): one privileged read — installed/active, default policies, and the rule
  list. ufw rules come from `ufw show added` (add-syntax, works active OR inactive),
  defaults from /etc/default/ufw; a CLUSEV_FWREAD_OK sentinel + a "Status:" check
  distinguish a real read from a failed/empty one.
- addRule(): validated action/proto/port/from (proto before `from`, ufw grammar);
  refuses deny/reject on the SSH port or portless (would block all inbound).
- deleteRule(): whitelists the spec against rules ufw actually reported (injection-
  proof — a forged spec can't match), deletes by spec (race-free, no rule number),
  guards SSH-port allow rules incl. ranges and trailing comments, distinguishes
  not_found from a real deletion so the audit only records true deletions.

firewalld is READ-ONLY this release: status() reads the runtime state of every active
zone (ports/services/rich rules, zone-attributed), rule mutation is refused with a
German note. on/off + hardening (Phase A) still work.

UI: x-panel with default-policy badges, "+ Regel" (modal modals.firewall-rule), per-
rule delete via R5 ConfirmAction (audit deferred to the real outcome), graceful
read-error / not-installed / inactive / firewalld-read-only states.

Fix: the firewall row's tone variable was renamed $tone -> $ruleTone — the page-level
$tone is a closure used by the gauges/Volumes panel; reusing the name clobbered it and
500'd the page below the firewall block. R12 now confirms the full loaded page.

Default-policy EDITING was intentionally not exposed (highest lockout risk).
Pint clean; Codex review clean; R12 — server-detail HTTP 200, 0 console errors,
firewall panel + rules + volumes all render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:53:52 +02:00
app feat(firewall): full UFW rule management + read-only firewalld view 2026-06-13 19:53:52 +02:00
bootstrap chore(audit): full code sweep — fix open-redirect, drop dead code, clean docs 2026-06-13 12:54:26 +02:00
config chore(audit): full code sweep — fix open-redirect, drop dead code, clean docs 2026-06-13 12:54:26 +02:00
database feat(security): dashboard hardening, credential mgmt, system domain/TLS + channel, self-hardening 2026-06-13 02:25:23 +02:00
docker feat(ssh): wire real fleet data over SSH (FleetService + live poller) 2026-06-12 21:34:13 +02:00
docs fix: apply v1 UI review (a11y, contrast, live-chart robustness) 2026-06-12 06:16:52 +02:00
public feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
resources feat(firewall): full UFW rule management + read-only firewalld view 2026-06-13 19:53:52 +02:00
routes feat(security): dashboard hardening, credential mgmt, system domain/TLS + channel, self-hardening 2026-06-13 02:25:23 +02:00
storage feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
tests feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
.dockerignore feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
.editorconfig feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
.env.example fix(live): poll-driven metrics (chart + gauges), bigger donuts, drop fake fleet 2026-06-12 22:38:51 +02:00
.gitattributes feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
.gitignore feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
CHANGELOG.md chore(audit): full code sweep — fix open-redirect, drop dead code, clean docs 2026-06-13 12:54:26 +02:00
CLAUDE.md fix+feat(release/security): apt timeout, real version/release model, domain in DB not .env, R15 2026-06-13 03:06:35 +02:00
Dockerfile feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
README.md feat(deploy): install.sh + Caddy auto-TLS + prod hardening 2026-06-12 15:17:32 +02:00
artisan feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
composer.json feat: auth + 2FA (login wall + TOTP + forced onboarding) 2026-06-12 13:57:02 +02:00
composer.lock feat: auth + 2FA (login wall + TOTP + forced onboarding) 2026-06-12 13:57:02 +02:00
docker-compose.prod.yml feat(deploy): install.sh + Caddy auto-TLS + prod hardening 2026-06-12 15:17:32 +02:00
docker-compose.yml feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
handoff.md feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
install.sh feat(security): dashboard hardening, credential mgmt, system domain/TLS + channel, self-hardening 2026-06-13 02:25:23 +02:00
kickoff-prompt.md chore: project bootstrap — rules.md, CLAUDE.md, .gitignore 2026-06-11 23:12:58 +02:00
package-lock.json feat: live metrics over Reverb (mock emitter) 2026-06-12 01:23:10 +02:00
package.json feat: live metrics over Reverb (mock emitter) 2026-06-12 01:23:10 +02:00
phpunit.xml feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00
rules.md fix+feat(release/security): apt timeout, real version/release model, domain in DB not .env, R15 2026-06-13 03:06:35 +02:00
vite.config.js feat: scaffold — Dockerized Laravel 13 + Livewire 3 + Tailwind 4 + Reverb 2026-06-12 00:31:50 +02:00

README.md

Clusev

Self-hosted control panel to administer a fleet of Linux servers from one dashboard, agentless over SSH. Clusev is the control-plane (UI + orchestration); it talks to real servers with phpseclib (exec + SFTP) — it never reimplements daemons. Security-first: 2FA, encrypted SSH-credential vault, and a full audit log. Multi-server is free and never paywalled.

Status: v1 — dashboard/live metrics, systemd services, SFTP file manager, server details, auth + 2FA. UI copy is German; meta-docs are English.

Stack: Laravel 13 · Livewire 3 · Tailwind v4 · Reverb (realtime) · Redis · MariaDB · phpseclib3. Everything runs in Docker. See CLAUDE.md for architecture and rules.md for the hard conventions.


Development

The host needs only Docker (no PHP/Composer/Node). The dev app container runs php-fpm + nginx + Vite via supervisor; reverb, queue, mariadb, redis are their own services.

cp .env.example .env                 # then set DB_PASSWORD / DB_ROOT_PASSWORD
docker compose up -d --build         # app on :80, Vite HMR on :5173
docker compose run --rm --no-deps -u "${HOST_UID:-1002}:${HOST_GID:-1002}" app php artisan key:generate
docker compose exec app php artisan migrate --seed

Run any tooling inside the container, e.g. docker compose exec app php artisan make:livewire Servers/Show (class-based — never Volt).


Production install

One host, only Docker + a sudo user. Caddy is the single host-exposed service: auto-TLS when a domain is set, plain HTTP on the bare IP otherwise. install.sh is idempotent — it generates secrets once (never regenerates), brings up the prod stack, migrates, and creates the first admin with a one-time random password printed only on the terminal.

git clone https://git.bave.dev/boban/clusev.git && cd clusev
./install.sh

Prompts (non-interactive: set CLUSEV_DOMAIN / CLUSEV_ADMIN_EMAIL in the environment):

Domain (empty = access by IP over HTTP):   clusev.example.com
Admin e-mail (login + Let's Encrypt):       admin@example.com

The closing banner shows the URL + the one-time admin password. On first login Clusev forces a password change and 2FA enrolment before the panel unlocks.

One knob: APP_DOMAIN

APP_DOMAIN proxy URL Reverb
(empty) Caddy serves plain HTTP on APP_PORT http://<ip> ws://<ip>/app/*
clusev.example.com Caddy gets a Let's Encrypt cert, forces HTTPS https://… wss://…/app/*

SITE_ADDRESS, APP_URL, REVERB_* are derived from it by the installer — nothing hardcoded. Bare-IP mode serves 2FA/audit over cleartext HTTP; the installer warns loudly. Let's Encrypt needs publicly reachable 80/443 — a private (RFC1918) target needs a DNS-01 Caddy build instead.

Manual deploy (for operators who don't curl | bash)

docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec -u app app php artisan migrate --force
docker compose -f docker-compose.prod.yml exec -u app app php artisan clusev:install --email=admin@example.com

In-dashboard updates (signed intent file + host-side updater, digest-pinned, cosign-verified, 2FA-gated, with backup + rollback) are designed in docs/install-update-design.md and land in v1.x — they are intentionally not shipped yet.


License

AGPL core + commercial Pro modules (open-core). Multi-server fleet management is always free.