Jail the control-panel terminal to the customer's home (chroot)
Deploy Ladill Hosting / deploy (push) Successful in 45s

SECURITY: the browser terminal ran an unconfined login shell as the account's
system user, so customers could browse the whole host (/var/www, other tenants,
/etc). Run it inside a jailkit chroot instead: a vetted, sudo-scoped launcher
(/usr/local/sbin/ladill-jailsh) enters a private mount namespace, bind-mounts
ONLY that user's home into /home/jail, and chroots as the user. They can no
longer see anything outside their own home. Provisioning committed in
deployment/ (setup-terminal-jail.sh + ladill-jailsh) so it is reproducible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
isaacclad
2026-06-26 23:41:18 +00:00
co-authored by Claude Opus 4.8
parent e86f95ddc7
commit 2a1228b40e
4 changed files with 95 additions and 1 deletions
@@ -131,7 +131,10 @@ class RunHostingTerminalWorkerCommand extends Command
$launchCommand = $this->buildUserShellBootstrap($account, $relativePath, $cols, $rows);
if ($this->shouldUseLocalExecution($account->node)) {
return new LocalPtyShellSession($this->buildLocalLaunchCommand($account, $launchCommand));
// Confine the customer to a jailkit chroot (only their own home is
// visible) via the vetted, sudo-scoped launcher. SECURITY: never run
// an unjailed login shell here — it exposes the whole host filesystem.
return new LocalPtyShellSession($this->buildJailedLaunchCommand($account, $relativePath));
}
$ssh = $provider->connect($account->node);
@@ -145,6 +148,27 @@ class RunHostingTerminalWorkerCommand extends Command
return new RemoteSshShellSession($ssh, $remoteCommand, $cols, $rows);
}
/**
* Local jailed shell: run the customer inside the jailkit chroot
* (config('hosting.terminal_jail_launcher')) as their own user. The launcher
* derives uid/gid, binds only that user's home into the jail, and chroots
* so the customer can never see the host filesystem (/var/www, other homes,
* /etc, secrets). Invoked via sudo -n, scoped in /etc/sudoers.d/ladill-jailsh
* because the terminal worker runs as www-data.
*/
private function buildJailedLaunchCommand(HostingAccount $account, string $relativePath): string
{
$launcher = (string) config('hosting.terminal_jail_launcher', '/usr/local/sbin/ladill-jailsh');
$rel = $relativePath !== '' ? $relativePath : '/public_html';
return implode(' ', [
'sudo', '-n',
escapeshellarg($launcher),
escapeshellarg($account->username),
escapeshellarg($rel),
]);
}
private function buildUserShellBootstrap(HostingAccount $account, string $relativePath, int $cols, int $rows): string
{
$homeDirectory = "/home/{$account->username}";