Apache, PHP on VPS

This post describes how to setup a VPS (virtual private server) to run Apache, PHP and MySQL on CentOS. Usually I configure only the LAMP stack on dev servers, but I had to go through some additional configuration when setting up a production web server with Apache + MySQL + PHP5 on CentOS 5.8 running on VPS. VPS servers are pretty cheap and the best way is to start with a bare bones server running CentOS 5.8 and then you can configure only the things you need. This gives you complete control on what runs on the server, but then you need to setup DNS nameserver and iptables firewall. This VPS instance uses a tiny amount of memory (total server memory is 128MB) and is running on CentOS 5.8. The intent is to setup a fully functional server that runs DNS nameserver, is secured using iptables and configured to use Apache Httpd, PHP5 and MySQL for production use. It uses CentOS services to manage all the applications and is configured to use log rotation.

Here’s a quick overview of how to secure a VPS (virtual private server) running CentOS 5.8x and configure Apache and PHP5.

  • Setup nameserver
  • Setup iptables
  • Setup MySQL
  • Setup PHP5
  • Configure Apache

SETUP NAMESERVER

  1. yum -list bind-*
  2. yum install bind-chroot.x86_64 (if needed)
  3. vi /etc/resolv.conf
  4. vi /var/named/chroot/etc/named.conf
  5. vi /var/named/chroot/var/named/mydomain.com.zone
  6. chmod 640 mydomain.com.zone
  7. chown root:named mydomain.com.zone
  8. service named restart
#FILE: /etc/resolv.conf

options {
    listen-on    port 53 { 127.0.0.1; };
    listen-on-v6 port 53 { ::1; };
    version             "none";
    directory           "/var/named";
    dump-file           "/var/named/data/cache_dump.db";
    statistics-file     "/var/named/data/named_stats.txt";
    memstatistics-file  "/var/named/data/named_mem_stats.txt";
    allow-query         { localhost; };
    allow-query-cache   { localhost; };
};
logging {
    channel my_log {
        file "data/named.run" versions 3 size 5m;
        severity debug;
        print-time yes;
        print-severity yes;
        print-category yes;
    };
    category default {
        my_log;
    };
};
view "localhost_resolver" {
    match-clients       { localhost; };
    match-destinations  { localhost; };
    recursion           yes;
    include             "/etc/named.rfc1912.zones";
};
#FILE: /var/named/chroot/etc/named.conf

options {
  listen-on     port 53 { any; };
  listen-on-v6  port 53 { any; };
  version             "none";
  directory           "/var/named";
  dump-file           "/var/named/data/cache_dump.db";
  statistics-file     "/var/named/data/named_stats.txt";
  memstatistics-file  "/var/named/data/named_mem_stats.txt";
};
logging {
  channel my_log {
    file "data/named.run" versions 3 size 5m;
    severity warning;
    print-time yes;
    print-severity yes;
    print-category yes;
  };
  category default {
      my_log;
  };
};
view "localhost_resolver" {
  match-clients       { localhost; };
  match-destinations  { localhost; };
  recursion yes;
  include "/etc/named.rfc1912.zones";
  zone "mydomain.com" {
    type master;
    file "/var/named/mydomain.com.zone";
  };
};
view "external" {
  match-clients       { any; };
  match-destinations  { any; };
  recursion no;
  allow-query-cache   { none; };
  zone "mydomain.com" {
    type master;
    file "/var/named/mydomain.com.zone";
  };
};
#FILE: /var/named/chroot/var/named/mydomain.com.zone

$TTL 1D
@   IN  SOA   ns1.mydomain.com. hostmaster.mydomain.com. (
2012090601    ; serial, todays date
12H           ; refresh
4H            ; retry
28D           ; expire
1D )          ; minimum
IN    NS      ns1.mydomain.com.
IN    NS      ns2.mydomain.com.
IN    MX      1 ASPMX.L.GOOGLE.COM.
IN    MX      5 ALT1.ASPMX.L.GOOGLE.COM.
IN    MX      5 ALT2.ASPMX.L.GOOGLE.COM.
IN    A       123.456.789.100
ns1                 IN    A       123.456.789.100
ns2                 IN    A       123.456.789.101
localhost           IN    A       127.0.0.1
www                 IN    CNAME   mydomain.com.

