如何允许 SSH chroot 用户在主机上重新启动 apache?

如何允许 SSH chroot 用户在主机上重新启动 apache?

这是我在 Ask Ubuntu 上的第一篇帖子。

我有一台 LAMP Ubuntu 服务器,它在 /etc/apache2/sites-enabled 中定义了两个 vhost。

其中一个站点(外部镜像站点)必须由使用带有 RSA 密钥身份验证的 SSH 登录的外部管理员维护。为此,我们授予远程用户 sudo 权限。

由于我们组织内部加强了安全策略,他们不再被允许通过 SSH 访问服务器,而这必然会授予对整个文件系统的访问权限。

我们正在实施的解决方案是使用 debootstrap 在 /home/chroot 下创建一个 chroot 环境,我们已经在其中安装了一个简单的环境,外部用户可以通过 SSH 进入。

我们在主机文件系统上安装了某些驱动器,以允许它们访问所需的 Web 目录:

mount -B /var/www/mirror_site /home/chroot/var/www/mirror_site
mount -B /etc/apache2/sites-available /home/chroot/etc/apache2/sites-available
mount -B /etc/apache2/sites-enabled /home/chroot/etc/apache2/sites-enabled
mount -B /etc/apache2/ssh /home/chroot/etc/apache2/ssh

(这些安装在 fstab 中重启后将永久生效)

这样,用户可以通过 SSH 将文件上传到他们的 ...www/mirror_site 目录并修改他们的 mirror_site.conf vhost 文件。他们还可以将证书添加到 /etc/apache2/ssh 目录中。由于主机文件系统和 chroot 目录之间的路径名一致,因此 mirror_site.conf 中给出的路径名将始终有效。

到目前为止一切顺利。chroot 工作正常,Apache 可以成功创建 vhost。

我遇到的唯一问题是,当外部用户修改完文件和配置后,他们无法重新启动/重新加载 apache。systemctl 命令在 chroot 中不起作用(因为 systemd 未安装到 chroot),尽管我编写了一个脚本并将其放在主机文件系统上以重新启动调用 systemctl 的 apache,但脚本中的路径仍然被限制在 chroot 文件系统中。

!#bin/bash
# a2reload.sh

echo 'reloading apache'
RESULT=$(systemctl reload apache2.service)
echo $RESULT
echo 'apache reloaded'

当从主机控制台以 root 身份执行时,上述操作运行良好,但当通过 SSH 由 chroot 用户执行时,无法找到 systemd。

所以我的问题是,如何允许外部 chroot() 用户在对站点进行修改后重新启动/重新加载 apache2 或在主机文件系统上运行 a2ensite / a2dissite?

谢谢您的指导。

答案1

正如 @muru 所建议的,解决这个小难题的方法是让一个监控程序以 root 身份运行。这是为了解析一个令牌文件 (.a2kickme),该文件由 chroot 用户在已安装的文件夹中调用的脚本创建(对主机和 chroot 文件系统都可见),其中包含 chroot 用户名和与 chroot 用户尝试运行的命令相关的“关键字”。

为了防止任何人手动创建这样的文件并插入任何命令,我们通过两层保护来阻止这种情况。

  1. 令牌文件为 .hidden,其内容使用哈希算法加密。其随机密码包含在另一个共享位置的隐藏 .secret 文件中,该文件具有 640 的权限。

  2. 监控程序解密 token 文件中的单个哈希,以提取编写它的用户的用户名,并将关键字指令与数组进行比较。如果关键字没有出现在数组中,则忽略该指令。

对于我来说,这个特定应用程序允许 chroot 用户修改他们自己站点的 vhost htdocs,以及 apache site.conf 配置和 SSL 证书。通过在 /etc/fstab 中使用 -B(绑定)使用挂载点,这很容易实现,我不需要在这里讨论。

但是,下面将介绍如何设置 chroot 并创建脚本来实现上述操作,希望其他人能够调整其中的一些原则以供自己使用。

创建 Chroot 目录:

$ sudo mkdir -p /home/chrt  755 root:root   # This *MUST* be owned by root and writeable by nobody else.
$ sudo touch /home/chrt/YOU_ARE_CHROOT      # This dummy file will appear in any listing (ls) of the root directory.
$ sudo chmod 644 /home/chrt/YOU_ARE_CHROOT  # Protects the file from being removed by chroot user.
$ sudo chattr +i /home/chrt/YOU_ARE_CHROOT  # Make the file immutable so that not even root can delete or modify. To unset use '$ sudo chattr -i ...'

