Release Notes
iNetPanel Changelog
Every iNetPanel release — new features, fixes, and security updates — sourced from GitHub.
v1.27.3 — Hotfix: the weekly Cloudflare RemoteIP refresh deleted its o
Hotfix: the weekly Cloudflare RemoteIP refresh deleted its own Apache config every Monday. /etc/cron.d files do not inherit PATH from /etc/crontab, so apache2ctl and the a2* helpers (all in /usr/sbin) were unreachable under cron; the script read that as its own config being invalid and removed it, leaving a dangling symlink. After the next Apache reload mod_remoteip went inert, every tunnelled request appeared to come from 127.0.0.1, and because fail2ban's ignoreip covers 127.0.0.1/8 that made all tunnelled traffic invisible to every jail. The script now sets PATH itself, verifies its tools before writing anything, checks install's exit status, and no longer assumes a failing configtest is its own fault. Recommended for every install behind a Cloudflare Tunnel.
v1.27.2 — Hotfix: restores admin hook scripts, which 1.27.1 silently d
Hotfix: restores admin hook scripts, which 1.27.1 silently disabled. 1.27.1 repointed the hook sudoers grant out of /tmp but left three callers creating their script there, so the sudo invocation no longer matched and every add_domain/delete_domain hook failed to run. Fails closed, so there is no security impact, and installs with no hook script enabled are unaffected. Upgrade immediately if you use hook scripts.
v1.27.1 — Security: privileged-command input is no longer staged in wo
Security: privileged-command input is no longer staged in world-writable /tmp. Same root cause as GHSA-mjmx-xpqq-p2h8 in a different set of sinks — a local hosting tenant could plant fixed, predictable filenames in /tmp and have a root cp follow a symlink (arbitrary root file read) or a root bash execute their script (arbitrary root code execution). All staging moves to /var/lib/inetpanel/staging, owner-only 0700, with unlink-then-O_EXCL writes. Upgrade recommended for any server with untrusted hosting accounts.
v1.27.0 — External exposure scanning via qsa.sh: run a real external p
External exposure scanning via qsa.sh: run a real external port and vulnerability scan of your public IP from the Firewall page or the CLI, with change monitoring and Discord/Slack webhook alerts. Also fixes CLI deployment under systemd ProtectSystem, and makes update failures report honestly instead of always claiming success.
v1.26.2 — **Hotfix. Update immediately if you are on 1.26.1.**
Hotfix. Update immediately if you are on 1.26.1.
Login returned HTTP 500 on 1.26.1
1.26.1 broke the panel on every install. Auth::clientIp() — added in that release to record the real client IP for logging and fail2ban — called itself as its own final fallback instead of reading REMOTE_ADDR. Infinite recursion, stack exhaustion, HTTP 500.
It only triggered when a request arrived with no CF-Connecting-IP and no X-Forwarded-For header, which is every direct request and every request after login. Requests arriving through a Cloudflare Tunnel took an earlier branch and returned before reaching it, which is why it was not caught before release.
One line. No configuration change, no data affected, nothing to undo.
If you are stuck on 1.26.1
The nightly auto-update will apply this. To fix it now from a root shell:
inetp panel_update
If the panel is unreachable and you would rather patch by hand, in /var/www/inetpanel/TiCore/Auth.php change the line inside clientIp():
$ra = self::clientIp(); // broken
$ra = $_SERVER['REMOTE_ADDR'] ?? ''; // correct
then systemctl reload php8.5-fpm.
Apologies
This should not have shipped. The security fixes in 1.26.1 were tested, but this helper's *output* was verified without ever calling it on the one code path that recursed. 1.26.1 remains the release that fixes the privilege escalation — please update to 1.26.2 rather than rolling back to 1.26.0.
v1.26.1 — **Security release. Update promptly** — one finding allows
Security release. Update promptly — one finding allows a hosting tenant to gain root on the host.
Local privilege escalation via .htaccess/.htpasswd (GHSA-mjmx-xpqq-p2h8)
Severity: High (CVSS 8.8). An authenticated hosting tenant — the lowest privilege the panel issues — could obtain arbitrary root file read, write and chown on the host.
The account portal's .htaccess/.htpasswd manager validated the *directory* of the target path but appended the filename afterwards, unchecked. Because a tenant controls their own document root over SFTP, replacing .htaccess with a symlink made the panel's root cp/cat/chown follow it — reading /etc/shadow, writing /etc/cron.d/, or taking ownership of any root-owned file.
All such access now goes through a single root helper. Callers pass an account and a *relative* directory, never a path; the helper re-derives the document root from the panel database, re-checks domain ownership, and opens the file with O_NOFOLLOW so the kernel refuses symlinks. Eight broad sudo grants over /home/* were removed and replaced with one narrow grant.
Reported privately. No action is required beyond updating — the fix applies automatically.
phpMyAdmin sign-on credentials were world-readable
The temporary sign-on token file held a plaintext database password — the MySQL root password on the admin route — at mode 0644. It is unlinked when phpMyAdmin picks it up, so the exposure was abandoned hand-offs, which lingered indefinitely and were readable by any local user, including every hosting tenant. Now created 0600 before any content is written, with stale tokens swept.
Hosted sites saw the wrong client IP for every request
Behind Cloudflare Tunnel, cloudflared connects over loopback, so REMOTE_ADDR was 127.0.0.1 for every request on every hosted site. Consequences:
- per-IP rate limiting in any hosted app was one global bucket
- access logs could not be used for abuse investigation
- the panel's own fail2ban jail recorded failed logins as loopback — which
jail.localignores — so it logged every brute-force attempt and could ban none - apps compensating by parsing
X-Forwarded-Forgot it wrong, because Cloudflare *appends* to that header: the left-most entry is attacker-controlled
inetp cf_remoteip now configures mod_remoteip with Cloudflare's published ranges, refreshed weekly. Hosted applications need no changes — REMOTE_ADDR is simply correct.
Sessions were shared across every domain in an account
Session storage was scoped per *account* rather than per domain, so a session and its CSRF token minted on one domain were valid on every other domain that account owns. For anyone using the multi-domain feature, a staging site or a legacy app with an XSS became a foothold against their more valuable domains.
Sessions are now scoped per domain with a per-domain cookie name, and hosted sites get HttpOnly, Secure, SameSite=Lax and strict session-id mode — none of which PHP sets by default.
> This logs users out once. The change applies to a domain when its PHP-FPM pool is next written (adding a domain, or inetp rebuild_pools). Existing sessions for that domain end at that point. Nothing else is affected.
PHP session files were never deleted
The panel's per-pool session path is invisible to Debian's session cleaner, which reads only the system php.ini. PHP's own collector is disabled on Debian by design. Nothing removed them: one account reached roughly 4.2 million files, with the directory entry alone at 281 MB — a cost paid on every session lookup.
inetp session_reaper now runs hourly, honours session.gc_maxlifetime, and works in bounded batches with nice/ionice, because an unbounded delete over millions of files can stall a container.
Run inetp session_reaper --dry-run after updating to see what has accumulated. Note that ext4 does not shrink a directory entry once grown — reclaiming that space requires recreating the directory, which the dry run flags.
Per-site logs were never rotated
Nothing in /etc/logrotate.d/ matched /home, so hosted sites' Apache logs grew without bound — 218 MB and 106 MB access logs were found unrotated since April — at mode 0644. Access logs record full query strings, so any application placing a token in a URL leaked it into a world-readable file.
Ships /etc/logrotate.d/inetpanel-sites (weekly, 8 kept, copytruncate, mode 0640), and tightens existing world-readable site logs on update.
Upgrading
inetp panel_update, or wait for the nightly auto-update. Everything except the session change is transparent.
With thanks to the security researcher who reported the privilege escalation privately, and to the operator whose fleet audit surfaced the rest.
v1.26.0 — Adds Debian 13 (Trixie) support and serves the setup wizard
Adds Debian 13 (Trixie) support and serves the setup wizard over HTTPS.
Debian 13
Both Debian 12 (Bookworm) and Debian 13 (Trixie) are supported. Debian 12 left regular support on 2026-07-11 but stays on LTS until June 2028, so existing servers do not need to migrate.
Verified by a full clean install on Debian 13: install → create account → DNS and certificate → delete account.
What had to change:
dnsutilsno longer exists on Trixie. It was a transitional package. The installer now usesbind9-dnsutils, which exists on both releases. This was the first hard failure — the installer died before installing anything.- The PHP repository was pinned to
bookworm. That suite still resolves, so the failure came later and confusingly: sury's bookworm build depends onlibssl3and Trixie only shipslibssl3t64. - cloudflared stays on the
bookwormsuite deliberately. Cloudflare publishes no Trixie suite, and the bookworm package is a static Go binary with no dependencies, so it runs correctly on Trixie. There is now also a direct.debfallback so a repository outage cannot abort an install part-way. - PHP version pinning is built dynamically. phpMyAdmin depends on
php-cli/php-mysql, which resolve to a full PHP 8.4 stack on Trixie. The old pin only blocked 8.6 and above, so two complete PHP stacks would end up installed side by side.
Setup wizard is now HTTPS
The wizard collects the admin password and, a few steps later, your Cloudflare API key. Both previously went over plain HTTP, and there was no HTTPS option until panel_ssl.sh was run by hand — which is after those credentials have been sent.
The installer now generates a self-signed certificate, serves the panel over TLS on 443, and redirects HTTP. Your browser will warn once; that is expected, and the closing message explains it. Replace it with a trusted certificate at any time:
inetp panel_ssl <hostname>
Fixed
Certificates could silently be self-signed on Debian 13. Trixie ships certbot 4.0, and certbot-dns-cloudflare 4.x removed support for the Cloudflare Global API Key. Issuance would fail, the panel would fall back to a self-signed certificate, and report success. It now uses a scoped API token (Settings → Cloudflare), refuses rather than degrading, and verifies the certificate issuer before calling a domain done.
inetp db_repair did nothing on Debian 13. MariaDB 11.x ships most mysql* compatibility symlinks but not mysqlcheck, and the error was discarded — so it reported zero tables checked and exited successfully.
Changing the SSH port could lock you out. Under systemd socket activation the listening port comes from ssh.socket, not sshd_config, but the check used sshd -T, which reads the config. It now verifies the actual listener and keeps the socket unit in step.
WireGuard setup failed where iptables was absent. It is not a dependency of wireguard-tools, and firewalld 2.x no longer pulls it in. The NAT rules now prefer nft.
A newly added domain could return 503. An API request omitting the PHP version reloaded a PHP-FPM service that did not exist, so the new pool's socket was never created. Related: a domain set to inherit the panel default produced an invalid service name, and its FPM reload silently never happened.
Also
install_LAMP.sh is now tracked in the repository, so it is covered by the same lint and secret-scanning as every other script.
Upgrading
Existing Debian 12 servers are unaffected and need no action. To move the panel to HTTPS on an existing install, run inetp panel_ssl <hostname>.
v1.25.0 — Account deletion is now atomic, resumable and verifiable. Pr
Account deletion is now atomic, resumable and verifiable. Previously a deletion could fail half-way through, report success anyway, and leave resources behind that the panel had already forgotten about — four accounts on one server were each found in a different state of partial deletion.
The root cause
delete_account.sh, delete_user.sh and remove_domain.sh all ended on an echo, so they exited 0 no matter how many steps had failed. The panel gates its database cleanup on that exit code, so it deleted the hosting_users row — the only record naming what still needed removing — and the leftover became invisible in the UI.
Every step in those scripts now verifies its own post-condition and the script exits non-zero if any of them failed. Re-running a deletion against a partially deleted account completes the job rather than refusing to start.
Fixed
Databases were never dropped when an account was deleted. delete_user.sh dropped none at all, and remove_domain.sh only dropped a database named after the domain — so any database created through the account portal survived every deletion.
User crontabs outlived the account. userdel -r removes /var/spool/cron/crontabs/<user>, but only while the passwd entry still exists, and one code path removed the home directory without going through userdel. The file is keyed by name but owned by the numeric uid, and that uid returns to the free pool — so a future account could inherit, and read, a dead account's scheduled jobs. Crontabs are now removed before the system user, and verified.
remove_domain.sh could drop the wrong databases. It matched LIKE '<user>_<domain>%', where _ is a single-character wildcard in MySQL and the trailing % matched anything sharing the prefix. Removing example.com could take example_com_dev and example_com_backup with it. It now matches the exact name.
Deleting an account whose system user was already gone was impossible. delete_account.sh exited immediately on "System user does not exist" — precisely the state a failed deletion leaves behind, so the panel could not clean up after itself.
FTP and portal access could survive deletion. The /etc/vsftpd.userlist cleanup silently did nothing when the account being removed was the only entry in the file. That file also authorizes hosting-portal login, so the entry was a live credential, not cosmetic.
Ports were burned permanently. The Listen line was only removed if the vhost file still existed, so a vhost that had already gone left its port allocated forever.
Accounts silently stopped being backed up. A hosting account whose home directory was missing was skipped with no message, no log entry and no counter — so it looked healthy in the UI while nothing was being saved. Two accounts were lost this way, having aged past the retention window while orphaned. Backups also now check tar's exit status instead of reporting success unconditionally, and the retention sweep no longer descends into subdirectories.
Added
inetp audit_orphans — reconciles hosting accounts against /etc/passwd, /home, the FTP allowlist, crontabs, databases, MariaDB users, Apache vhosts, PHP-FPM pools, certificates and ports, reporting divergence in both directions:
sudo inetp audit_orphans # whole server
sudo inetp audit_orphans --user bob # one account
It is read-only and has no --fix mode by design. Exit codes: 0 clean, 1 something was found, 2 could not check.
A last-chance backup before deletion. Written to /backup/deleted/, which the retention sweep cannot reach. If it fails, the deletion aborts with nothing destroyed.
Deletion intent is recorded before anything is destroyed, under /var/lib/inetpanel/deleting/, and cleared only once every step has confirmed success. An interrupted deletion leaves a findable record instead of an invisible orphan, and the panel refuses to create a new account with a name whose deletion never finished.
A MariaDB plugin-version check (#22). When mariadb-plugin-provider-* packages fall out of step with mariadb-server, the daemon refuses to start on its next restart. The audit now reports this before something restarts MariaDB, rather than after.
Upgrading
Nothing runs automatically on update. After upgrading, sudo inetp audit_orphans is worth running once — it will tell you whether any previous deletion left something behind.
v1.24.5 — Reliability release. Fixes the orphaned-vhost bug behind #17
Reliability release. Fixes the orphaned-vhost bug behind #17, where a deleted domain could leave Apache unable to start and take every site on the server down.
Fixed
Orphaned Apache vhosts (#17). Deleting an account enumerated its domains by scanning the home directory. If that directory was already gone, the vhost survived pointing at a DocumentRoot and log directory that no longer existed. Apache kept serving on its loaded config, so nothing looked wrong — until the next restart (an apt upgrade, a reboot), at which point a missing log directory is fatal and Apache refused to start. Every site on the box then returned 502. Domains are now resolved from the panel database and vhost ownership as well as the filesystem.
Apache config is validated before every reload. remove_domain.sh ran reload || restart — backwards, because a failed reload leaves the working config serving, but the restart that followed took the whole server down. Both add_domain.sh and remove_domain.sh now run apache2ctl configtest first and never escalate to a restart.
add_domain no longer overwrites a live site's index.php. Re-adding an existing domain to regenerate its vhost copied the welcome placeholder over the real homepage. The placeholder is now only written into an empty web root.
inetp status printed garbage and always claimed an update was available. The version lookup matched every line mentioning APP_VERSION, including doc comments, producing a multi-line blob that never compared equal. It also falls back to ASCII on non-UTF-8 consoles, where the status dots rendered as ?.
inetp dns_check example.com now works. It previously required --domain and rejected a bare domain.
The SSH port survives an openssh-server upgrade. The port was written into /etc/ssh/sshd_config, which is ucf-managed on Debian — accepting the maintainer's version on the upgrade prompt silently reverted it and locked you out on the next reconnect. It now lives in /etc/ssh/sshd_config.d/99-inetpanel.conf, and the change is verified to have taken effect before being reported as successful.
Added
inetp rebuild_vhosts — regenerates missing Apache vhosts from the panel database and reports orphaned ones. Run inetp rebuild_vhosts --check for a read-only report; it will tell you whether a broken vhost is waiting to take your server down on the next reboot.
Upgrading
Nothing in this release runs automatically. All of the above execute only on explicit action, so the update itself is a straight file replacement.
If you have ever deleted a domain or an account, sudo inetp rebuild_vhosts --check is worth running once after updating.
v1.24.4 — CRITICAL: php-fpm ProtectSystem fix (account creation broken on updated servers)
⚠️ CRITICAL — please read, and enable automatic backups
This release fixes a system-wide bug that can silently break account creation, domain setup, and SSL on servers that were previously working perfectly. If you run iNetPanel, update and read this.
► Enable automatic backups now
Settings → Backups → enable daily automatic backups, and take a manual backup before updating. The failures in this bug are silent — operations report success while doing nothing — so a known-good backup is your safety net.The bug — full details
- What: php-fpm's systemd unit ships with
ProtectSystem=full, which mounts/etcread-only for every process php-fpm spawns. The panel makes privileged system changes —useradd, Apache vhosts, PHP-FPM pools,vsftpdentries, Let's Encrypt certs — via root helpers launched by php-fpm, andsudodoes not escape php-fpm's mount namespace, so all of those/etcwrites fail. - Symptoms: creating an account fails with
useradd: cannot lock /etc/passwd; try again laterorRead-only file system; adding domains / issuing SSL can fail the same way. Until this release the scripts reported success anyway, leaving "phantom" accounts (ahosting_usersrow with no Linux user) that then block re-creating that username. - Why it appeared out of nowhere:
ProtectSystem=fullis part of the stock php-fpm unit; a routine PHP package update in the past few months enabled it on servers that were fine before. No panel change was involved — a working system simply stops being able to create accounts. It looks like a storage/permission problem but isn't — it happens identically on ext4 and ZFS.
The fix
A php-fpm drop-in setsProtectSystem=true (keeps /usr and /boot read-only, allows /etc). New installs get it from the installer before php-fpm first starts; existing installs get it on update.
⚠️ Applying it on an already-affected server
Most servers need no action — the nightly auto-update (runs as root via cron, outside the sandbox) applies this fix automatically. Just make sure auto-update is enabled under Settings → Updates.To apply it immediately, or if auto-update is off, run once from a root shell:
inetp panel_update # or: php /var/www/inetpanel/scripts/panel_update.php
The web "Update Now" button cannot apply this particular fix — it runs *inside* php-fpm's read-only sandbox. After the fix applies, php-fpm stops sandboxing
/etc and everything (including "Update Now") works normally again. If a username is stuck as a phantom account, delete its hosting_users row and re-create it.
---
Also in v1.24.4
create_user.sh now fails loudly when useradd fails (no more phantom accounts) — this is what finally surfaced the bug above.v1.24.3 — HTTP/1.1 origin hardening + port-collision & multi-PHP fixes
iNetPanel v1.24.3
Bug-fix release hardening multi-tenant isolation and fixing per-domain PHP switching.
Fixed
- Cross-vhost content contamination. The Cloudflare→Apache origin hop is now pinned to HTTP/1.1 via a managed
conf-available/inetpanel-origin.conf(Protocols http/1.1), applied idempotently on update (configtest + safe reload, auto-revert on failure) and by the installer for new installs. This prevents HTTP/2 connection coalescing from ever serving one domain's content (e.g.sitemap.xml) under another. Visitors keep HTTP/2/3 from Cloudflare's edge — only the origin hop changes. - Port-collision isolation.
add_domainnow allocates each vhost port atomically (flock) and scans real usage (ports file + live vhosts), refusing to co-locate two domains on one port. Backup restore re-derives unique, free ports server-side instead of trusting client-submitted values. - Per-domain PHP version switch. Switching a domain's PHP version now runs through a root
multiphp_manage --action set_domainaction using the correct{user}_{domain}pool naming, and validates the domain first. - Existing installs apply the hardening and fixes automatically on update; new installs include them in the installer.
Upgrade
v1.24.2 — Installer cascade fix (#14, #15) + per-domain disk cache
Bug Fixes
- Installer fails on fresh Debian 12 (closes #15). The installer's
exec_cmdhelper ran commands in a backgrounded subshell that didn't have/usr/sbinin PATH, soa2enmodfailed. Added a global PATH export near the top ofinstall_LAMP.shand switcheda2enmod/a2ensite/a2dissite/a2disconfto absolute/usr/sbin/paths. - Unusable MariaDB / phpMyAdmin on fresh install (closes #14). Same root cause as #15: when
a2enmodexited early, the installer skipped the sudoers file, phpMyAdmin storage DB, and panel deploy. Without the sudoers rulewww-datacouldn't read the MariaDB root password — producing *"Access denied for user 'root'@'localhost' (using password: NO)"*. Fix #15 resolves the cascade. - Same disk usage shown on every domain for multi-domain users.
accountDisk()was summing/home/<user>(the whole home dir) and the user's DB total on every row. Now a newdisk_cacheSQLite table tracks per-domain file usage,disk_cache_usertracks the per-user DB total, and the Accounts page shows each domain's actual files size plus a single *Total:* badge per user (files across all their domains + MariaDB total). - /admin/accounts load time. With 30+ domains and 100 GB+ data the page hung for seconds while
du -sbran live for every user. Newscripts/system/disk_cache_scan.shpopulates the SQLite cache every 10 minutes via cron, and fires immediately after add/remove/create/delete via backgroundinetpcalls. The API now reads cached rows — O(ms) regardless of dataset size. - README install command now pre-installs
curlfor fresh Debian 12 installs (apt-get install -y curl && bash <(curl -s …/latest)). build_release.shnow also generateslatest-betaalongsidelatest— same installer, just with the download URL swapped to the main-branch zipball so beta testers pull the latest code instead of the tagged release zip.
Performance
Docs / Packaging
v1.24.1 — Dashboard speed + www ServerAlias hotfix
Performance
- Dashboard load time —
/api/accounts?action=listwas runningdu -sb+inetp db_sizefor every hosting user on every dashboard load (5+ seconds with 31 domains), even though the dashboard only displays 6 rows and doesn't use disk usage. Added?limit=Nand?skip_disk=1query params; dashboard now requestslimit=6&skip_disk=1. Accounts page unchanged — still shows disk usage. - Apache vhost missing www ServerAlias — Apex domains (
example.com) had the DNSwwwCNAME auto-created by the Cloudflare tunnel logic, but the Apache vhost only declaredServerName ${DOMAIN}, sowww.{domain}requests fell through to the default vhost.add_domain.shandrestore_account.shnow addServerAlias www.${DOMAIN}for apex domains (dot-count heuristic: exactly 1 dot, not already prefixed withwww.).
Bug Fixes
v1.24 — Backup Restore with CF Migration Support
New Features
- Backup Restore — Full account restore from backup archives with 4-step wizard: Upload (web with progress bar, FTP, SSH/SCP) → Review (username, domains, ports, databases) → Cloudflare routing check with override → Execute with progress and credential summary.
- Domain availability checker —
inetp check_domainsuses RDAP (free, no API key) for single, bulk, and auto-generated variant checks. - CF tunnel migration — Restore override adds route to new tunnel and removes from old tunnel automatically. Searches all account tunnels for domain conflicts, not just the current server's tunnel.
- Tunnel creation fails on fresh install — Removed invalid
tunnel_secretfrom API-managed tunnel creation. - Beta channel update detection — Was always showing "up to date"; now compares commit SHAs.
- Beta version string stacking — Stripped
-beta.x-beta.ychains to just base + latest hash. - Domain deletion "Connection error" — FPM reload in
remove_domain.shkilled panel worker; deferred to after response. - Restore connection reset at 95% — Same FPM reload issue in
restore_account.sh. - Restore FTP login — Fixed nologin shell, sudo permissions, password (now copies root's shadow hash).
- Restore parse on large backups — Replaced full
tar -tzfbuffering with streaming grep pipes (constant memory). - CF CNAME safety —
removeTunnelHostname()now only deletes CNAME if it points to the specific tunnel being cleaned up, preventing breakage when domains are migrated between servers. - CF domain conflict detection —
check_domainandadd_domainnow search all account tunnels, not just the current server's.
Bug Fixes
v1.23.3 — Fix tunnel creation, beta update detection, add domain checker
Bug Fixes
- Tunnel creation fails on fresh install — Removed
tunnel_secretfromcreateTunnel(). API-managed tunnels (config_src: cloudflare) reject client-side secrets; Cloudflare's Rust serde threw a JSON deserialize error. Token is already retrieved separately viagetTunnelToken(). - Beta channel update detection broken —
update_check.phpandsettings.phpcheck_updatesaction only queried GitHub releases API, never checking main branch commits. Beta channel now compares latest commit SHA against installed SHA to properly detect available updates. - Domain availability checker — Added
check_domains.sh(inetp check_domains). Uses RDAP (free, no API key). Supports single domain, bulk file (-f), and auto-generated variant checks (-g keyword).
New Features
Hotfix
- Installer fails to download panel source —
install_LAMP.shwas pointing atinetpanel.tuxxin.com/inetpanel-latest.zip(404). The zip is a GitHub release asset, not hosted on the website. Fixed URL togithub.com/tuxxin/iNetPanel/releases/latest/download/inetpanel-latest.zip.
Install / Update
bash <(curl -s https://inetpanel.tuxxin.com/latest)
Hotfix
- Multi-PHP install fails silently on fresh installs — The
/var/www/inetpanel/storage/directory wasn't created during deployment, causing status files and logs to never be written. The API now creates the directory if missing and returns a clear error if it can't.
Install / Update
bash <(curl -s https://inetpanel.tuxxin.com/latest)
Existing installs: run inetp panel_update from the admin panel or CLI.
New: Release Channels (Stable vs Beta)
- Installer now uses release tarballs instead of
git clone— the repo (mainbranch) is now beta code, while GitHub Releases are stable.
- - Settings → Updates → Release Channel — toggle between stable and beta without reinstalling. Stable pulls from tagged releases, beta pulls the latest commit from
main. - Multi-PHP install broken on all environments —
systemd-run --scopefails silently in LXC/Proxmox containers with no fallback, leaving the status file stuck at "running" forever. Added direct background execution fallback when systemd-run fails. Simplified the API exec() call and set a 3-minute timeout.
inetpanel.tuxxin.com/latest — stable installer (downloads release zip)
- inetpanel.tuxxin.com/latest-beta — beta installer (clones main branch)
Fixes
Install / Update
bash <(curl -s https://inetpanel.tuxxin.com/latest)
Existing installs: run inetp panel_update from the admin panel or CLI.
Fixes
- Dashboard disk stats inflated —
disk_free_space()excludes ext4 reserved blocks (~5%), making "used" appear ~10 GB higher than reality. Now usesdfoutput for accurate numbers matching what the OS reports. - PHP 5.6 install timeout — Polling timeout increased from 120s to 360s. Older PHP versions take longer to install via apt.
- SSH port fix for LXC containers — Previous fix only checked
is-activeonssh.socket, which misses cases where the socket is enabled but not yet active (common in LXC). Now checkslist-unit-filesandmasks the socket unit to prevent reactivation.
Install / Update
curl -o latest https://inetpanel.tuxxin.com/latest && bash latest
Existing installs: run inetp panel_update from the admin panel or CLI.
Fixes
- SSH port ignored on Debian 12 — Installer now disables
ssh.socket(systemd socket activation hardcodes port 22, overridingsshd_config Port 1022) - SSL cert reissue blocked after self-signed fallback — Cleans up non-LE certs from
/etc/letsencrypt/live/before certbot retry - CertBot DNS propagation timeout — Added
--dns-cloudflare-propagation-seconds 30(default 10s was too short) - Dashboard graph empty on fresh install —
inetpanel_statswas missing frommanage_cron.shallowed list, so the stats collector cron was never created inetp optimize_server— Auto-tunes Apache2mpm_eventand MariaDB InnoDB/buffers based on detected RAM, CPU cores, disk type (SSD/HDD), and hosted domain count. Dry-run by default,--applyto write changes with automatic config backups.
New
Install / Update
curl -o latest https://inetpanel.tuxxin.com/latest && bash latest
Existing installs: run inetp panel_update from the admin panel or CLI.
Logo rebrand & dark mode fixes (v1.22.1)
New branding
- New logo — redesigned iNetPanel logo (PNG with transparency), replaces old
.webp - Favicon — new
Logo-Icon.webpfavicon added to admin and account portals - Logo automatically inverts to white in dark mode via CSS filter
- Account portal logo resized to 28px for cleaner navigation fit
- Table headers —
.table-light<thead>elements now display light text (previously black on dark background) - Nav tabs — dark mode styling for tabbed interfaces (multi-php, firewall pages)
- Bootstrap 5.3 subtle utilities —
bg-success-subtle,bg-primary-subtle,bg-warning-subtle,bg-danger-subtlewith proper dark variants - Close button — filter inversion for visibility on dark backgrounds
- Warning badges — retain dark text for contrast on yellow background
- Table borders — consistent border color in dark mode
Dark mode — root-level fix
The core issue with dark mode was that Bootstrap 5.3 uses internal CSS variables (--bs-body-color, --bs-table-color, --bs-border-color, etc.) that our custom [data-theme="dark"] wasn't overriding. This caused all Bootstrap components to inherit dark text colors regardless of theme.
Fix: Override all Bootstrap 5.3 internal CSS variables at the [data-theme="dark"] root level. This makes every Bootstrap component — tables, cards, headings, links, borders — automatically inherit correct dark mode colors without needing per-component overrides.
Specific fixes
Pages fixed
Accounts, DNS, Email, SSL, Firewall, Multi-PHP, Services, and all other pages that use Bootstrap tables, cards, or badges.Closes #10
Dark mode audit & fixes (v1.22)
Addresses #10 — dark mode inconsistencies and unreadable text.
CSS additions (58 new dark mode rules)
- Badges —
bg-light,text-dark,bg-secondarynow theme-aware - Alerts — info, success, warning, danger with proper dark backgrounds and contrast
- Pre/code blocks — proper dark background and text colors
- Buttons —
outline-dark,outline-secondary,btn-lightvariants - Components — breadcrumbs, nav pills, pagination, input groups, progress bars, accordion, offcanvas, toast
- Logo — brightness inversion filter for dark sidebar backgrounds
- Dashboard chart grid/tick colors now adapt to theme
- QR code colors invert in dark mode (light modules on dark background)
- Removed hardcoded
#e8f4f8alert background in settings - WireGuard code block uses theme-aware
bg-lightinstead ofbg-white
Hardcoded color fixes
Fix PHP package extension install/remove (v1.21.9)
Same root cause as v1.21.8 — dpkg triggers restart php-fpm during apt-get install/remove of individual extensions (e.g. php8.5-uploadprogress), killing the PHP worker mid-request.
Changes
- Extension install/remove now runs inside
systemd-run --scopeto escape the FPM cgroup - Frontend uses async status polling with progress modal (matches multi-php pattern)
- Modal only shown after API confirms operation started (prevents stuck modal on errors)
- Added
pkg_statusAPI endpoint for polling extension install/remove progress
Fix Multi-PHP install/remove (v1.21.8)
Root cause: dpkg triggers restart php-fpm during package install/remove, which kills all processes in the FPM cgroup — including the background apt script launched from PHP exec(). nohup/setsid don't help because systemd kills by cgroup, not process group.
Changes
- Script re-launches itself via
systemd-run --scopeto escape the FPM cgroup before running apt - Fixed status file path mismatch between API and backend script
- Fixed concurrent operation guard falling through to
exec()instead of breaking the switch - Modal now only shown after API confirms success (prevents stuck modal on errors)
- Status file removed immediately after verification, before slow post-cleanup steps
- Moved apt logic from inline PHP to dedicated
multiphp_manage.shbash script
v1.21.7 — Fix Multi-PHP: detached apt execution
Bug Fix
Multi-PHP install/remove silently fails
apt-get ran inside the PHP-FPM worker process. dpkg post-install hooks restart php-fpm, which kills the worker mid-execution — apt never completes, status file stuck at 'running', modal spins forever.
Fix: All apt operations now run in a detached bash script (sudo bash script.sh &) that survives FPM restarts. The script handles the full lifecycle: dpkg configure, apt install/purge, FPM enable/start, upload limits, module reinstall, and cleanup. Status tracking via temp file allows the polling UI to detect success (file removed) or failure (file contains 'error').
Files Changed
api/multiphp.php— Detached bash wrapper for apt operationssrc/multiphp.php— Improved polling with seenRunning state trackingTiCore/Version.php— Version bump
v1.21.6 — Fix Multi-PHP status tracking
Bug Fix
Multi-PHP status file stuck at 'running'
The v1.21.5 status file mechanism never transitioned from 'running' to 'done' because dpkg post-install hooks triggered an FPM restart that killed the PHP worker before it could update the file. The polling modal would spin until timeout.Fix: Status file is deleted immediately after apt succeeds, before any FPM operations. On apt failure, error is written and execution stops immediately.
Files Changed
api/multiphp.php— Reorder status file cleanup before FPM operationsTiCore/Version.php— Version bump
v1.21.5 — Fix Multi-PHP background failure detection
Bug Fix
Multi-PHP install/remove modal spins forever on failure
Whenapt-get failed silently in the background (e.g. package not available, dpkg lock), the polling modal would spin indefinitely since the version state never changed.
Fix: Background apt operations now write status to a temp file. The polling list action reports this status, allowing the UI to detect errors and show them immediately instead of spinning for 3 minutes.
Files Changed
api/multiphp.php— Status file tracking for background apt operationssrc/multiphp.php— Poll checks for background error statusTiCore/Version.php— Version bump
v1.21.4 — Fix Multi-PHP removal & FPM pool cleanup
Bug Fixes
Multi-PHP removal causes 500 error
Removing a PHP version via the admin UI triggered an Internal Server Error becauseapt-get purge restarted the panel's own PHP-FPM process mid-request.
Fix: apt-get now runs after fastcgi_finish_request() so the browser receives a success response before the package removal begins. The UI polls until the version is fully removed before closing the progress modal.
Multi-PHP removal leaves packages behind
The purge command only removed specific packages (fpm, cli, common, etc.) but missed sub-packages like bz2, gd, intl — leaving the binary installed.Fix: Uses wildcard php{ver}-* purge + apt-get autoremove to fully remove all packages for that version.
FPM pools not cleaned up on domain/account removal
Removing a domain or account from the admin UI deleted the pool config file but never reloaded PHP-FPM, leaving orphaned worker processes running.Fix: Both the shell script and API now reload FPM after pool removal.
Database creation SQL syntax error
Creating a database from the client portal failed with "Unknown command" due to escaped backticks in the SQL statement.New Features
inetp rebuild_pools
New CLI command to regenerate all missing PHP-FPM pool configs from the panel database. Useful for recovery after pool files are accidentally deleted.
Dashboard "Accounts & Domains"
The dashboard card now shows both account count and domain count (e.g. "3 / 5").Files Changed (9 files)
TiCore/Version.php, api/account.php, api/accounts.php, api/multiphp.php, scripts/system/inetp, scripts/system/rebuild_pools.sh, scripts/system/remove_domain.sh, src/dashboard.php, src/multiphp.php
v1.21.3 — Fix backup toggle and retention
Bug Fixes
Backup toggle not disabling backups
Toggling backups off in admin settings had no effect — the cron was always written with the backup command, and the script never checked thebackup_enabled setting. Fresh installs saw system config backups appearing even with backups toggled off.
Fix:
api/settings.php: Checksbackup_enabledbefore writing backup cron; writes a disabled comment when offbackup_accounts.sh: Exits early ifbackup_enabled = 0(safety net);--singlemode still works for domain removalapi/settings.php— Backup toggle cron logicscripts/system/backup_accounts.sh— Early exit guard + retention fixTiCore/Version.php— Version bump
Backup retention not cleaning old files
The retention cleanup never removed old backups becauseMTIME_DAYS was calculated inside the loop body but used in the find command's process substitution (which runs before the loop).
Fix: Moved MTIME_DAYS calculation before the find command.
Files Changed
v1.21.2 — Fix database creation, updated README
Bug Fix
Database creation from client portal
CREATE DATABASE used escapeshellarg() which wraps the name in single quotes — MySQL requires backticks for identifiers. All database creation attempts from /user/dashboard#database failed with ERROR 1064. Now uses backtick-wrapped identifiers with regex sanitization.
Updated
- README rewritten — streamlined with home hosting focus, technical stack details, directory layout, and links to [inetpanel.tuxxin.com](https://inetpanel.tuxxin.com) for full documentation
api/account.php— Fix CREATE DATABASE SQL syntaxREADME.md— RewriteTiCore/Version.php— Version bump
Files Changed
Release notes are fetched from github.com/tuxxin/iNetPanel/releases and cached for 6 hours.