using named.root.hints file

  1. cp /usr/share/doc/bind-*/sample/etc/named.rfc1912.zones /var/named/chroot/etc
  2. cp /usr/share/doc/bind-*/sample/etc/named.root.hints /var/named/chroot/etc
  3. wget –user=ftp –password=ftp ftp://ftp.rs.internic.net/domain/root.zone -O /var/named/chroot/var/named/named.root
  4. rndc reload

SETUP IPTABLES

  1. yum list iptables*
  2. iptables -L
  3. system-config-securitylevel
  4. iptables-save > iptables_default.out
  5. service iptables save
#!/bin/bash
# Flush all current rules from iptables
iptables -F
# Set access for localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT
iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT
# Accept packets belonging to established and related connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow SSH connections on tcp port 22
# Your IP:   123.456.0.0/16
iptables -A INPUT -p tcp -s 123.456.0.0/16   --dport 22 -j ACCEPT
# Lock down the system
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Open DNS and HTTP ports
iptables -A INPUT -p tcp -m tcp --dport 53   -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 53   -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 80   -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 443  -j ACCEPT
# Save settings
service iptables save
# List rules
iptables -L -v
  1. Install Remi repository for CentOS 5.8
  1. Check available MySQL versions
  • yum –enablerepo=remi,remi-test list mysql mysql-server
  1. Install MySQL
  • yum –enablerepo=remi,remi-test install mysql.x8664 mysql-server.x8664
  1. Start MySQL and enable auto start on boot
  • /etc/init.d/mysqld start
  • chkconfig –levels 235 mysqld on
  1. MySQL secure installation
  • usr/bin/mysqlsecureinstallation
  • mysqladmin -u root password [yourpasswordhere]

