Jail the control-panel terminal to the customer's home (chroot)
Deploy Ladill Hosting / deploy (push) Successful in 45s
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:
co-authored by
Claude Opus 4.8
parent
e86f95ddc7
commit
2a1228b40e
@@ -131,7 +131,10 @@ class RunHostingTerminalWorkerCommand extends Command
|
|||||||
$launchCommand = $this->buildUserShellBootstrap($account, $relativePath, $cols, $rows);
|
$launchCommand = $this->buildUserShellBootstrap($account, $relativePath, $cols, $rows);
|
||||||
|
|
||||||
if ($this->shouldUseLocalExecution($account->node)) {
|
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);
|
$ssh = $provider->connect($account->node);
|
||||||
@@ -145,6 +148,27 @@ class RunHostingTerminalWorkerCommand extends Command
|
|||||||
return new RemoteSshShellSession($ssh, $remoteCommand, $cols, $rows);
|
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
|
private function buildUserShellBootstrap(HostingAccount $account, string $relativePath, int $cols, int $rows): string
|
||||||
{
|
{
|
||||||
$homeDirectory = "/home/{$account->username}";
|
$homeDirectory = "/home/{$account->username}";
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// Path to the jailed-shell launcher used by the control-panel terminal. It
|
||||||
|
// chroots a customer into the jailkit jail (only their own home visible).
|
||||||
|
// Provisioned on the host (see deployment/ladill-jailsh + sudoers).
|
||||||
|
'terminal_jail_launcher' => env('HOSTING_TERMINAL_JAIL_LAUNCHER', '/usr/local/sbin/ladill-jailsh'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Central developer access
|
| Central developer access
|
||||||
|
|||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ladill jailed shell for the hosting browser terminal. Confines a hosting
|
||||||
|
# account to /home/jail with only its own home bind-mounted (private mount ns).
|
||||||
|
# Usage: ladill-jailsh <username> [relpath]
|
||||||
|
set -euo pipefail
|
||||||
|
JAIL=/home/jail
|
||||||
|
U=${1:-}
|
||||||
|
REL=${2:-/public_html}
|
||||||
|
[ -n "$U" ] || { echo 'usage: ladill-jailsh <user> [relpath]'; exit 2; }
|
||||||
|
# resolve + validate the target user (must be a real hosting account)
|
||||||
|
UID_=$(id -u "$U" 2>/dev/null || true)
|
||||||
|
GID_=$(id -g "$U" 2>/dev/null || true)
|
||||||
|
[ -n "$UID_" ] && [ "$UID_" -ge 1000 ] || { echo 'refusing: not a hosting user'; exit 1; }
|
||||||
|
HOMEDIR=/home/$U
|
||||||
|
[ -d "$HOMEDIR" ] || { echo 'no home directory'; exit 1; }
|
||||||
|
case "$REL" in *..*) REL=/public_html;; esac
|
||||||
|
mkdir -p "$JAIL/home/$U"
|
||||||
|
# re-exec into a private mount namespace so the bind is per-session
|
||||||
|
if [ -z "${LADILL_JAIL_NS:-}" ]; then
|
||||||
|
exec env LADILL_JAIL_NS=1 unshare --mount --propagation private -- "$0" "$U" "$REL"
|
||||||
|
fi
|
||||||
|
mount --bind "$HOMEDIR" "$JAIL/home/$U"
|
||||||
|
export HOME="/home/$U" USER="$U" LOGNAME="$U" SHELL=/bin/bash PATH=/usr/bin:/bin
|
||||||
|
export TERM="${TERM:-xterm-256color}"
|
||||||
|
exec /usr/sbin/chroot --userspec="$UID_:$GID_" "$JAIL" /bin/bash -l -c "cd '/home/$U$REL' 2>/dev/null || cd ~; exec /bin/bash -l"
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Provision the jailkit chroot used by the control-panel browser terminal so the
|
||||||
|
# customer shell is confined to its own home (cannot see /var/www, other tenants,
|
||||||
|
# /etc, or secrets). Idempotent; run as root on each hosting node.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
JAIL=/home/jail
|
||||||
|
|
||||||
|
echo "==> Installing jailkit"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
command -v jk_init >/dev/null 2>&1 || apt-get install -y jailkit
|
||||||
|
|
||||||
|
echo "==> Building jail tree at $JAIL"
|
||||||
|
jk_init -j "$JAIL" uidbasics netbasics basicshell interactiveshell extendedshell editors netutils terminfo || true
|
||||||
|
jk_cp -j "$JAIL" /usr/bin/id /usr/bin/whoami /usr/bin/find /usr/bin/grep /usr/bin/less \
|
||||||
|
/usr/bin/head /usr/bin/tail /usr/bin/du /usr/bin/df /usr/bin/wc /usr/bin/clear /usr/bin/sed /usr/bin/awk || true
|
||||||
|
|
||||||
|
# Terminal definitions (xterm*) for a usable TTY.
|
||||||
|
mkdir -p "$JAIL/usr/share/terminfo"
|
||||||
|
cp -rn /usr/share/terminfo/x "$JAIL/usr/share/terminfo/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Non-listable jail /home (each session bind-mounts only its own home here).
|
||||||
|
install -d -o root -g root -m 711 "$JAIL/home"
|
||||||
|
install -d -m 1777 "$JAIL/tmp"
|
||||||
|
|
||||||
|
echo "==> Syncing hosting users into the jail passwd/group (for name resolution)"
|
||||||
|
while IFS=: read -r name _ uid gid _ home shell; do
|
||||||
|
[ "$uid" -ge 1000 ] 2>/dev/null || continue
|
||||||
|
[ -d "/home/$name" ] || continue
|
||||||
|
grep -q "^$name:" "$JAIL/etc/passwd" || getent passwd "$name" >> "$JAIL/etc/passwd"
|
||||||
|
grep -q "^$name:" "$JAIL/etc/group" || getent group "$name" >> "$JAIL/etc/group"
|
||||||
|
done < <(getent passwd)
|
||||||
|
|
||||||
|
echo "==> Installing the jailed-shell launcher + scoped sudoers"
|
||||||
|
install -o root -g root -m 755 "$(dirname "$0")/ladill-jailsh" /usr/local/sbin/ladill-jailsh
|
||||||
|
echo 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/ladill-jailsh *' > /etc/sudoers.d/ladill-jailsh
|
||||||
|
chmod 440 /etc/sudoers.d/ladill-jailsh
|
||||||
|
visudo -cf /etc/sudoers.d/ladill-jailsh
|
||||||
|
|
||||||
|
echo "==> Done. Customer terminals now run jailed in $JAIL."
|
||||||
Reference in New Issue
Block a user