Apache2 mit mod_fcgid und PHP5 auf CentOS 6

mod_fcgid führt PHP nicht mit Berechtigung des Webservers aus (normalerweise apache oder www-data), sondern mit der des Eigentümers. Dies ist auf gemeinsam benutztem Webspace mit verschiedenen virtuellen Hosts natürlich wesentlich sicherer, weshalb Webinterface-Manager wie ISPConfig davon Gebrauch machen. Zur Erhöhung der Sicherheit wird zusätzlich open_basedir für jeden vHost verwendet. Dies verbietet dem Webserver die Ausführung von PHP auf fremden vHosts bzw. gestattet PHP nur auf dem eigenen Webspace.

In dieser Anleitung wird auf ein Webinterface wie ISPConfig verzichtet, statt dessen wird erläutert wie man die virtuellen Hosts manuell konfiguriert, mit und ohne SSL, daher mit jeweils eigenen IP-Adressen.

Zum Einsatz kommen noch MySQL 5 und phpMyAdmin 4.0. Gewiß heutzutage ältere Versionen, aber hin und wieder ergeben sich Situationen, wo man froh ist dafür ein funktionierendes Tutorial zu haben. Zumal mod_fcgid nicht in den Repos von CentOS enthalten ist und man daher auf Extra Packages angewiesen ist.

Wenn man nicht hunderte von vHosts zu konfigurieren hat sondern nur einige wenige, ist die hier beschrieben Methode sogar einfacher da nicht so umfangreich und kompliziert wie jene mit ISPConfig. Alles in allem eine elegante Lösung, aber nichts für Klick-Admins! 😉

Versionsübersicht

In diesem Tutorial werden folgende Versionen benutzt:

  • CentOS-6.9-x86_64-minimal
  • kernel-2.6.32-696.el6.x86_64
  • httpd-2.2.15-60.el6.centos.6.x86_64
  • mod_fcgid-2.3.9-1.el6.x86_64
  • php-5.3.3-49.el6.x86_64
  • mysql-server-5.1.73-8.el6_8.x86_64
  • phpMyAdmin-4.0.10.20
  • postfix-2.6.6-8.el6.x86_64

CentOS installieren

Prädestiniert hiefür ist die VirtualBox von Oracle. Wir laden uns also die Datei CentOS-6.9-x86_64-minimal.iso (etwas über 400 MB) von einem schnellen Mirror

herunter, erstellen in der VirtualBox eine neue virtuelle Maschine (Red Hat 64-bit) mit 20 GB Harddisk, 4 GB RAM und gebridgtem Netzwerk und fahren die ISO-Datei damit hoch. Nach ein paar Minuten ist die virtuelle Maschine auch schon installiert und wir können (neu)starten.

Netzwerkkarte aktivieren

Aus einem unerfindlichen Grund scheint in der virtuellen Maschine das Netzwerk nicht zu funktionieren, wie ein ifconfig an der Oracle VM Konsole zeigt. Erst ein ifup eth0startet das Netzwerk, welches aber beim nächsten Neustart wieder „verschwunden“ ist. Damit es einen Neustart überlebt, muß ONBOOT auf yes stehen:

vi /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
HWADDR=01:02:03:04:05:06
TYPE=Ethernet
UUID=71558e170-1b5-57a2-5afd-c4704c32ddc
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=dhcp

Nach einem

service network restart

ist die Netzwerkkarte persistent.

Updates installieren

yum -y update

Firewall deaktivieren

service iptables stop
chkconfig iptables off
service ip6tables stop
chkconfig ip6tables off

SELINUX deaktivieren

vi /etc/selinux/config
# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
#SELINUX=enforcing
SELINUX=disabled
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected,
# mls - Multi Level Security protection.
SELINUXTYPE=targeted
reboot

EPEL installieren

EPEL sind die Extra Packages for Enterprise Linux – mod_fcgid ist nämlich nicht in den Standardrepos von CentOS enthalten.

yum -y install epel-release

YUM-Priorities installieren

Sehr wichtig, das sonst früher oder später Kompatibilitätsprobleme auftauchen werden!

yum install yum-priorities -y

