Score:2

How to allow SSH chroot user to restart apache on host?

cc flag

My first post here on Ask Ubuntu.

I have an LAMP Ubuntu server which has two vhosts defined in /etc/apache2/sites-enabled.

One of these sites (an external mirror site) must be maintained by an external administrator who uses SSH with RSA Key Authentication to log in. To do this, we gave the remote user sudo privileges.

Due to enhanced security policies within our organisation, they are no longer permitted to access the server by SSH, which necessarily grants access to the full filesystem.

The solution we are implementing, is to create a chroot environment under /home/chroot using debootstrap, where we have installed a simple environment which the external user can SSH into.

We have mounted some drives on the host filesystem to allow them access to the required web directories:

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

(These mounts are made permanent on reboot in fstab)

In this way, the user can upload files via SSH to their ...www/mirror_site directory and make modifications to their mirror_site.conf vhost file. They can also add certificates into the /etc/apache2/ssh directory. As the path names are consistent between the host filesystem and the chroot directory, then the path names given in the mirror_site.conf will always work.

So far so good. The chroot works as expected and Apache can create the vhost successfully.

The only issue I have is that when the external user has finished modifying their files and configuration, they cannot restart/reload apache. The systemctl commands do not work inside the chroot (because systemd is not installed to the chroot) and even though I wrote a script which I placed on the host filesystem to restart apache which calls systemctl, the path inside the script is still jailed to the chroot filesystem.

!#bin/bash
# a2reload.sh

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

The above runs fine when executed as root from the host console, but cannot find systemd when executed by the chroot user via SSH.

So my question is, how to allow the external chroot()ed user to restart/reload apache2 or run a2ensite / a2dissite on the host filesystem after making their site modifications?

Your guidance would be appreciated.