创建新的 Chroot 所有者和组:

$ sudo useradd -r chrt # System User, No Login
$ sudo usermod -d /nonexistent chrt # No Home Directory
$ sudo usermod -s /bin/false chrt # No Shell

将以下行添加到/etc/ssh/sshd_config:(建议使用外部编辑器进行初始设置)

#define group to apply chroot jail to
Match group chrt
#specify chroot jail
    ChrootDirectory /home/chrt
    AllowTcpForwarding no
    AllowAgentForwarding no
    PermitTunnel no
    X11Forwarding no

创建一个新文件以在 SSH 提示符中显示 chroot 名称:

(Chroot 用户提示符将显示为(chrt){username}@myserver:~$:)

$ sudo touch /home/chrt/etc/debian_chroot

...并添加以下条目:chrt

安装基本的 Chroot 环境: 注意:此环境不包含除基本 Bash 命令之外的任何工具(例如 sudo、systemd、chmod、chown、nano 或 vim)。root 用户可以使用命令chroot /home/chrt /bin/bash进入 chroot,然后根据需要运行 apt-get 来添加工具。

要创建基本的 chroot 环境:

$ sudo apt-get update
$ sudo apt-get install debootstrap
$ sudo debootstrap --variant=buildd jammy /home/chrt  < installs Ubuntu Jammy Jellyfish - change to any supported distro shortname

创建新的 SSH 用户并将其纳入 chrt 组: (chrt 组中的用户将始终通过 sshd 进行 chroot())

$ sudo useradd -s /bin/bash -d /home/chrt/home/{username}/ -m -G chrt {username}

将密码和组文件从主机复制到 chroot 文件系统(每次用户和组发生变化时都执行此操作。)

$ sudo cp /etc/{passwd, group} /home/chrt/etc/

从主机主目录创建符号链接,以便 ssh 密钥可以起作用:

$ sudo ln -s /home/chrt/home/{username} /home/

注意:这是因为 sshd_config: 'AuthorizedKeysFile %h/.ssh/authorized_keys' - 其中 sshd 期望 %h 为 /home/{username} 所有 SSH 用户的密码均已禁用,因此需要密钥对进行身份验证

使用以下方式创建 ssh 公钥/私钥对 PuTTYgen

将公钥复制到 /home/chrt/home/{username}/.ssh/authorized_keys 将私钥(keyfile.ppk)复制到您的 SSH 客户端

$ sudo chmod 644 /home/chrt/home/{username}/.ssh/authorized_keys
$ sudo chown {username}:{username} /home/chrt/home/{username}/.ssh/authorized_keys

在以下位置创建 .secret 文件/path/to/shared/chrt(可以是任何已安装的共享位置,但要确保所有需要它的脚本都使用此位置)

$ sudo touch .secret    # < Add a random password after creation    
$ chmod 640 root:chrt  /path/to/shared/chrt/.secret
# Make the file immutable so that not even root can delete or modify. 
# To unset use '$ sudo chattr -i ...'   
$ sudo chattr +i /path/to/shared/chrt/.secret   

创建脚本: 安装到:/home/chrt/usr/bin/

a2cleanup 750 root:chrt < 用法:a2cleanup [-f](使用用户提示删除 /var/www/chrt/.a2kickme。使用 -f 标志强制删除)

a2reload 750 root:chrt < 重新加载 apache (相当于 systemctl reload apache2.service)

a2restart 750 root:chrt < 重新启动 apache (相当于 systemctl restart apache2.service)

a2start 750 root:chrt < 启动 apache(相当于 systemctl start apache2.service)

a2status 750 root:chrt < 显示 apache 状态(相当于 systemctl status apache2.service)

a2enable 750 root:chrt < 启用 site.conf(相当于 $ sudo a2ensite site.conf。注意 a2ensite 在 chroot 中不会接受任何参数)

a2disable 750 root:chrt < 禁用 site.conf(相当于 $ sudo a2dissite site.conf。注意 a2dissite 在 chroot 中不会接受任何参数)

上述所有“命令”文件都基于相同的代码,唯一的区别是定义部分中的 $CMD 变量。我在下面重现了其中一个:

!/bin/bash
# Written by Andy Woolford
# CHROOT/usr/bin/a2status