Vertrauen ist gut – Kontrolle ist besser!

vi /etc/yum/pluginconf.d/priorities.conf

Hier muß ein Einser stehen:

[main]
enabled = 1

CentOS-Base.repo erhält höchste Priorität

vi /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-$releasever - Base
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6
priority=1

[base], [updates] und [extras] erhalten Priorität 1, [contrib] und [centosplus] erhalten Priorität 2.

EPEL.repo erhält niedrigste Priorität

vi /etc/yum.repos.d/epel.repo
[epel]
name=Extra Packages for Enterprise Linux 6 - $basearch
#baseurl=http://download.fedoraproject.org/pub/epel/6/$basearch
mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch
failovermethod=priority
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6
priority=10

[epel], [epel-debuginfo] und [epel-source] erhalten die Priorität 10.

mod_fcgid installieren

Zuerst ein kurzes

yum update

um dann zum Höhepunkt zu kommen:

yum install mod_fcgid -y

PHP5 Apache-Modul deaktivieren

cd /etc/httpd/conf.d/
mv php.conf php.conf.disabled

cgi.fix_pathinfo=1 unkommmentieren

also das Semikolon zu Beginn der Zeile entfernen

vi /etc/php.ini
; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://www.php.net/manual/en/ini.core.php#ini.cgi.fix-pathinfo
cgi.fix_pathinfo=1

PHP_Fix_Pathinfo enablen

vi /etc/httpd/conf.d/fcgid.conf
# This is the Apache server configuration file for providing FastCGI support
# through mod_fcgid
#
# Documentation is available at
# http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html

LoadModule fcgid_module modules/mod_fcgid.so

# Use FastCGI to process .fcg .fcgi & .fpl scripts
AddHandler fcgid-script fcg fcgi fpl

# Sane place to put sockets and shared memory file
FcgidIPCDir /var/run/mod_fcgid
FcgidProcessTableFile /var/run/mod_fcgid/fcgid_shm

# Diese Zeile am Ende hinzufügen:
PHP_Fix_Pathinfo_Enable 1

Apache beim Neustart automatisch starten

chkconfig --levels 235 httpd on

Zwischenzeitlich mal den Apache neu starten

service httpd restart

Nutzer und Gruppen anlegen

Unser Konzept sieht vor, daß es für jeden vHost einen eigenen User und eigene Gruppe geben soll und somit für jeden eigene Berechtigungen. Wir nummerieren ganz einfach durch und beginnen mit 1, also web1web2web3, usw.

groupadd site1
groupadd site2
useradd -s /bin/false -d /var/www/sites/site1 -m -g site1 web1
useradd -s /bin/false -d /var/www/sites/site2 -m -g site2 web2
chmod 755 /var/www/sites/site1
chmod 755 /var/www/sites/site2

Document-Root erstellen und Eigentümer zuweisen

mkdir -p /var/www/sites/site1/web
mkdir -p /var/www/sites/site2/web
mkdir -p /var/www/sites/site1/tmp
mkdir -p /var/www/sites/site2/tmp
chown -R web1:site1 /var/www/sites/site1
chown -R web2:site2 /var/www/sites/site2

PHP-FCGI-Wrapper erstellen

Wir betreiben hier PHP mit SuExec und das erlaubt nichts außerhalb des SuExec Document Root. Dies kann zum Beispiel zu Problemen mit phpMyAdmin führen, doch davon später mehr.

/usr/sbin/suexec -V
 -D AP_DOC_ROOT="/var/www"
 -D AP_GID_MIN=100
 -D AP_HTTPD_USER="apache"
 -D AP_LOG_EXEC="/var/log/httpd/suexec.log"
 -D AP_SAFE_PATH="/usr/local/bin:/usr/bin:/bin"
 -D AP_UID_MIN=500
 -D AP_USERDIR_SUFFIX="public_html"