SETUP PHP5

  1. yum list php5*
  2. yum install php53.x8664 php53-mcrypt.x8664 php53-mysql.x86_64
  3. yum list pcre ;make sure you’re using 8.x and not 6.x
  • ERROR: PHP Warning: pregmatchall(): Compilation failed: unrecognized character after (?< at offset 3
  • REASON: Upgrade pcre package to 8.10x from 6.6x
  1. rpm –import http://www.jasonlitka.com/media/RPM-GPG-KEY-jlitka
  2. nano -w /etc/yum.repos.d/utterramblings.repo
  3. Type this in editor::
  • [utterramblings]
  • name=Jason's Utter Ramblings Repo
  • baseurl=http://www.jasonlitka.com/media/EL$releasever/$basearch/
  • enabled=1
  • gpgcheck=1
  • gpgkey=http://www.jasonlitka.com/media/RPM-GPG-KEY-jlitka
  1. yum –enablerepo=utterramblings list pcre
  2. yum –enablerepo=utterramblings install pcre

SETUP APACHE

  1. configuration file - /etc/httpd/conf/httpd.conf
  2. log files - /etc/httpd/logs
  3. html dir - /var/www/html
  4. apache log rotation - /etc/logrotate.d/apache
  • edit config: vi /etc/logrotate.d/apache
  • test run: /etc/cron.daily/logrotate
#FILE: /etc/httpd/conf/httpd.conf

ServerRoot "/etc/httpd"
PidFile run/httpd.pid
Timeout 120
KeepAlive Off
MaxKeepAliveRequests 100
KeepAliveTimeout 15

<IfModule prefork.c>
StartServers            8
MinSpareServers         5
MaxSpareServers        20
ServerLimit           256
MaxClients            256
MaxRequestsPerChild  4000
</IfModule>

<IfModule worker.c>
StartServers            2
MaxClients            150
MinSpareThreads        25
MaxSpareThreads        75
ThreadsPerChild        25
MaxRequestsPerChild     0
</IfModule>

Listen 80

LoadModule authz_host_module modules/mod_authz_host.so
LoadModule include_module modules/mod_include.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule logio_module modules/mod_logio.so
LoadModule env_module modules/mod_env.so
LoadModule ext_filter_module modules/mod_ext_filter.so
LoadModule expires_module modules/mod_expires.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule mime_module modules/mod_mime.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so

Include conf.d/*.conf

User apache
Group apache

ServerAdmin root@localhost
ServerName www.mydomain.com:80
UseCanonicalName Off
DocumentRoot "/var/www/html"

<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>

<Directory "/var/www/html">
Options FollowSymLinks
AllowOverride None
Order allow,deny
Allow from all

#BSH: redirect to www, forward everything to index.php
<IfModule mod_rewrite.c>
  RewriteEngine on

  RewriteCond %{HTTPS} off
  RewriteCond %{HTTP_HOST} !^www\.mydomain\.com$ [NC]
  RewriteRule .? http://www.mydomain.com%{REQUEST_URI} [R=301,L]

  RewriteCond %{HTTPS} on
  RewriteCond %{HTTP_HOST} !^www\.mydomain\.com$ [NC]
  RewriteRule .? https://www.mydomain.com%{REQUEST_URI} [R=301,L]

  RewriteCond %{HTTPS} off
  RewriteCond %{REQUEST_URI} api.* [OR]
  RewriteCond %{REQUEST_URI} app.* [OR]
  RewriteCond %{REQUEST_URI} login.*
  RewriteRule .? https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

  RewriteCond %{REQUEST_URI} api.* [OR]
  RewriteCond %{REQUEST_URI} app.* [OR]
  RewriteCond %{REQUEST_URI} login.* [OR]
  RewriteCond %{REQUEST_URI} rdr.*
  RewriteRule ^(.*)$ /index.php [L,QSA]
</IfModule>

</Directory>

<IfModule mod_userdir.c>
  UserDir disable
</IfModule>

DirectoryIndex index.html index.php
AccessFileName .htaccess
<Files ~ "^\.ht">
Order allow,deny
Deny from all
</Files>

TypesConfig /etc/mime.types
DefaultType text/plain

# BSH: remove Last-Modified
FileETag MTime Size
Header unset Last-Modified
Header unset Server
Header unset X-Powered-By

# BSH: set ETag for HTML files
<FilesMatch "\.(html|htm|xml|txt|xsl)$">
  Header unset Cache-Control
</FilesMatch>

# BSH: Cache static content for 12 MONTHS
<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|swf|mp3|mp4|css|js|txt|xml)$">
FileETag None
Header unset ETag
Header unset Last-Modified
Header set Cache-Control "public"
Header set Cache-Control "max-age=31104000, public"
</FilesMatch>

HostnameLookups Off
ServerTokens Prod
ServerSignature Off

ErrorLog logs/error_log
LogLevel warn
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
CustomLog logs/access_log common

Alias /icons/ "/var/www/icons/"
<Directory "/var/www/icons">
Options None
AllowOverride None
Order allow,deny
Allow from all
</Directory>

ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"
<Directory "/var/www/cgi-bin">
AllowOverride None
Options None
Order allow,deny
Allow from all
</Directory>

AddDefaultCharset UTF-8
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

# BSH: Compress text html javascript css xml
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
#FILE:  /etc/httpd/conf/php.conf

<IfModule prefork.c>
    LoadModule php5_module modules/libphp5.so
</IfModule>
<IfModule worker.c>
    LoadModule php5_module modules/libphp5-zts.so
</IfModule>

# BSH: php error and cookie settings
<IfModule mod_php5.c>
    php_value display_errors          "0"
    php_value session.name            "SMBLSID"
    php_value session.use_cookies     "1"
    php_value session.cookie_httponly "1"
    php_value session.cookie_lifetime "0"
    php_value session.gc_maxlifetime  "1800"
    php_value session.gc_divisor      "100"
    php_value session.gc_probability  "100"
</IfModule>

AddHandler php5-script .php
AddType text/html .php
#FILE:  /etc/logrotate.d/apache

/var/log/httpd/*log {
    daily
    missingok
    notifempty
    sharedscripts
    rotate 10
    size 25M
    delaycompress
    dateext
    maxage 60
    postrotate
           /sbin/service httpd reload > /dev/null 2>/dev/null || true
    endscript
}