← Blog

Secure SSH for hosting accounts, without CageFS

How CorePanel gives hosting accounts bash, rsync, git and wp-cli over SSH without showing them the server: a per-session jail, systemd limits and SELinux.

A row of glass bell jars standing apart on a dark blue floor. The second one glows bright blue around a terminal window floating inside it, its light stopping at the glass, with an emerald seam sealing its base; the jars around it are dim and empty or hold faint shapes of their own

Sooner or later every customer on a hosting server asks for SSH. They want to deploy with rsync, pull with git, update WordPress with wp-cli and run composer without uploading a vendor folder over FTP. A plain login gives them all of that, along with a full view of the server: every installed tool, every readable file, the process list, and as many resources as they care to take.

There have been two answers so far. cPanel’s jailed shell hides the server behind bind mounts under /home/virtfs, and caps a shell with Shell Fork Bomb Protection, a WHM switch that applies a set of ulimit values with no CPU limit at all. CloudLinux adds CageFS and LVE limits, as a separate operating system with its own licence. CorePanel has a third answer. Every account can get a real shell, jailed and capped, on a stock RHEL, AlmaLinux or Rocky server, in every edition, Personal included.

Four levels, one port

Each account has an SSH access level, set from its SSH access section or from the hosting package:

  • Off: no SSH at all.
  • SFTP only: file transfer, jailed to the account’s home. This is where new accounts start, so a customer can upload files on day one without ever getting a shell.
  • Shell: bash inside a jail.
  • Legacy: an account that already had a shell before CorePanel managed SSH, usually one migrated from cPanel. It moves to Shell in one click, keys included.

Everything runs on the SSH port you already use. Administrators and hosting accounts are told apart by group membership in Match blocks, not by a second port, so there is no new firewall rule and nothing new to reach. root and your own users are never matched: how you log in does not change.

What the customer finds inside

At each login, pam_namespace builds a mount namespace for that session alone. Inside it, the account finds:

  • Its home, at its real path, /home/<user>, so scripts and configs that hard-code the path keep working.
  • A /tmp of its own, which no other account sees.
  • The server’s allowlisted tools, read-only: the file and text utilities, tar and the compressors, vi and nano, curl, git, rsync, ssh and scp, the MariaDB client, every installed PHP with wp-cli, and composer, screen or tmux when the server has them.

What it does not find matters more. There is no other account, no server configuration and no setuid program: /etc holds only what a shell needs, and sudo, su, passwd and pkexec are not there to try. When customers need a program the jail does not carry, you add it under Settings → Shell jail utilities, and it goes through the same checks as the built-in set.

No jail left mounted in /home

This is where the design differs most from what hosting providers are used to.

cPanel builds its jailed shell, VirtFS, out of bind mounts under /home/virtfs/<user>: /usr/bin, the libraries, the user’s own home, all linked into one tree that stays there. cPanel’s own documentation warns that running rm on anything mounted under /home/virtfs also deletes the files it points to, and can leave the server nonfunctional. It also notes that du may report disk usage that isn’t real. cPanel even drops a 0_README_BEFORE_DELETING_VIRTFS file in the folder, because every administrator who finds a large, strange directory under /home wants to clean it up. Unmounting those trees is a job of its own, with a dedicated script, clear_orphaned_virtfs_mounts.

CorePanel’s shell jail exists only inside the session’s mount namespace. Nothing else on the server sees it, not even root from outside, and when the session ends the namespace goes with it and leaves no mount on the host. Between sessions, /home holds homes and nothing else: no phantom trees, no orphaned mounts, nothing for a cleanup script or a backup tool to trip over. The home itself stays 0700 and owned by the customer. The jail does not need it handed over to root to work.

Limits that hold, even against a fork bomb

A shell is also a way to start processes, so every shell account runs inside its own systemd slice with limits taken from its hosting package:

  • Memory and CPU.
  • Tasks, the number of processes and threads.
  • Open files and maximum file size.
  • Concurrent logins.