vidarlo avatar
om flag
Why would they have to restart apache?
Andy Woolford avatar
cc flag
Because they might change something that causes Apache to stop.
Andy Woolford avatar
cc flag
True. I see your point. However, they still need to reload apache when they change the site conf file. This could cause it to stop (if there was a typo). Then they would need to restart. The question applies to both reloading and restarting, and running a2ensite and a2dissite by a chroot()ed user.
muru avatar
us flag
"I wrote a script which ... is still jailed to the chroot filesystem" - so what you can do is set up a script on the host system which watches a directory shared with the chroot, and then restarts Apache when it detects changes in that directory. (Also the script you have provided in the question is nonsense and doesn't do anything useful aside from echoing some lines)
Andy Woolford avatar
cc flag
@muru Thanks for the suggestion. I will try that. As for the bash script, it runs the systemctl command, the output of which is written to the RESULT variable. The result is then echoed to the console. So executing `.\a2reload` is the equivalent of `systemctl reload apache2.service`. The script runs as expected on the host as root but not in the chroot. I will try your suggestion and report back here.
muru avatar
us flag
No, it doesn't. The line `RESULT = systemctl reload apache2.service` tries to `RESULT` as a command with the rest as arguments. You'd want `RESULT=$(systemctl reload apache2.service)`. But even that's unnecessary if you're just echoing the variable the very next line.
Andy Woolford avatar
cc flag
You are right. Sorry my mistake. Typed from memory. I have edited the question now.
Andy Woolford avatar
cc flag
The good news is your suggestion works and I have written a suite of scripts which encrypt the username and command into a hidden file, which is written to a common mount point. The host runs a script at boot time which monitors this folder and reads the hash value from the file, decrypts it, validates the user and the command and then executes $(systemctl $CMD apache2.service). Thanks for your help and patience.
muru avatar
us flag
Excellent! Maybe you can post your scripts, suitably redacted, as an answer to help future users
Andy Woolford avatar
cc flag
Yes, I'm in the process of fine tuning atm. I will post one example of each script when done along with a description. Thanks again for your help.
Score:1
cc flag

The answer to this little conundrum, as helpfully suggested by @muru, is to have a monitor program running as root. This is designed to parse a token file (.a2kickme) which is created by a script invoked by the chroot user in a mounted folder, (visible to both the host and chroot filesystems), and which contains the chroot username and a 'keyword' which relates to the command that the chroot user is trying to run.

To prevent anyone from just creating such a file by hand and inserting any command, this is prevented by two layers of protection.

  1. The token file is .hidden and the contents are encrypted using a hash algorithm. The random password for this is contained in a hidden .secret file with permissons 640, in another shared location.

  2. The monitor program decrypts the single hash in the token file to extract the username of the user who wrote it and compare the keyword instruction to an array. If the keyword does not appear in the array, then the instruction is ignored.

The particular application for my purposes, is to allow a chroot user to modify the vhost htdocs for their own site, along with the apache site.conf configuration and SSL certificates. This was easy enough to achieve with the use of mount points using -B (bind) in /etc/fstab and I will not need to discuss here.

Setting up the chroot and creating the scripts to achieve the above, however, is described below in the hope that others may adapt some of the principles employed for their own use.

Create Chroot Directory:

$ 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 ...'

Create New Chroot Owner and Group:

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

Add the following Lines to /etc/ssh/sshd_config: (Recommended to use external editor for initial setup)

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

Create a new file to display the chroot name in SSH prompt:

(Chroot user prompt will appear as: (chrt){username}@myserver:~$ )

$ sudo touch /home/chrt/etc/debian_chroot

...and add the following entry: chrt

Install basic Chroot environment: Note: this environment will not contain any tools (such as sudo, systemd, chmod, chown, nano or vim for example) other than the basic Bash commands. Tools can be added by the root user using the chroot /home/chrt /bin/bash command to enter the chroot and then running apt-get as needed.

To create the basic chroot environment:

$ 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

Create New SSH User and include in chrt group: (Users in chrt group will always be chroot()ed by sshd)

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

Copy passwd and group files from host to chroot filesystem (Do this every time there is a change to the users and groups.)

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

Create symlink from host home directory so ssh keys work:

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

Note: This is because of sshd_config: 'AuthorizedKeysFile %h/.ssh/authorized_keys' - where sshd expects %h to be /home/{username} PASSWORDS ARE DISABLED FOR ALL SSH USERS, so a key pair is required for authentication

Create ssh public/private key pair using PuTTYgen

Copy public key to /home/chrt/home/{username}/.ssh/authorized_keys Copy private key (keyfile.ppk) to your SSH Client

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

Create .secret file in /path/to/shared/chrt (It can be any mounted shared location, but ensure this location is used in all the scripts that need it)

$ 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   

Create Scripts: Install into: /home/chrt/usr/bin/

a2cleanup 750 root:chrt < usage: a2cleanup [-f] (removes /var/www/chrt/.a2kickme with user prompt. Forces removal with -f flag)

a2reload 750 root:chrt < reloads apache (equivalent to systemctl reload apache2.service)

a2restart 750 root:chrt < restarts apache (equivalent to systemctl restart apache2.service)

a2start 750 root:chrt < starts apache (equivalent to systemctl start apache2.service)

a2status 750 root:chrt < displays apache status (equivalent to systemctl status apache2.service)

a2enable 750 root:chrt < enables site.conf (equivalent to $ sudo a2ensite site.conf. Note a2ensite will NOT take any arguments in chroot)

a2disable 750 root:chrt < disables site.conf (equivalent to $ sudo a2dissite site.conf. Note a2dissite will NOT take any arguments in chroot)

All of the above 'command' files are based on the same code, with the only difference being the $CMD variable in the definitions section. I reproduce one of them below:

!/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

The a2cleanup file, performs the function of removing the token file .a2kickme. It may be run interactively, requiring a user prompt to remove, or with the -f flag to force silent removal. It is called by a2monitor.sh after executing the requested command, but can be run manually if the token file is stuck in the folder for some reason. The above command file will prompt the user to do this should that happen.

#!/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

The a2monitor.sh file is located in the host filesystem root directory. It is NOT accessible by the chroot user. In my application, a2monitor.sh watches /var/www/chrt and parses .a2kickme using the .secret password to recover the encrypted username and $CMD. It is only executable by root, but nevertheless it double checks to ensure the $USER system variable which invoked it is "root", (in case of tampering), and will ignore all commands if not. It also checks to make sure that the $CMD is valid before executing as root. The script will start at boot time and loop every x seconds. I have this set to 1 second in the TLOOP variable, but YMMV. Can be run manually if stopped with $ sudo systemctl start a2monitor.service. a2cleanup -f is invoked by a2monitor when it completes, to silently remove the temporary file.

To adjust the loop delay:

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

The a2monitor.sh script will also pipe the output of any command back to the console of the user who invoked it.

#!/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

Create a .service file in /etc/systemd/system/a2monitor.service:

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

Edit the file and add the following:

[Unit]
Description=Apache functions for chroot

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

[Install]
WantedBy=multi-user.target

Then enable it:

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

The service may now be started manually with:

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

Given that the service is enabled, it should start on reboot of the host machine.

Now that these scripts are installed, SSH into the console of the chroot user and verify that the user is actually chrooted. (chrt){username}@myserver:~$ cd / should take you to the root of the chroot, not the root of the host. You can confirm this with a simple listing (chrt){username}@myserver:~$ ls and the immutable file "YOU_ARE_CHROOT" should be in this list. The user prompt should also start with (chrt).

Assuming you have created the "a2status" script above and placed it in the CHROOT/usr/bin/ directory, then typing (chrt){username}@myserver:~$ a2status should be the equivalent of root@myserver:~# systemctl status apache2.service and the output should appear on the chroot user console after a short delay of approximately the TLOOP time.

I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.