Confining Developer SSH and WP-CLI Access in HestiaCP with Custom Jailkit Chroots

Confining Developer SSH and WP-CLI Access in HestiaCP with Custom Jailkit Chroots

Granting standard SSH shell access (/bin/bash) to external contractors or agency developers on a multi-tenant web server introduces severe security exposure. Even without root privileges, an unconfined user can inspect system directories in /etc, enumerate neighbor domain names, monitor background processes, and probe unauthenticated local service ports (127.0.0.1:3306, 127.0.0.1:6379).

Across hosting environments managed with panels such as HestiaCP, where several client websites share the same host, unrestricted shell access facilitates lateral movement: a compromised third-party credential immediately exposes adjacent tenants on the server.

The secure method to provide developers with essential tools (git, composer, wp-cli, and php-cli) is a hardened chroot sandbox powered by Jailkit. By configuring HestiaCP’s jailbash shell and custom initialization profiles, each developer is locked entirely inside /home/username, with access restricted to explicit binaries and dynamically linked libraries while the remainder of the operating system remains completely invisible.

At Webxtek Studio, where we operate infrastructure for DevOps & Cloud Architecture Specialists and high-security sectors such as Alarm Monitoring Centers, we enforce this isolation as part of the defense-in-depth model detailed in our CISA, NSA & FBI Emergency Playbook: PHP, Node.js & Port Hardening.

Below I break down chroot kernel isolation mechanics and provide the exact configuration files to build locked-down developer sandboxes in HestiaCP.

1. From the Isolated Hallway to the chroot() Syscall: How Jailkit Works in HestiaCP

To sandbox developer SSH access in HestiaCP without breaking critical development tooling like Git, PHP-CLI, Composer, and WP-CLI, you must declare custom profiles in /etc/jailkit/jk_init.ini, assign the jailbash shell via the v-change-user-shell CLI utility, and restrict file-transfer accounts to OpenSSH internal-sftp.

