Jabali Panel is a Laravel 12 + Filament v5 web hosting control panel with privileged operations delegated to a PHP agent daemon.
User Browser
↓
nginx (port 80, 443)
↓
FrankenPHP Panel (port 8443, PANEL_PORT env configurable)
├── Systemd service: TimeoutStopSec=10, KillMode=mixed (graceful shutdown)
├── Filament Admin UI (/jabali-admin/)
├── Filament User UI (/jabali-panel/)
└── REST API endpoints
↓
AgentClient → Unix Socket (/var/run/jabali-agent.sock)
↓
jabali-agent (PHP binary, bin/jabali-agent)
├── File system operations (owned by root)
├── Service management
├── SSL certificate handling
├── User/domain operations
└── System commands (escapeshellarg, proc_open for safety)
Process: Laravel app running on FrankenPHP, listening on port 8443 (configurable via PANEL_PORT environment variable). This is independent of nginx, which proxies requests from ports 80/443.
Guards:
adminguard at/jabali-admin/for administrative accesswebguard at/jabali-panel/for user access- Both guards share the same
usersprovider
Entry Points:
bootstrap/app.php— middleware, exception handling, routing configuration (Laravel 12 streamlined structure)routes/web.php— Filament route registrationroutes/api.php— API endpoints for agent communication
Key Directories:
app/Filament/Admin/Pages/— admin panel pages (Services, SSL, Users, System, etc.)app/Filament/Jabali/Pages/— user panel pages (Dashboard, Domains, Backups, Emails, etc.)app/Services/Agent/AgentClient— all privileged operations route through this service
Binary: bin/jabali-agent (PHP standalone executable, ~21k lines)
Communication: Unix socket at /var/run/jabali-agent.sock. Panel communicates via AgentClient::call(), passing JSON-encoded command and arguments. Returns JSON response.
Addons: Agent loads addon route handlers from /etc/jabali/agent.d/*.php. Addons return action→handler arrays for custom operations (e.g., jabali-backup plugin).
Responsibility: All operations requiring root privileges:
- File ownership/permissions
- Service start/stop/restart
- SSL certificate issuance and installation
- User home directory creation/deletion
- Cron job management
- Shell command execution with proper escaping (
escapeshellarg()) - Password management via
proc_open()tochpasswd
Safety:
- All shell arguments escaped with
escapeshellarg() - Passwords piped via
proc_open()(not via command line) - File paths validated with
PathSanitizer::clean()to prevent traversal - Cron commands validated against allowlist via
CronCommandValidator::validate() - Service names checked against
$allowedServicesglobal array
Response Format: JSON with success boolean, result (data), and optional error string.
Role: Only mail backend. Supports SMTP, IMAP, JMAP, ManageSieve.
Configuration: MySQL-backed storage for accounts, domains, settings.
Management:
jabali mail:*commands (create, delete, password, quota, list, log, queue, queue-retry, queue-delete)- Mailbox quotas stored in database
- Log viewing via
mail:log(filters by domain, type likesmtpordelivery)
Integration: Panel syncs mailbox operations to Stalwart, reads statistics and logs from database and log files.
Role: Authoritative DNS server with REST API and MySQL backend. Supports DNSSEC.
Configuration: MySQL stores zones and records. REST API at http://localhost:8081 (Poweradmin credentials).
Management:
jabali dns:*commands (list, records, add, delete-record, sync)- Zone files synced from PowerDNS API
- Records created/updated/deleted via API
dns:syncrefreshes zone from API (useful after bulk imports)
Integration: Panel manages DNS records for domains and subdomains, including mail.$domain (MX records).
MariaDB: Always enabled, supports CRUD operations, user management, privilege assignment, backup (mysqldump), and restore (file upload).
PostgreSQL: Optional. Admins enable/disable from Server Settings > Databases tab. Supports service control, database CRUD, role/privilege management (CREATE, CONNECT, TEMPORARY, ALL), backup (pg_dump), restore (file upload), and database info queries. Appears in ServicesTableWidget when enabled.
Pool Management: Server Settings > PHP-FPM tab provides configurable defaults:
- Process Manager type: Dynamic (adjusts based on load), Static (fixed workers), or On Demand (spawns only when needed)
- Conditional fields: Dynamic mode shows start/min/max spare server controls; static and on-demand modes hide these
- Per-pool settings: Max processes, max requests, memory limit, open files limit, process priority, request timeout
- Application scope: Settings apply to new user pools by default, or can be applied to all existing pools via "Apply to All" action
Role: Per-domain nginx configuration injected into vhost server blocks.
Management:
- Users configure directives from the Domains page "Nginx Directives" action
- Admin validation: only blocks dangerous directives (load_module, lua, perl, js)
- User validation: allowlist of safe directives (rewrite, return, try_files, add_header, proxy_pass, location, etc.)
- Agent applies directives to vhost config and runs
nginx -tbefore reload - Path traversal (
../) blocked in all directive values
Implementation: NginxDirectiveValidator service, ManagesNginxDirectives Filament concern, custom_nginx_directives column on domains table.
Status: Fully removed. Rebuilt as standalone tool (jabali-backup) with agent addon architecture. See ADR-0004.
Role: SSL/TLS certificate issuance and renewal using webroot mode.
Configuration: Webroot at domain document root, certificates stored in /etc/letsencrypt/live/{domain}/.
Management:
jabali ssl:*commands (issue, renew, check, list, status, panel, panel-issue)- Automatic renewal via systemd timer
- Panel certificate separate (for FrankenPHP itself)
- Includes
mail.$domainas additional SAN
Integration: Panel issues certs, checks expiration, renews on demand, auto-deploys to web serving.
Role: Real-time web traffic analytics. Daemon mode with WebSocket updates.
Configuration: Reads nginx access logs, generates HTML report. WebSocket server for live updates.
Integration: Panel displays bandwidth and visitor statistics on user domains page.
Role: Live file browsing and management via Filament page.
Two components:
App\FileBrowser\Pages\FileBrowser— full Filament page with all actions (upload, edit, rename, permissions, trash, etc.)App\FileBrowser\Components\FileBrowserWidget— embeddable Livewire component for nesting inside other pages (e.g. backup restore modal)
Feature control:
$readOnly = truedisables all write operations (upload, edit, rename, trash, permissions, extract, newFile, newFolder, move, copy)$disabledFeaturesarray controls individual actions:['upload', 'edit', 'trash', 'rename', 'view', 'download', ...]$selectable = true(widget only) shows row checkboxes for file selection- All checks enforced server-side via Filament's action resolution — not just UI hiding
Embedding the widget:
@livewire('file-browser-widget', [
'adapterClass' => ResticSnapshotAdapter::class,
'adapterConfig' => ['username' => 'shuki', 'snapshot_id' => 'a9ee58cd'],
'readOnly' => true,
'selectable' => true,
])Adapter contract: Custom adapters must implement FileBrowserAdapter and provide a static fromConfig(array $config): static method for Livewire re-hydration (adapters are PHP objects that can't survive between requests).
Integration: Full page accessible from user/admin panel. Widget embeddable by addons (e.g. jabali-backup restore flow).
Binary: Next.js JMAP client at /opt/bulwark, served via nginx proxy at /webmail/.
Port: 3000 (internal), proxied by nginx on user-facing port 443.
Authentication: SSO via token file at /var/lib/jabali/sso-tokens/{token}. Custom endpoint PUT /webmail/api/auth/session accepts token + username, returns session cookie.
Patches: Applied during install.sh via patch_bulwark():
- basePath configuration (running under
/webmail/) - SSO integration (token file reading)
- auth-store modifications (session handling)
- proxy.ts configuration (upstream headers)
Update: upgrade_bulwark() rebuilds from source, applies patches, validates version marker.
Daemon: jabali-security (separate repo, PHP service).
Features:
- Brute-force protection (IP-based rate limiting on login)
- WAF (Web Application Firewall)
- Malware scanning (on-demand via
wp:scanfor WordPress) - CrowdSec integration (optional, for threat intelligence)
Integration: Filament plugin provides admin dashboard, WordPress plugin hooks into scan workflow.
Technology: systemd-nspawn containers (lightweight namespaced environments).
Purpose: SSH shell access sandboxed per user, preventing lateral movement.
Architecture:
- One container per user with user-owned filesystem
- SSH login via
jabali-shellset as user's login shell (viachsh), not ForceCommand in sshd_config jabali-shelluses login shell args and TTY detection for VS Code Remote SSH compatibilityjabali-shellsets C.UTF-8 locale fallback to prevent setlocale warnings and supports container nsenter- Container idle timeout via systemd timer (
jabali-container-idle-check.sh) - Web serving (PHP-FPM) runs on host, not in container
Integration: ssh_shell_enabled toggle on hosting packages, auto-enabled on user creation.
Technology: systemd slices backed by cgroup v2 unified hierarchy.
Purpose: Per-user resource caps enforced at the kernel level. Prevents any single user from consuming all server resources.
Limits:
- CPU —
CPUQuotapercentage of one core (100 = 1 core, 200 = 2 cores) - Memory —
MemoryMaxhard cap +MemoryHighsoft limit at 90% (triggers reclaim before OOM kill) - I/O —
IOReadBandwidthMax/IOWriteBandwidthMaxin MB/s per block device - Processes —
TasksMaxcaps concurrent tasks (FPM workers + SSH sessions + cron)
Slice hierarchy: /sys/fs/cgroup/jabali.slice/jabali-user.slice/jabali-user-{username}.slice/
Configuration: Set via Hosting Packages (defaults) with per-user overrides. Applied on user create/edit, synced when package changes.
Enforcement:
- PHP-FPM workers moved into user's slice by agent on apply and by health monitor every ~90s (catches respawns)
- SSH sessions (nspawn and standard) move themselves into the slice on login via
jabali-shell.sh - Kernel throttles CPU, kills processes exceeding memory, rejects fork() past process limit
CLI: jabali cgroup check, jabali cgroup apply <user>, jabali cgroup status <user>, jabali cgroup apply --all
Agent handlers: cgroup.check, cgroup.apply, cgroup.remove, cgroup.status
Purpose: Per-user request rate and connection limits at the nginx layer. Protects against traffic floods before requests reach PHP-FPM.
Implementation:
- Tiered
limit_req_zonedefinitions in/etc/nginx/conf.d/jabali-ratelimit.conf(10, 30, 50, 100 req/s zones) limit_conn_zonefor concurrent connection limits per IP- Directives injected into vhost server blocks during
nginx:regenerate - Burst set to 50% of rate with
nodelay; returns HTTP 429 when exceeded
Configuration: nginx_req_per_sec and nginx_connections on Hosting Packages and Users (same override pattern as cgroup limits).
Technology: Laravel queue system with database driver (default), or Redis for high-volume.
Jobs:
- Backup creation (
CreateBackupJob) - SSL certificate issuance (
IssueCertificateJob) - User deletion (cleanup of domains, emails, databases)
- WordPress operations (update, scan, import)
Processing: Supervisor or systemd service runs queue:work daemon.
/home/shuki/projects/jabali/
├── app/
│ ├── Console/Commands/Cli/ # 86 CLI command classes
│ ├── Filament/Admin/Pages/ # Admin panel pages
│ ├── Filament/Admin/Resources/ # Admin resources (Users, Domains, etc.)
│ ├── Filament/Jabali/Pages/ # User panel pages
│ ├── Filament/Jabali/Resources/ # User resources
│ ├── Http/Controllers/ # API controllers
│ ├── Http/Requests/ # Form request validation
│ ├── Jobs/ # Queue jobs
│ ├── Models/ # Eloquent models (User, Domain, Backup, etc.)
│ ├── Services/Agent/AgentClient.php # Agent communication
│ ├── Services/Agent/AgentRequest.php # Request builder
│ ├── Services/Auth/ # Authentication services
│ └── Providers/ # Service providers
├── bin/
│ ├── jabali # CLI entry point
│ └── jabali-agent # Privileged agent binary
├── bootstrap/
│ └── app.php # Application configuration (Laravel 12)
├── config/
│ ├── app.php, cache.php, database.php, etc. # Configuration files
│ └── services.php # Third-party service configs
├── database/
│ ├── migrations/ # Database schema
│ └── factories/ # Model factories
├── docs/
│ ├── cli-reference.md
│ ├── architecture.md # This file
│ ├── mail.md
│ ├── ssl.md
│ ├── security.md
│ ├── dns.md
│ ├── one-time-login.md
│ └── diagnostic-logs.md
├── resources/
│ ├── views/ # Blade templates
│ ├── wordpress/jabali-cache/ # WordPress cache plugin
│ └── css/, js/ # Frontend assets
├── routes/
│ ├── web.php # Filament routes
│ └── api.php # API routes
├── storage/
│ ├── logs/ # Log files
│ └── uploads/ # User uploads (symlinked to public/storage)
├── stubs/
│ ├── jabali-shell.sh # SSH login shell (login shell via chsh, login args, TTY detection, nsenter, locale)
│ ├── jabali-container-idle-check.sh # Container idle timeout (systemd timer)
│ └── [config templates] # nginx, PHP-FPM, service templates (sshd_config before Match blocks)
├── tests/
│ ├── Feature/ # Feature tests (Livewire, API)
│ ├── Unit/ # Unit tests
│ └── Pest.php (or phpunit.xml) # Test configuration
├── .env.example # Environment template
├── composer.json # PHP dependencies
├── package.json # NPM dependencies
├── artisan # Laravel CLI
├── install.sh # Server installation script
└── README.md
- Admin clicks "Create User" in
/jabali-admin/ - Filament form posts to
CreateUserAction - Action validates input, creates User model record
- Agent command dispatched:
createUser(username, password, email, is_admin) - Agent creates Unix user, home directory, SSH key, database user
- Panel stores user record with agent response data
- If disk quota set, agent applies filesystem quota
- If resource limits set (CPU/memory/IO/processes), agent creates systemd cgroup slice
- If SSH isolation mode enabled, container or sandbox configured
- User clicks "Add Domain" in
/jabali-panel/ - Filament form posts to domain creation action
- Panel validates domain ownership (DNS TXT record or email verification)
- Agent creates vhost directory, symlinks to user home
- Agent adds DNS zone to PowerDNS
- Panel creates Domain model, associates with User and SSL certificate request
- Certbot automatically issues certificate via webroot
- Panel detects new domain or expiring certificate
- Dispatches
IssueCertificateJob - Job calls agent
sslInstallCertificate(domain) - Agent calls Certbot with webroot, stores cert in
/etc/letsencrypt/live/{domain}/ - Agent updates nginx vhost config with cert paths
- Panel stores certificate metadata (expiration, issuer)
- Auto-renewal via systemd timer