Wie man sieht, ist das SuExec Document Root /var/www. Weil aber die PHP-Binary in /usr/bin liegt (/usr/bin/php-cgi, also außerhalb des SuExec Document Root), kann diese nicht direkt aufgerufen werden da dies von SuExec verboten wird. Weil SuExec auch keine Symlinks zuläßt, ist der einzige Weg über einen Wrapper, also ein Script innerhalb des SuExec Document Root, welches die PHP-Binary aufruft und deshalb ausführbar sein muß. Weil aber der Wrapper nur vom Eigentümer des vHosts aufgerufen werden kann, ist für jeden virtuellen Host ein eigener Wrapper notwendig, welcher die Benutzerrechte des jeweiligen vHosts haben muß.

mkdir -p /var/www/php-fcgi-scripts/site1
mkdir -p /var/www/php-fcgi-scripts/site2
vi /var/www/php-fcgi-scripts/site1/.php-fcgi-starter
#!/bin/sh
PHPRC=/etc/
export PHPRC
export PHP_FCGI_MAX_REQUESTS=5000
export PHP_FCGI_CHILDREN=8
exec /usr/bin/php-cgi \
-d sendmail_path="/usr/sbin/sendmail -t -i -fwebmaster@site1.lan" \
-d session.save_path=/var/www/sites/site1/tmp \
-d upload_tmp_dir=/var/www/sites/site1/tmp \
-d open_basedir=/var/www/sites/site1/web/:/var/www/sites/site1/tmp/:/usr/share/php/
vi /var/www/php-fcgi-scripts/site2/.php-fcgi-starter
#!/bin/sh
PHPRC=/etc/
export PHPRC
export PHP_FCGI_MAX_REQUESTS=5000
export PHP_FCGI_CHILDREN=8
exec /usr/bin/php-cgi \
-d sendmail_path="/usr/sbin/sendmail -t -i -fwebmaster@site2.lan" \
-d session.save_path=/var/www/sites/site2/tmp \
-d upload_tmp_dir=/var/www/sites/site2/tmp \
-d open_basedir=/var/www/sites/site2/web/:/var/www/sites/site2/tmp/:/usr/share/php/

PHPRC ist der Pfad zur php.ini. Wenn für jeden vHost eine eigene php.ini benötigt wird, einfach die /etc/php.ini nach /var/www/sites/siteX kopieren und im Wrapper den Pfad ändern. In diesem Fall wäre es PHPRC=/var/www/sites/siteX.

chmod 755 /var/www/php-fcgi-scripts/site1/.php-fcgi-starter
chmod 755 /var/www/php-fcgi-scripts/site2/.php-fcgi-starter
chown -R web1:site1 /var/www/php-fcgi-scripts/site1
chown -R web2:site2 /var/www/php-fcgi-scripts/site2

Virtuelle Hosts einrichten

Es werden 2 virtuelle Hosts, jeweils mit und ohne SSL, mit verschiedenen IP-Adressen konfiguriert.

www.site1.lan    site1.lan    192.168.16.49
www.site2.lan    site2.lan    192.168.16.150

Damit es auch funktioniert, muß dies entweder im Nameserver (A, PTR und MX-Records) oder in der Hosts-Datei eingetragen werden, ansonsten kann Apache keine Namensauflösung durchführen. Die Hosts-Datei ist allerdings weniger gut geeignet, da man hier keine MX-Records eintragen kann. Es geht eben nichts über einen eigenen Nameserver!

Virtuelles Netzwerkinterface einrichten

Die erste IP-Adresse in diesem Tutorial wurde durch den DHCP-Server an eth0zugewiesen und die zweite ist virtuell. Solange man aber dies dem Netzwerkinterface nicht mitteilt, wird der zweite virtuelle Host nicht funktionieren. Wir erstellen also das virtuelle Netzwerkinterface mit

vi /etc/sysconfig/network-scripts/ifcfg-eth0:0
# Please read /usr/share/doc/initscripts-*/sysconfig.txt
# for the documentation of these parameters.
DEVICE=eth0:0
BOOTPROTO=none
NETMASK=255.255.255.0
TYPE=Ethernet
IPADDR=192.168.16.150
IPV6INIT=no
USERCTL=no

Nach einem Neustart des Netzwerkes mit

service network restart

ist das virtuelle Netzwerkinterface auch schon präsent, wie ein ifconfig zeigt:

