Security model
The design goal is that a compromised panel web application cannot become a compromised server, and that one client cannot reach another client's data.
Two processes, one narrow interface
minipanel-webruns as the unprivileged userminipanel, listens on 127.0.0.1:8081 behind Apache (port 8443) and holds no privileges: it cannot write system files, run commands or read client homes. It only keeps browser sessions (in its own database, session ids stored hashed) and forwards requests.minipaneldruns as root and performs every change. It accepts connections only on a Unix socket ownedroot:minipaneland identifies the caller by its kernel-reported uid: root callers (panelctl) may run admin actions, theminipaneluser may run client actions, everyone else is refused.- The interface is a fixed list of named actions
(
domain.add,mail.mailbox.create,file.upload, …). There is no "run a command" or "write a file" action, so even full control of the web app only allows what a client could do through the panel — for that client's own account. - The helper re-validates every input against the rules in the specification (names, domains, paths, schedules, addresses) regardless of what the web app already checked, and checks that the account owns the resource being changed.
No shell, no templates with holes
- Programs are run with explicit argument lists and absolute paths, a minimal environment, and never through a shell.
- Every configuration file is rendered from a template to a temporary
file, checked with the service's own validator
(
apachectl configtest,php-fpm -t,postfix check,doveconf,sshd -t), and only then atomically moved into place and the service reloaded. If the check fails the previous file stays and the account is marked "not applied" for the admin. - Configuration is always regenerated from the state database
(
panelctl rebuild), so a bad manual edit can never persist.
Client isolation
- Each account is a Unix user (uid 2000+) with a home of mode
0710; the web server only gets a traversal ACL to the document roots. Another account's PHP cannot read it. - PHP runs in a per-account PHP-FPM pool as that
user; the pool's socket is only connectable by Apache
(
www-data, mode 0660). Uploads, sessions and temp files go to the account's owntmp/. - The file manager performs every operation in a
child process that has dropped privileges to the account's uid/gid
(supplementary groups cleared, no-new-privileges) and works inside a Go
os.Rootconfined to the home. Symlinks are never followed out of the home, archives are checked for path traversal, links and compression bombs before extraction. - Mail: mailboxes are virtual (no Unix users), Maildirs are owned by the account, delivery runs as the account's uid, and an authenticated mailbox may only send as its own address.
- Databases: users are
<name>@localhostonly; identifiers are validated and quoted, andGRANTescapes the_/%wildcards so a grant onacme_shopcan never match another account's database. - SSH/SFTP: access is by group membership
(
minipanel-sftpwithForceCommand internal-sftp, no TTY, no forwarding;minipanel-sshfor a real shell)./etc/cron.allowcontains only root, so shell users cannot edit crontabs behind the panel's back. - Domains: a client cannot add a domain that another account owns, a subdomain of it, or a subdomain of the server hostname.
Admin web UI
The admin UI (port 2087) is a separate process running as
minipanel-admin, a third caller the helper recognises by
uid and restricts to an explicit allowlist: account
list/show/create/modify/suspend/unsuspend/password reset/quota, the
client actions inside an account ("Manage"), certificate retry, backup
run/list, health checks and the audit log. Account termination is on the
list since 1.7 but the helper demands a valid authenticator code of the
admin for it and always writes a 30-day archive first. Restores,
rebuilds and admin management are not on the list and remain
panelctl-only. Admin logins live in their own table,
require TOTP, share the lockout rules below, and every action carries
the admin's name into the audit log.
Webmail and phpMyAdmin
Roundcube and phpMyAdmin run in their own PHP-FPM pool as the Unix
user minipanel-apps, which is deliberately
not in the www-data group (that group can
connect to every client's PHP socket): it cannot read client homes,
cannot reach client PHP pools and cannot reach the helper socket. They
are served only on the server hostname, never on client domains.
phpMyAdmin allows cookie logins of client database users only (root
refused, no arbitrary servers, system databases hidden); both apps'
failed logins feed fail2ban with the real client address.
Authentication
- Panel passwords are 12–128 characters, checked against a list of common passwords, and stored as Argon2id. The same password is the account's SFTP/SSH password (kept in sync via PAM).
- Lockouts: 5 failed logins per 15 minutes per IP (web app) and per account (helper, so it applies to any caller); fail2ban bans repeat offenders at the firewall.
- Optional TOTP two-factor authentication: the secret and the recovery codes live only in the helper's database; the web app forwards codes and never sees the secret. Codes are accepted once, recovery codes are single-use and stored hashed, and failed second-factor attempts count towards the same lockout.
- Sessions: random 256-bit ids,
Secure; HttpOnly; SameSite=Strictcookies, 8-hour absolute and 1-hour idle timeouts, id rotated at login; a CSRF token on every state-changing form; a strict Content-Security-Policy with no inline scripts. - There is no self-service password reset; the admin resets with
panelctl account passwd.
Audit and secrets
- Every state-changing action is written to
/var/log/minipanel/audit.logwith caller, account, action, target and result — never parameters, never passwords. - Passwords and hashes never appear in logs, error messages or the web app's memory beyond the request that needs them. Generated database passwords and 2FA secrets are shown exactly once.
Network exposure
Only these ports are open: 22 (SSH), 25 (SMTP in), 80/443 (sites), 465/587 (mail submission), 993/995 (IMAPS/POP3S), 8443 (panel). MariaDB, PHP-FPM, Rspamd, Redis and the helper socket are local only.