FILE=/var/www/chrt/.a2kickme
SECRET_FILE=/path/to/shared/chrt/.secret
MSGB=$'already exists.  Another process may be using this file.\n\nTry again later, or run ./a2cleanup'
MSG="The file: $FILE $MSGB"
CMD="status"
U=${SUDO_USER:-${USER}}
if [ -f "$FILE" ]; then
    echo "$MSG"
else
    touch $FILE
    SECRET=($(<$SECRET_FILE))
    echo "$U $CMD" | openssl enc -aes-256-cbc -md sha512 -a -pbkdf2 -iter 100000 \
-salt -pass pass:"$SECRET" > $FILE
fi
exit 0

a2cleanup 文件执行删除令牌文件 .a2kickme 的功能。它可以以交互方式运行,需要用户提示删除,或使用 -f 标志强制静默删除。执行请求的命令后,它会由 a2monitor.sh 调用,但如果令牌文件因某种原因卡在文件夹中,则可以手动运行。如果发生这种情况,上述命令文件将提示用户执行此操作。

#!/bin/bash
# Written by Andy Woolford
# CHROOT/usr/bin/a2cleanup

U=${SUDO_USER:-${USER}}
FILE=/var/www/chrt/.a2kickme
SECRET_FILE=/path/to/shared/chrt/.secret
SUCCESS="$FILE was removed."
NOOP="Nothing to do. Exiting cleanly."
CANCEL="Operaton cancelled by user."
SMH="Please answer y or n."
PROMPTB=$'already exists.  Another process may be using this file.\n\nAre you sure you want to remove it? y/n: '
PROMPT="The file: $FILE $PROMPTB"
DENIED="Access Denied!  To force cleanup, use a2cleanup -f"
EXISTS=false
[ -f "$FILE" ] && EXISTS=true

# Exit if nothing to do
if ! $EXISTS; then
    echo $NOOP
    exit 0
fi

# Force deletion of the target file when invoked with -f flag, regardless
while getopts "f" flag; do
    case "${flag}" in
    f)  if $EXISTS; then
            rm $FILE
            echo $SUCCESS
        else
            echo $NOOP
        fi
        exit 0;;
    esac
exit 0
done
    
F_READ=($(<$FILE))
SECRET=($(<$SECRET_FILE))

# Ensure that the user deleting the file is the same as the user who created it
if [ "$F_READ" != "" ] && [ "$SECRET" != "" ]; then
    F_USER=$(echo "$F_READ" | openssl enc -aes-256-cbc -md sha512 -a -d -pbkdf2 -iter 100000 \
    -salt -pass pass:"$SECRET" | awk '{print $1}')

# If the user is not the same, then refuse to delete it unless run with -f flag
    if [ "$F_USER" != "$U" ] && [ "$U" != "root" ]; then
        echo $DENIED
        exit 1
    else
        while true; do
            read -p "$PROMPT" YN
            case $YN in
                [Yy]) rm $FILE
                      echo $SUCCESS
                      break;;
                [Nn]) echo $CANCEL; break;;
                *) echo $SMH;;
            esac
        done
    fi
else
# If either the target file or the secret file is blank then just delete it.
    rm $FILE
    echo $SUCCESS
fi
exit 0

a2monitor.sh 文件位于主机文件系统根目录中。它是不是chroot 用户可访问。在我的应用程序中,a2monitor.sh 监视 /var/www/chrt 并使用 .secret 密码解析 .a2kickme 以恢复加密的用户名和 $CMD。它只能由 root 执行,但它会进行双重检查以确保调用它的 $USER 系统变量是“root”(以防篡改),如果不是,则会忽略所有命令。它还会检查以确保在以 root 身份执行之前 $CMD 有效。该脚本将在启动时启动并每 x 秒循环一次。我在 TLOOP 变量中将其设置为 1 秒,但 YMMV。如果使用 停止,可以手动运行$ sudo systemctl start a2monitor.service。a2monitor 在完成时调用 a2cleanup -f,以静默删除临时文件。

要调整循环延迟:

TLOOP=1  # Set to 0 to exit after one pass only for testing

a2monitor.sh 脚本还会将任何命令的输出通过管道传回调用该命令的用户的控制台。

#!/bin/bash
# Written by Andy Woolford
# /root/a2monitor.sh

