Centos7 - Apache Chroot Jail

One method of hardening apache in centos7 is by running apache in a chroot jail.

We are going to use modsecurity to enforce the chroot jail, using the SecChrootDir command. Then, where possible, we will use read-only directory bindmounts to expose the system files apache needs to run to the jail.
#first we're going to install and configure apache 2.4 in centos7
yum install httpd mod_security -y;
#disable some apache modules
sed -i "s/^/### X ###/" /etc/httpd/conf.d/autoindex.conf
sed -i "s/^/### X ###/" /etc/httpd/conf.d/userdir.conf
sed -i "s/^/### X ###/" /etc/httpd/conf.d/welcome.conf
sed -i "s/^/### X ###/" /etc/httpd/conf.d/mod_security.conf
cat << EOT >> /etc/httpd/conf.d/mod_security.conf
<IfModule mod_security2.c>
    SecChrootDir "/var/chroot"
#disable references to /var/www
sed -i '/^<Directory ".var.www.html">/,/^<\/Directory>/ s/^/### X ###/'    /etc/httpd/conf/httpd.conf
sed -i '/^<IfModule alias_module>/,/^<\/IfModule>/ s/^/### X ###/'         /etc/httpd/conf/httpd.conf
sed -i '/^<Directory ".var.www.cgi-bin">/,/^<\/Directory>/ s/^/### X ###/' /etc/httpd/conf/httpd.conf
sed -i '/^<Directory ".var.www">/,/^<\/Directory>/ s/^/### X ###/'         /etc/httpd/conf/httpd.conf
sed -i 's|^DocumentRoot.*|### X ###DocumentRoot "/var/www/html/|'          /etc/httpd/conf/httpd.conf
cat << EOT >> /etc/httpd/conf/httpd.conf
ServerName localhost
<Directory "/var/chroot">
    AllowOverride None
    Require all granted