eth0   Link encap:Ethernet Hardware Adresse 01:02:03:04:05:06
       inet Adresse:192.168.16.49 Bcast:192.168.16.255 Maske:255.255.255.0
       UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
       RX packets:169992 errors:0 dropped:0 overruns:0 frame:0
       TX packets:71039 errors:0 dropped:0 overruns:0 carrier:0
       Kollisionen:0 Sendewarteschlangenlänge:1000
       RX bytes:110034286 (104.9 MiB) TX bytes:25549000 (24.3 MiB)

eth0:0 Link encap:Ethernet Hardware Adresse 01:02:03:04:05:06
       inet Adresse:192.168.16.150 Bcast:192.168.16.255 Maske:255.255.255.0
       UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1

lo     Link encap:Lokale Schleife
       inet Adresse:127.0.0.1 Maske:255.0.0.0
       UP LOOPBACK RUNNING MTU:65536 Metric:1
       RX packets:60 errors:0 dropped:0 overruns:0 frame:0
       TX packets:60 errors:0 dropped:0 overruns:0 carrier:0
       Kollisionen:0 Sendewarteschlangenlänge:0
       RX bytes:70654 (68.9 KiB) TX bytes:70654 (68.9 KiB)

Apache-Konfigurationsdatei modifizieren

vi /etc/httpd/conf/httpd.conf

Am Ende anfügen:

#####################################################################################
LogFormat "%v\t%h\t%l\t%u\t%t\t%r\t%>s\t%b\t%{Referer}i\t%{User-Agent}i" tab-combined
CustomLog logs/access_log tab-combined

DirectoryIndex index.php index.shtml index.html

# Domainredirects
Include redirects.d

# vSites
Include sites.d/www.site1.lan
Include sites.d/www.site2.lan

Verzeichnis für vHosts einrichten

mkdir /etc/httpd/sites.d

Verzeichnis für Domainredirects einrichten

mkdir /etc/httpd/redirects.d

Domainredirects sind natürlich nur interessant für Domaingräbbler oder Leute, die glauben verschiedene Domains besitzen zu müssen.

In redirects.d wird für jede Domain eine eigene Datei angelegt. Als Beispiel soll die Domain irgendwas.lan (mit und ohne www) auf irgendwoh.in/?ras=klat zeigen. Natürlich müssen auch der Nameserver und das Netzwerkinterface davon wissen, sonst wird es nicht funktionieren.

vi /etc/httpd/redirects.d/irgendwas.lan
NameVirtualHost 192.168.16.254:80
<VirtualHost 192.168.16.254:80>
  ServerName irgendwas.lan
  ServerAlias www.irgendwas.lan
  Redirect / http://irgenwoh.in/?ras=klat
</VirtualHost>

vHost für site1 konfigurieren

vi /etc/httpd/sites.d/www.site1.lan
NameVirtualHost 192.168.16.49:80
<VirtualHost 192.168.16.49:80>
    Include sites.d/www.site1.lan.inc
</VirtualHost>
NameVirtualHost 192.168.16.49:443
<VirtualHost 192.168.16.49:443>
    Include sites.d/www.site1.lan.inc
    Include sites.d/www.site1.lan.ssl
</VirtualHost>
vi /etc/httpd/sites.d/www.site1.lan.inc
ServerName www.site1.lan
ServerAlias site1.lan
ServerAdmin webmaster@site1.lan
DocumentRoot /var/www/sites/site1/web/
<IfModule mod_fcgid.c>
    SuexecUserGroup web1 site1
    <Directory /var/www/sites/site1/web/>
        Options +ExecCGI
        AllowOverride All
        AddHandler fcgid-script .php
        FCGIWrapper /var/www/php-fcgi-scripts/site1/.php-fcgi-starter .php
        Order allow,deny
        Allow from all
    </Directory>
</IfModule>
vi /etc/httpd/sites.d/www.site1.lan.ssl
<IfModule mod_ssl.c>
    SSLEngine on
    SSLProtocol All -SSLv2 -SSLv3
    SSLHonorCipherOrder on
    SSLCertificateFile    /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</IfModule>

vHost für site2 konfigurieren