To understand why adjusting directory permissions with chmod 750 in /home fails to secure a system, you must understand how POSIX permissions function in Linux. For the Nginx web server, PHP-FPM workers, and system daemons to function, thousands of files across /etc, /usr, and /var require world-readable permissions (0644). When a developer connects via SSH with /bin/bash, Linux blocks them from entering /home/other_client, but nothing stops them from running cat /etc/nginx/conf.d/domains/*.conf (revealing all customer domains, IP bindings, and SSL certificate paths) or executing ss -tulpn to map local services like Redis, MySQL, or memcached.

As we analyzed extensively in our investigation on the cPanel, Plesk, CyberPanel & HestiaCP: PHP2Shell Mitigation & FPM Hardening, unprotected internal ports and loose panel permissions represent the primary gateway for horizontal privilege escalation across shared hosting environments.

The classic Unix engineering solution is the chroot() (Change Root) system call, securely implemented by the open-source Jailkit suite by Olivier Sessink and integrated directly into HestiaCP system settings:

Filesystem Visibility Comparison (Linux Kernel Virtual Filesystem - VFS):

[Scenario 1: Default /bin/bash SSH Login (INSECURE)]
Root seen by user (/): Actual real root of the Linux operating system
- Can read /etc/passwd, /etc/hosts, /etc/nginx/conf.d/* (exposes all customer domains!)
- Can execute global binaries in /usr/bin (curl, wget, nc, python3, find, gcc)
- Can inspect local sockets and running processes if hidepid is not mounted on /proc

[Scenario 2: HestiaCP jailbash Login (/usr/sbin/jk_chrootsh)]
Root seen by user (/): The /home/username directory becomes the absolute root (/)!
- When the developer executes 'cd /' and runs 'ls', they see strictly their own /home/username folders.
- /etc/nginx, /root, /var/log, and sister customer directories are mathematically unreachable.
- Only binaries explicitly copied with jk_init exist inside /home/username/usr/bin.

How does this isolation execute the millisecond a developer initiates an SSH session?

  1. In /etc/passwd on the host system, the user is assigned /usr/sbin/jk_chrootsh rather than /bin/bash.
  2. The jk_chrootsh binary executes strict pre-flight security audits: it verifies that /home/username is owned by root:root with 0755 permissions (preventing the user from tampering with system files inside the jail to achieve privilege escalation).
  3. The jk_chrootsh wrapper invokes the chroot("/home/username") kernel syscall, immediately drops all superuser capabilities (setuid / setgid down to the unprivileged client UID), and executes the /bin/bash shell living inside the jail (/home/username/bin/bash).
  4. From that moment forward, the Linux kernel blocks any directory traversal attempt (cd ../../../): when a process sits at the apex of its chroot jail, the parent directory (..) evaluates to itself!

2. The Shared Library (.so) Challenge and Crafting a Production jk_init.ini Profile

In production environments, I frequently observe administrators enabling jailbash with default settings, only to immediately receive developer complaints: “I connected via SSH, but running composer install or wp plugin list throws command not found or SSL certificate validation errors!”.

Why does this happen? When you confine a user to a chroot jail, they lose access to /usr/bin and /lib/x86_64-linux-gnu on the host system. If you want them to execute PHP 8.4, Composer, WP-CLI, and Git inside the sandbox, you must copy not only the primary executable (/usr/bin/php8.4), but also:

  • All dynamic shared C libraries (.so) that the executable links against (discoverable via ldd /usr/bin/php8.4).
  • PHP extension modules (/usr/lib/php/20240924/*.so) and CLI configuration files (/etc/php/8.4/cli).
  • Trusted root CA certificates (/etc/ssl/certs) and glibc name-service switch libraries (libnss_dns.so.2 and /etc/resolv.conf), without which Git and Composer cannot establish TLS handshakes with GitHub or Packagist.

Fortunately, you do not have to map these dependencies manually. Jailkit’s jk_init utility automatically parses ldd trees when given a properly structured definition in /etc/jailkit/jk_init.ini.

Step 1: Install WP-CLI and Define the [webxtek_dev] Profile in /etc/jailkit/jk_init.ini

Connect to your HestiaCP host as root (Debian 12 or Ubuntu 24.04) and append this custom profile to /etc/jailkit/jk_init.ini:

# ============================================================================
# Append to the end of /etc/jailkit/jk_init.ini
# Hardened Sandbox Profile for Full-Stack / WordPress / Laravel Developers (Webxtek)
# ============================================================================

[webxtek_dev]
comment = Hardened chroot sandbox with Git, PHP 8.3/8.4 CLI, Composer, WP-CLI, and SSH

# Mandatory base sections for DNS resolution, UID mappings, and basic interactive shell
includesections = netbasics, uidbasics, basicshell, git, ssh

# Binaries and runtime configurations copied automatically with ldd dependency resolution
paths = /usr/bin/php8.3, /usr/bin/php8.4, /usr/bin/php, /etc/alternatives/php, /usr/lib/php, /etc/php/8.3/cli, /etc/php/8.4/cli, /etc/php/8.3/mods-available, /etc/php/8.4/mods-available, /usr/local/bin/composer, /usr/local/bin/wp, /usr/bin/curl, /usr/bin/unzip, /usr/bin/zip, /usr/bin/tar, /usr/bin/gzip, /usr/bin/nano, /usr/bin/less, /usr/bin/clear, /usr/bin/env, /usr/bin/which, /usr/bin/md5sum, /usr/bin/sha256sum, /etc/ssl/certs, /usr/share/ca-certificates, /usr/share/zoneinfo

# Safe virtual character devices required inside the jail (/dev/null, /dev/urandom, /dev/zero)
devices = /dev/null, /dev/zero, /dev/urandom, /dev/random

Notice that we deliberately exclude reconnaissance and compilation utilities (such as nmap, nc, gcc, python3, find, ps, ss, or top). The developer is equipped with every tool needed to manage application code, but completely deprived of tools to probe the host operating system.

Step 2: Enable jailbash in HestiaCP and Seed the Profile into the User Jail

In HestiaCP, never edit /etc/passwd manually, as automated control panel tasks (v-rebuild-user) will overwrite your modifications. Always execute official HestiaCP CLI commands located in /usr/local/hestia/bin/:

# Define the HestiaCP target username to sandbox
HESTIA_USER="cliente_agencia"

# 1. Enable jailed SSH (jailbash) using the native HestiaCP CLI
/usr/local/hestia/bin/v-change-user-shell "$HESTIA_USER" jailbash

# 2. Seed our custom [webxtek_dev] profile into the user jail (/home/$HESTIA_USER)
sudo jk_init -j "/home/${HESTIA_USER}" -k webxtek_dev

# 3. Ensure pseudo-devices exist with correct permissions inside the sandbox
sudo chmod 0666 "/home/${HESTIA_USER}/dev/null" "/home/${HESTIA_USER}/dev/zero" "/home/${HESTIA_USER}/dev/urandom" 2>/dev/null || true

# 4. Create an isolated temporary directory (preventing Composer and WP-CLI from using global /tmp)
sudo mkdir -p "/home/${HESTIA_USER}/tmp"
sudo chown "${HESTIA_USER}:${HESTIA_USER}" "/home/${HESTIA_USER}/tmp"
sudo chmod 0700 "/home/${HESTIA_USER}/tmp"

Notice the -k (--hardlink) flag passed to jk_init: whenever /home and /usr reside on the same filesystem partition, jk_init -k creates hard links for binaries and shared libraries rather than copying files physically. This means provisioning PHP, Git, and curl inside a jail consumes zero additional disk space and completes in less than half a second!


3. Bridging PHP-CLI and WP-CLI Inside the Jail to MariaDB and Redis

Here we address a systems engineering detail that distinguishes experienced administrators from novices in production.

When a developer operates inside /home/cliente_agencia and executes wp db export or php artisan migrate, what happens if wp-config.php or .env specifies DB_HOST=localhost? In Linux, specifying localhost instructs MySQL/MariaDB drivers to connect over the UNIX domain socket /run/mysqld/mysqld.sock. However, because the developer is jailed inside /home/cliente_agencia, the host /run/mysqld/ directory does not exist within their chroot view, and database connections fail with No such file or directory!

You have two clean engineering solutions:

  • Fast TCP Loopback Solution (127.0.0.1): Set DB_HOST=127.0.0.1 in .env or wp-config.php to force connections over the loopback network stack on port 3306.
  • High-Performance UNIX Socket Bind Mount (Webxtek Standard): To retain the latency advantages of UNIX sockets and permit access strictly to authorized database and cache endpoints, expose the socket directory inside the jail using a read-only bind mount via fstab:
# Create the MariaDB socket mount point inside the user jail
sudo mkdir -p "/home/${HESTIA_USER}/run/mysqld"

# Mount the host MariaDB socket directory into the jail in read-only mode
sudo mount --bind /run/mysqld "/home/${HESTIA_USER}/run/mysqld"

# Make the bind mount persistent across server reboots via /etc/fstab
echo "/run/mysqld /home/${HESTIA_USER}/run/mysqld none bind,ro,nofail 0 0" | sudo tee -a /etc/fstab

With that bind,ro configuration, wp-cli and artisan inside the jail find /run/mysqld/mysqld.sock immediately without breaking the chroot boundary.


4. Strict SFTP Isolation (internal-sftp) and Resource Controls (limits.conf / cgroups)

Not every collaborator requires terminal access. When onboarding an external graphic designer (for instance, during a Logo & Core Brand Identity project) or a content coordinator who strictly uploads media assets via FileZilla or Cyberduck, avoid shell access altogether: assign the nologin shell in HestiaCP (/usr/local/hestia/bin/v-change-user-shell username nologin).

HestiaCP automatically appends a Match block for restricted users to the bottom of /etc/ssh/sshd_config. However, by default, that block still permits TCP port forwarding (AllowTcpForwarding yes), which would allow an SFTP-only user to turn their SSH client into a local SOCKS5 proxy to probe internal services!

Beyond filesystem isolation, static SSH keys should be systematically replaced with cryptographically signed, short-lived identities, as detailed in our guide on Zero-Trust SSH: Ephemeral Certificates with step-ca.

Harden SFTP configurations and user process limits with these two adjustments:

A. Hardening the internal-sftp Subsystem in /etc/ssh/sshd_config

Ensure the Match block in /etc/ssh/sshd_config disables network tunnels and terminal allocation for SFTP-only accounts:

# Block at the end of /etc/ssh/sshd_config for SFTP-only accounts without shell access
Match Group sftp-only
    ChrootDirectory %h
    ForceCommand internal-sftp -u 0022
    AllowTcpForwarding no
    AllowAgentForwarding no
    PermitTunnel no
    X11Forwarding no
    PermitTTY no

B. Blocking Fork Bombs and Memory Exhaustion (/etc/security/limits.d/)

What happens if a developer running code inside jailbash triggers an infinite loop in PHP-CLI or attempts a malicious fork bomb (:(){ :|:& };:) to exhaust server CPU cycles? A chroot jail isolates the filesystem, but does not constrain RAM allocation or thread counts on its own!

Deploy a PAM limits configuration (/etc/security/limits.d/90-hestia-jailkit.conf) to guarantee no sandboxed SSH account can exceed 64 simultaneous processes:

# File: /etc/security/limits.d/90-hestia-jailkit.conf
# Restrict process counts (nproc), open file descriptors (nofile), and virtual memory for hosting users
# Apply to individual accounts or the client SSH group
cliente_agencia soft nproc 40
cliente_agencia hard nproc 64
cliente_agencia soft nofile 4096
cliente_agencia hard nofile 8192
cliente_agencia hard as 2097152

Furthermore, enforce hidepid=2 (or hidepid=invisible) on /proc in /etc/fstab so that unprivileged users can inspect only processes matching their exact UID:

# Remount /proc immediately with hidepid=invisible (unprivileged users see strictly their own processes)
sudo mount -o remount,rw,nosuid,nodev,noexec,relatime,hidepid=invisible /proc

5. Comparative Matrix: HestiaCP SSH Access Modes by Risk Profile

The following matrix compares the three access tiers available on a HestiaCP server and their real-world security impact during technical audits:

Security Dimension / Operation Default Shell (/bin/bash Global) Restricted SFTP (nologin + internal-sftp) Webxtek Standard (jailbash + jk_init + limits.d)
Visibility of Sister Clients (/etc/nginx) Exposed (can enumerate all hosted domains) 0% Visible (confined to /home/user with no shell) 0% Visible (airtight chroot under /home/user)
Tooling Support (git, composer, wp-cli) Yes (with unrestricted access to all host binaries) No (strictly SFTP file transfers) Yes (strictly binaries declared in [webxtek_dev])
Attack Utilities (nc, nmap, gcc, python) Accessible if present on host system Inaccessible Non-existent inside the jail
Protection Against SOCKS Tunnels / Port Forwarding Permitted by default Blocked via AllowTcpForwarding no Blocked via SSH group policy and host firewall
Protection Against Process Exhaustion (Fork Bomb) Vulnerable without PAM/cgroups limits Immune (PermitTTY no) Hardened (max 64 processes via limits.d + hidepid)

This architecture enables DevOps & Cloud Architecture Specialists to safely collaborate with third-party developers on High-Performance Websites while maintaining the institutional security assurances of our enterprise Managed Hosting platforms.


6. Jail Maintenance (jk_update) and Practical 5-Step Checklist

When operating Linux chroot jails, remember a fundamental rule: whenever you upgrade packages on the host operating system with apt update && apt upgrade (for instance, applying a critical patch to PHP 8.4), you must synchronize updated binaries into existing jails using jk_update.

Run this command or hook it into your post-upgrade automation:

# Synchronize updated binaries and shared libraries from host into user chroot jail
sudo jk_update -j "/home/${HESTIA_USER}"

Before delivering SSH credentials to an external contractor, verify these five steps:

  1. Verify Jail Confinement as the User: Connect via SSH using the developer account, execute pwd, run cd / && ls -la, and verify that you see strictly the internal hierarchy of /home/username, with zero access to sister tenant folders.
  2. Validate TLS Handshakes for Composer and Git: Inside the sandboxed session, execute composer diagnose and git ls-remote https://github.com/wp-cli/wp-cli.git to confirm that DNS resolution and /etc/ssl/certs bundles operate reliably within the chroot.
  3. Verify Jail Root Permissions (0755 root:root): Execute ls -ld /home/username on the host server. The apex jail directory must be owned by root:root with 0755 permissions (the user writes strictly inside subdirectories like web/, tmp/, and home/username/). If ownership is changed to the user, jk_chrootsh and internal-sftp reject authentication immediately.
  4. Automate Jail Package Synchronization (jk_update): Deploy a weekly cron script at /etc/cron.weekly/update-hestia-jails that executes jk_update -j across all /home/* directories containing an active /usr/bin folder.
  5. Revoke Shell Access at Project Milestones: As soon as an external agency completes their deployment sprint, run /usr/local/hestia/bin/v-change-user-shell username nologin to disable interactive terminal access until the next maintenance window.

Empowering development teams should never require surrendering the keys to your entire server infrastructure. At Webxtek Studio, we engineer Linux permission boundaries so you can scale multi-vendor projects without compromising operational integrity. Through our core services in enterprise Managed Hosting, engineering of High-Performance Websites, and strategic Logo & Core Brand Identity, we ensure your digital operations grow on an impenetrable, high-speed foundation.

[+] [ INDEX_DIRECTORY ] 19 topics ▼
[ SYSTEM.FAQ ]

Frequently Asked Questions

What is the actual security risk of assigning a standard /bin/bash shell to an external developer on a HestiaCP server?

A standard /bin/bash shell grants comprehensive read access across the global Linux filesystem. Even without root privileges, that user can read files in /etc/nginx/conf.d/ to enumerate all other client domains hosted on the machine, inspect running processes via /proc, probe internal loopback listening ports (127.0.0.1), and search for misconfigured file permissions across sister directories.

How does Jailkit (jk_chrootsh) isolate a user in HestiaCP without breaking development tools like Git, Composer, or WP-CLI?

The SUID binary jk_chrootsh executes the chroot() system call upon SSH login, redefining /home/username as the absolute root (/) for that session. Because a jailed process cannot access /usr/bin outside its jail, the jk_init utility copies strictly authorized binaries (such as php8.4, git, composer, and wp) along with their required dynamic shared libraries (.so) into the user jail directory.

Why do PHP-CLI and Git fail on HTTPS connections inside an improperly configured chroot jail?

Tools communicating over TLS and performing DNS lookups depend on system network configuration files (/etc/resolv.conf, /etc/hosts, /etc/nsswitch.conf), GNU C Library name resolution libraries (libnss_dns.so, libnss_files.so), and CA root certificate bundles (/etc/ssl/certs/ca-certificates.crt). If you omit the netbasics section and SSL certificates from /etc/jailkit/jk_init.ini, Composer and Git fail TLS certificate validation against GitHub and Packagist.

What is the operational difference between OpenSSH ChrootDirectory internal-sftp with a nologin shell and HestiaCP jailbash?

The internal-sftp subsystem paired with a nologin shell completely disables interactive shell execution, permitting strictly file transfers over SFTP (ideal for designers and content editors). In contrast, jailbash uses Jailkit to provide a confined interactive terminal, built specifically for developers who must run command-line tools like composer install, wp cache flush, or git pull without exposing the rest of the server.

> START_PROJECT

Need a website that earns trust, ranks in search, and gives your business a stronger digital presence? Start the conversation here.