TraceEnable off
ServerSignature Off
Include /etc/httpd/sites.d/*.conf
#setup an example virtualhost
mkdir -p /etc/httpd/sites.d/
cat << 'EOT' >> /etc/httpd/sites.d/www.example.com.conf
<VirtualHost *:80>
    ServerName www.example.com
    ServerAlias example.com
    DocumentRoot /var/chroot/www.example.com
    ErrorLog  /var/log/httpd/error.log
    CustomLog /var/log/httpd/access.log combined
mkdir -p /var/chroot/www.example.com
#for selinux management, if needed
yum install policycoreutils-python -y;
semanage fcontext -a -s system_u -t httpd_config_t      '/etc/httpd/sites.d(/.*)?'
restorecon -R  /etc/httpd/sites.d
semanage fcontext -a -s system_u -t httpd_sys_content_t '/var/chroot/www.example.com(/.*)?'
restorecon -R  /var/chroot/www.example.com
##many websites need these selinux options
#setsebool -P httpd_can_network_connect_db 1
#setsebool -P httpd_can_network_connect 1
#setsebool -P httpd_can_sendmail 1
##you may also want to install mod_ssl
#yum install mod_ssl -y;

#And now... lets build the chroot
mkdir -p /var/chroot/etc/
cat /etc/passwd |egrep 'root:x|apache|nobody' > /var/chroot/etc/passwd
cat /etc/group  |egrep 'root:x|apache|nobody' |sed 's|bin/bash|sbin/nologin|' > /var/chroot/etc/group
cat /etc/shadow |egrep 'root:x|apache|nobody' > /var/chroot/etc/shadow
chmod 000 /var/chroot/etc/shadow
mkdir -p /var/chroot/etc/httpd/
mkdir -p /var/chroot/etc/pki/
mkdir -p /var/chroot/var/log/httpd
mkdir -p /var/chroot/run/httpd
ln -s ../../run/httpd /var/chroot/etc/httpd/run
ln -s ../../var/log/httpd /var/chroot/etc/httpd/logs
mkdir -p /var/chroot/{dev,etc,tmp,usr,log,var}
mkdir -p /var/chroot/usr/{bin,lib,lib64,libexec,local,sbin,src,share}
mkdir -p /var/chroot/usr/share/misc/
mkdir -p /var/chroot/usr/lib/locale
mkdir -p /var/chroot/run/{httpd,systemd}
ln -s usr/lib /var/chroot/lib
ln -s usr/lib64 /var/chroot/lib64
ln -s usr/sbin /var/chroot/sbin
ln -s usr/bin /var/chroot/bin
ln -s .. /var/chroot/var/chroot
ln -s ../tmp /var/chroot/var/tmp
chmod 1777 /var/chroot/tmp
mkdir -p /var/chroot/dev
mknod -m 666 /var/chroot/dev/null c 1 3
mknod -m 666 /var/chroot/dev/urandom c 1 9
mknod -m 666 /var/chroot/dev/zero c 1 5
#add more /dev paths here if your application requires them
cat << EOF >> /etc/fstab
##systemd doesn't like the readonly bindmount here, so just do a regular bindmount for now
/run/systemd              /var/chroot/run/systemd      none  bind            0 0
/etc/pki                  /var/chroot/etc/pki          none  bind            0 0
/usr/lib/locale           /var/chroot/usr/lib/locale   none  bind            0 0
/usr/lib64                /var/chroot/usr/lib64        none  bind            0 0
/usr/share                /var/chroot/usr/share        none  bind            0 0
##if you add an entry here, also add in /root/bin/chroot-init.sh
#will run on boot
cat << EOF > /etc/systemd/system/multi-user.target.wants/chroot.service
Description=chroot init service
#script to remount bindmounts as read-only
mkdir -p /root/bin/
cat << 'EOF' > /root/bin/chroot-init.sh
# used by /etc/systemd/system/chroot.service
mount -a
mount -o remount,ro /var/chroot/run/systemd
mount -o remount,ro /var/chroot/etc/pki
mount -o remount,ro /var/chroot/usr/lib/locale 
mount -o remount,ro /var/chroot/usr/lib64
mount -o remount,ro /var/chroot/usr/share
#cp files instead of readonly bind mount
for i in {hosts,resolv.conf,nsswitch.conf,localtime}; do 
    cp -f /etc/$i /var/chroot/etc/$i
#if you need to add files to you chroot jail, add more cp commands here
chmod 755 /root/bin/chroot-init.sh
systemctl stop httpd
systemctl start httpd
systemctl enable httpd
systemctl restart httpd
#if you're running selinux you may want to run these...
semanage fcontext -a -s system_u -t etc_t               '/var/chroot/etc(/.*)?'
semanage fcontext -a -s system_u -t cert_t              '/var/chroot/etc/pki(/.*)?'
semanage fcontext -a -s system_u -t httpd_config_t -f f '/var/chroot/etc/httpd'
semanage fcontext -a -s system_u -t shadow_t       -f f '/var/chroot/etc/shadow'
semanage fcontext -a -s system_u -t passwd_file_t  -f f '/var/chroot/etc/passwd'
semanage fcontext -a -s system_u -t passwd_file_t  -f f '/var/chroot/etc/group'
semanage fcontext -a -s system_u -t net_conf_t     -f f '/var/chroot/etc/hosts'
semanage fcontext -a -s system_u -t net_conf_t     -f f '/var/chroot/etc/resolv.conf'
restorecon -R  /var/chroot/etc
semanage fcontext -a -s system_u -t device_t              '/var/chroot/dev(/.*)?'
semanage fcontext -a -s system_u -t urandom_device_t -f c '/var/chroot/dev/urandom'
semanage fcontext -a -s system_u -t null_device_t -f c    '/var/chroot/dev/null'
semanage fcontext -a -s system_u -t zero_device_t -f c    '/var/chroot/dev/zero'
restorecon -R  /var/chroot/dev
semanage fcontext -a -s system_u -t httpd_var_run_t     '/var/chroot/run/httpd(/.*)?'
semanage fcontext -a -s system_u -t bin_t               '/var/chroot/usr/bin(/.*)?'
semanage fcontext -a -s system_u -t var_log_t           '/var/chroot/log(/.*)?'
semanage fcontext -a -s system_u -t tmp_t               '/var/chroot/tmp(/.*)?'
semanage fcontext -a -s system_u -t httpd_var_run_t     '/var/chroot/run/httpd(/.*)?'
restorecon -R /var/chroot/run/httpd
restorecon -R /var/chroot/log
restorecon -R /var/chroot/tmp
restorecon -R /var/chroot/usr/bin
semanage fcontext -l
getsebool -a

#now, lets prove the jail is working....
yum install php curl -y
systemctl restart httpd
cat << EOF > /var/chroot/www.example.com/chroot.php
#should output something like...
    [0] => /bin
    [1] => /dev
    [2] => /etc
    [3] => /lib
    [4] => /lib64
    [5] => /log
    [6] => /run
    [7] => /sbin
    [8] => /tmp
    [9] => /usr
    [10] => /var
    [11] => /www.example.com

By using read-only bindmounts it allows the chroot jail libraries to stay in sync with the system libraries whenever the host OS is patched or updated. Depending on what apache modules or php extensions you use, you may find you need to add additional system files to your chroot jail. For DIRs the simplest way is to add the dirs to /etc/fstab and /root/bin/chroot-init.sh. For files, add cp commands to /root/bin/chroot-init.sh, and then. For instance, php exec() uses /bin/sh internally, so if your application requires that... you'd need to cp /bin/sh to /var/chroot/bin/sh.

code snippets are licensed under Creative Commons CC-By-SA 3.0 (unless otherwise specified)