U=$USER
MESG=$(mesg | awk '{print $2}')
FILE=/var/www/chrt/.a2kickme
LOGFILE=/var/log/apache2/chrt/a2functions.log
SECRET_FILE=/path/to/shared/chrt/.secret
CLEANUP="/home/chrt/usr/bin/a2cleanup -f"
COMMANDS="status start restart reload enable disable"
CMD_ES="$(which a2ensite) site.conf"
CMD_DS="$(which a2dissite) site.conf"
CMD_SD="$(which systemctl)"
SD_SVC="apache2.service"
CURRENTDATE=`date`          
TLOOP=1  # Set to 0 to exit after one pass only for testing

function isNotIn {
    LIST=$1
    VALUE=$2
    ! [[ $LIST =~ (^| )$VALUE($| ) ]]
}

# Loop every TLOOP seconds
while true; do
    EXISTS=false
    [ -f "$FILE" ] && EXISTS=true
    if $EXISTS; then
        F_READ=($(<$FILE))
        SECRET=($(<$SECRET_FILE))
        
        # Ensure that valid data exists and decrypt it
        if [ "$F_READ" != "" ] && [ "$SECRET" != "" ]; then
            F_USER=$(echo "$F_READ" | openssl enc -aes-256-cbc -md sha512 -a -d -pbkdf2 -iter 100000 \
         -salt -pass pass:"$SECRET" | awk '{print $1}')
            F_CMD=$(echo "$F_READ" | openssl enc -aes-256-cbc -md sha512 -a -d -pbkdf2 -iter 100000 \
         -salt -pass pass:"$SECRET" | awk '{print $2}')

            # Test if user and command are valid.
            # Log failures and cleanup. 
            # Continue monitoring.
            if isNotIn "$COMMANDS" $F_CMD || [ $U != "root" ]; then
                echo "$CURRENTDATE $U $F_CMD Not Authorised!" >> $LOGFILE
                $CLEANUP > /dev/null
                continue
            fi

            case $F_CMD in
                enable) CMD="$CMD_ES" 
                ;;
                disable) CMD="$CMD_DS" 
                ;;
                *) CMD="$CMD_SD $F_CMD $SD_SVC" ;;
            esac
            # Append the user and action to Log
            echo "$CURRENTDATE $U $F_CMD Success" >> $LOGFILE

            # Execute CMD and store result
            RESULT=$($CMD)
            $CLEANUP > /dev/null

            if [ "$RESULT" != "" ]; then
                # Ensure messages are enabled
                mesg y
                # Pipe the result to the user initiating the command
                echo "$RESULT" | write "$F_USER"
                # Reset messages to whatever it was before
                mesg $MESG
            fi
        else
            # If no valid data exists:  
            $CLEANUP > /dev/null
        fi
    fi
    # For testing
    if [ $TLOOP == 0 ]; then
        break
    else
        sleep $TLOOP
    fi
done

exit 0

在 /etc/systemd/system/a2monitor.service 中创建一个 .service 文件:

$ sudo touch /etc/systemd/system/a2monitor.service
$ sudo chmod 644 /etc/systemd/system/a2monitor.service

编辑文件并添加以下内容:

[Unit]
Description=Apache functions for chroot

[Service]
ExecStart=/bin/bash -c "/root/a2monitor.sh"

[Install]
WantedBy=multi-user.target

然后启用它:

$ sudo systemctl daemon-reload
$ sudo systemctl enable a2monitor.service

现在可以使用以下命令手动启动该服务:

$ sudo systemctl start a2monitor.service (the .service is optional)

鉴于该服务已启用,它应该在主机重启时启动。

现在这些脚本已安装完毕,请通过 SSH 进入 chroot 用户的控制台并验证用户是否确实已 chroot。 (chrt){username}@myserver:~$ cd /应该会将您带到 chroot 的根目录,而不是主机的根目录。您可以使用简单的列表来确认这一点(chrt){username}@myserver:~$ ls,并且不可变文件“YOU_ARE_CHROOT”应该在此列表中。用户提示符也应以 (chrt) 开头。

假设您已经创建了上述“a2status”脚本并将其放在 CHROOT/usr/bin/ 目录中,那么输入(chrt){username}@myserver:~$ a2status应该相当于root@myserver:~# systemctl status apache2.service,并且输出应该在大约 TLOOP 时间的短暂延迟后出现在 chroot 用户控制台上。

相关内容