A new package caps tasks at 256. A fork bomb run in a customer’s shell hits that cap and stops there, while the server and every other site on it keep answering. A ulimit caps memory one process at a time; the slice caps the account as a whole, so memory, CPU and tasks are one budget for every session it has open at once. Change a package’s limits and new accounts take them. Change one account’s limits from its Shell resources card, which the customer sees read-only in their own panel. CorePanel’s values win over the server’s limits.d and systemctl set-property, so nothing has to be tuned in two places.

SELinux does the part a chroot can’t

A filesystem jail decides what a process can see. It does not decide what a process can do, so CorePanel does not stop there. With SELinux enforcing, the RHEL default, every shell session runs confined as user_u. It cannot step into an administrative domain, and it cannot reach the account’s own processes running outside the jail, such as its PHP workers.

We don’t ask you to turn SELinux off to make hosting work. We use it as one of the walls.

Keys nobody can plant

Most hosting servers read ~/.ssh/authorized_keys from the customer’s home. That means any code that can write to the home, such as a compromised WordPress plugin, can add an attacker’s key and keep coming back. CorePanel keeps the key files outside the home, owned by root and managed from the panel, so a compromised site cannot add its own.

The rest follows the same idea:

  • Keys by default. Password login is something you grant per account, with an optional expiry, never something the customer turns on for themselves.
  • No tunnels. Local, remote and dynamic port forwarding, agent forwarding and X11 are all denied, so a shell account cannot become a proxy into your network.
  • Brute-force protection on SSH only. An IP guessing passwords is banned from the SSH port and nothing else, so nobody locks themselves out of the panel they would use to lift the ban.
  • Suspension means suspension. Suspending an account closes its open SSH sessions, not just new logins.

How it compares to CloudLinux

CloudLinux’s CageFS is the best-known answer to this problem, and it has been around for years. It is a per-user virtualized filesystem, with resource limits from LVE. It is also a separate operating system with its own kernel module and its own licence.

CloudLinux (CageFS + LVE)CorePanel Shell
Operating systemCloudLinux OSStock RHEL, AlmaLinux or Rocky
KernelThe kmod-lve moduleThe distribution’s kernel, unchanged
ConfinementCageFS + LVEMount namespace + systemd slices + SELinux
SELinuxSupported from version 8, though its own docs warn it “might not work or be compatible with control panels and various components”Enforcing, and part of the confinement
Price$18 per server per month for shared hosting (CloudLinux OS Unlimited), on top of the panelIncluded in every CorePanel edition

For the customer, the result is the same: a shell that sees their own files and nothing else, and can’t take the server down. We get there with parts the operating system already has and maintains (mount namespaces, systemd and SELinux), so there is no second licence, no extra kernel module, and nothing to remove if you change your mind. PHP gets the same treatment from a different angle, through 0700 homes and a PHP-FPM pool per account; True user isolation, explained covers that half.

What it doesn’t do

  • No port forwarding, by design. That rules out VS Code’s Remote-SSH, which needs it. rsync, scp, git over SSH and interactive sessions all work.
  • No long-running services. systemctl --user and anything meant to outlive the session don’t run. Applications belong in CorePanel’s application runtime.
  • Only the tools the jail carries. Anything else has to be added under Settings → Shell jail utilities first.
  • On RHEL 8, ps shows nothing. The jail has no /proc there; newer releases list the account’s own processes.
  • SELinux has to be on. On a server with SELinux disabled, the Shell level is refused, because the jail alone would not confine the account’s own code. SFTP only and Legacy work either way.

Try it

Open an account, go to SSH access and pick Shell from the access level dropdown. The first shell on a server takes about a minute while CorePanel builds the shared copy of the tools; later accounts are quick. Every detail, from limits to keys and what CorePanel writes in sshd, is in the SSH access documentation.