vi /etc/httpd/sites.d/www.site2.lan
NameVirtualHost 192.168.16.150:80
<VirtualHost 192.168.16.150:80>
    Include sites.d/www.site2.lan.inc
</VirtualHost>
NameVirtualHost 192.168.16.150:443
<VirtualHost 192.168.16.150:443>
    Include sites.d/www.site2.lan.inc
    Include sites.d/www.site2.lan.ssl
</VirtualHost>
vi /etc/httpd/sites.d/www.site2.lan.inc
ServerName www.site2.lan
ServerAlias site2.lan
ServerAdmin webmaster@site2.lan
DocumentRoot /var/www/sites/site2/web/
<IfModule mod_fcgid.c>
    SuexecUserGroup web2 site2
    <Directory /var/www/sites/site2/web/>
        Options +ExecCGI
        AllowOverride All
        AddHandler fcgid-script .php
        FCGIWrapper /var/www/php-fcgi-scripts/site2/.php-fcgi-starter .php
        Order allow,deny
        Allow from all
    </Directory>
</IfModule>
vi /etc/httpd/sites.d/www.site2.lan.ssl
<IfModule mod_ssl.c>
    SSLEngine on
    SSLProtocol All -SSLv2 -SSLv3
    SSLHonorCipherOrder on
    SSLCertificateFile    /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</IfModule>

Geschafft!

service httpd restart

Was jetzt noch fehlt ist Content für die beiden vHosts. Ohne Content wird Apache seine Default-Seite anzeigen.

vi /var/www/sites/web1/web/index.php
<?php phpinfo(); ?>
chown web1:web1 /var/www/sites/web1/web/index.php

MySQL

Sollte eigentlich schon installiert sein.

rpm -qa | grep mysql
mysql-5.1.73-8.el6_8.x86_64
mysql-libs-5.1.73-8.el6_8.x86_64
php-mysql-5.3.3-49.el6.x86_64
mysql-server-5.1.73-8.el6_8.x86_64
chkconfig --list|grep mysql
mysqld 0:Aus 1:Aus 2:Aus 3:Aus 4:Aus 5:Aus 6:Aus

Offenbar wird MySQL beim Neustart der virtuellen Maschine nicht automatisch gestartet, dies nachholen mit

chkconfig --levels 235 mysqld on

Nachdem das Root-Passwort für MySQL gesetzt wurde, den Server neu starten

service mysqld restart
netstat -putan | grep mysql
tcp   0   0   0.0.0.0:3306    0.0.0.0:*    LISTEN     10194/mysqld

Sieht gut aus! Jetzt steht phpMyAdmin nichts mehr im Weg.

phpMyAdmin

Die allseits bekannte und beliebte Datenbankadministrationsoberfläche ist in der EPEL-Repo einthalten.

yum install phpmyadmin

Dies installiert phpMyAdmin in

/usr/share/phpMyAdmin

Da wir aber ein System mit SuExec betreiben, wird phpMyAdmin klarerweise nicht funktionieren, wie oben im Abschnitt „PHP-FCGI-Wrapper erstellen“ erläutert. Es wird dauernd ein „403 Forbidden“ kommen, ganz gleich wie die Zugriffsrechte von phpMyAdmin eingestellt sind.

Wir lassen also in diesem Fall die EPEL-Repo aus und installieren phpMyAdmin manuell, am besten in unserer site1.lan, auf die nur der Admin zugreifen dürfen wird. Wir laden uns die letzte funktionierende Version für PHP 5.3 (phpMyAdmin-4.0.10.20-all-languages.zip) von phpmyadmin.net/downloads/ herunter

cd /var/www/sites/web1/web
wget https://files.phpmyadmin.net/phpMyAdmin/4.0.10.20/phpMyAdmin-4.0.10.20-all-languages.zip

Ein schnelles yum install unzip installiert den Entpacker für ZIP-Dateien.

unzip phpMyAdmin-4.0.10.20-all-languages.zip
mv phpMyAdmin-4.0.10.20-all-languages phpMyAdmin
chown -R web1:web1 phpMyAdmin

Das wars eigentlich schon, jetzt ist phpMyAdmin auf https://www.site1.lan/phpMyAdmin erreichbar.