XAXA Server control panel

Guides

Security Model

XAXA can change anything on your server, so it is designed to keep a bug in the panel — or in one of your sites — from becoming a full server compromise.

The panel never runs as root

  • The web UI runs as the unprivileged xaxa user in its own PHP-FPM pool, behind nginx on port 8443.
  • The panel code in /opt/xaxa is owned by root and read-only for xaxa. Only storage/ and bootstrap/cache/ are writable; panel data lives in /var/lib/xaxa/panel.
  • The panel's PHP-FPM service runs with ProtectSystem=true instead of the package default full: /usr, /boot and /efi stay read-only and the other hardening options are kept, while /etc becomes writable for the root agent (started through sudo), so it can manage users, groups and service configuration. Panel and site users are still limited by file permissions; this does not give them write access to /etc.

One door to root: the agent

Privileged work goes through /usr/local/sbin/xaxa-agent, a single root-owned PHP script. The sudoers rule allows xaxa to run exactly this file and nothing else:

xaxa ALL=(root) NOPASSWD: /usr/local/sbin/xaxa-agent

The agent:

  • does not load Laravel or any file the panel can write,

  • has a fixed list of actions (site.create, ssl.issue, db.create, …),

  • re-validates every input itself: domain and user-name patterns, PHP version allow-list, port ranges, IP/CIDR syntax, cron expressions, password character sets,

  • only touches Linux users in the xaxa-sites group — never root or system users,

  • runs commands as argument arrays with proc_open, never through a shell,

  • never writes as root into a site user's home directory (it uses runuser so symlink tricks can only hurt the site user),

  • rejects dangerous nginx directives in vhosts (module loading, scripting, includes outside the site's folder, log paths outside /var/log/nginx, certificate or password files outside the site's own folders) and keeps every vhost inside its own site: document roots in the site's home, PHP only through the site's own pool, server_name limited to the site's domains, ports 80/443 only, no .. or request values in file paths (try_files, index, rewrite, error_page). The raw vhost editor is admin-only, because nginx can read every site's files. Vhosts are tokenized exactly the way nginx reads them — quotes, escapes and # comments included — so a directive cannot be hidden inside a string or comment.

  • never puts a secret on a command line (every local user can read process arguments): passwords go to child processes through their environment, stdin or private files.

Every call is written to /var/log/xaxa-agent.log (root only): time, action, calling user, result and duration. Passwords, keys, file contents and environment values are masked. Admins can read it under Events → Root agent.

On an installed server you can read the whole agent at /usr/local/sbin/xaxa-agent.

Sites are isolated from each other

  • Each site has its own Linux user and 750 home directory.
  • PHP sites run in a per-site PHP-FPM pool as that user, isolated by Linux permissions (750 home directories, 0711 /home).
  • Node.js and Python apps run as systemd services under the site user with NoNewPrivileges, ProtectSystem=full and a private /tmp.
  • Redis is shared, but each site can get its own Redis user that only sees keys starting with its name; new servers require such a user (see Databases → Redis).
  • The SMTP login in /etc/msmtprc is readable by every site user so sites can send mail. Give the sites a login of their own (Instance → Mail → Site login), so the panel's alerts are sent with a login no site can read; otherwise use a credential meant only for sending.
  • A site's Linux user also names its Redis user and its xaxa-<user> service, PHP pool and files, so a new site cannot use a name the server already has (Redis default, xaxa-admin, the panel's own workers and pools).
  • No site can take the panel's domain, and the panel cannot take the domain of a site.

phpMyAdmin and the web terminal

  • Both run on port 2083, a separate browser origin from the panel: a script running inside phpMyAdmin (for example through a bug that shows database content as HTML) cannot read panel pages or their CSRF token.
  • Every request goes through the panel session gate. phpMyAdmin also needs a temporary database login that the same account opened from the panel (one database, 60 minutes), and only those temporary logins can sign in there.
  • The terminal program (ttyd) runs as the site user, not root, on a socket only nginx can open; it accepts WebSockets only from its own origin.
  • Signing out, changing the password, losing a site or a deleted account closes open terminals and drops temporary database logins at once.
  • phpMyAdmin runs as its own user, outside the www-data group, so it cannot connect to the PHP-FPM pools of the panel or of sites.

Updates

xaxa update runs the installer from inside the release package after checking its minisign signature with the public key saved on the server at install time. The installer on the website is used only for the first install.

  • nginx logs are written to root-owned /var/log/nginx/xaxa, not into the site's home.
  • Every mysql client the agent runs as root has LOAD DATA LOCAL turned off, so an imported or restored dump cannot read a file of the server.

Accounts and sessions

  • The first admin is created via a one-time setup link printed by the installer — no "first visitor becomes admin" window.
  • Passwords are hashed with bcrypt; TOTP two-factor authentication is built in.
  • Login and 2FA attempts are rate-limited per IP and per account (10 wrong passwords or 5 wrong codes in 15 minutes). The panel trusts no proxy header, so a request from the server itself cannot pose as another IP. Actions that ask for the password again (changing it, turning 2FA off, new recovery codes, API tokens, signing out sessions, deleting a site) allow 10 tries a minute per account. Failed attempts are written to storage/logs/auth.log. A fail2ban jail (xaxa-panel) bans an IP on port 8443 and 443 (the panel domain) after repeated failures.
  • Account & 2FA → Sessions lists the signed-in browsers with their address and sign-in time. Sign out one of them, or all others, with your password. Failed sign-ins of the last 30 days are listed there too, and a sign-in from an address the account has not used before sends an alert.
  • API tokens can expire (30/90/365 days), can be limited to deployments and to some sites, and need the password when they can change things or never expire. Deleting an account deletes its tokens.
  • The alert webhook URL and its signing secret are stored encrypted.
  • Every change is recorded in the audit log.
  • The panel's access log masks secret links (the one-time setup link and deploy webhook URLs).
  • SSH, database and basic-auth passwords are shown once and not stored by the panel. Credentials from a one-click install or a clone (database user, WordPress admin) stay on the site's Overview, encrypted with the app key, until you dismiss them. The S3 secret key lives only in a root-only rclone config, and off-site copies are encrypted before upload with a password shown once.

Server hardening

The installer enables UFW (deny incoming by default), fail2ban for SSH and the panel login, and unattended security upgrades — including PHP packages from the Sury/Ondřej repository, which the default configuration would skip. Node.js and Python versions receive patch updates weekly.

What admins can do

Panel admins are effectively root: they can, for example, restore backups over a site or open firewall ports. Protect admin accounts with 2FA and give teammates the user role when they only need to manage sites.

Reporting a vulnerability

Please report security issues privately to security@xaxa.com.tr. Do not post them publicly.

Found a mistake or need help? support@xaxa.com.tr