Yet Another WordPress single install with multisite, Multitenancy configuration

OK. Yes, here we are again. The convenient evil of WordPress. I have multiple domains, and need multiple sites, that are pretty light on content. I run FreeBSD as the OS and have been doing system administration and large scale operations for a long time. In the past (early 2000’s) I had my own colo-ed servers, ran my own email, etc. I have since moved email to G-apps/G-suites and had my web sites hosted with the web hosting at my domain registrar. However, that site was raising prices, so I moved to a more basic registrar, and bought a t2.small reserve instance on AWS/EC2 – which was cheaper than the yearly subscription the shared web host offered at the registrar. Now, getting it to run with just a single installation of WordPress.

This is yet another how-to – because I found every other one – didn’t work. At least not out of the box, and not with my large use of Apache HTTPD 2.4 + .htaccess. It seemed straight forward, but getting multi-site to work with no changes to the package’s distribution (WordPress source code), separate wp-config.php and wp-content directories, proved to be slightly more challenging.

But before we get into WordPress specifically, lets configure php, apache, and get some SSL Certificates. (Setting up an EC2 instance or server is beyond this how-to.)

Apache HTTPD + SSL

There is simply no excuse anymore, SSL Certificates are free from Let’s Encrypt  . If you haven’t heard of it, read up on it. Before we can do that, we need to setup apache. (I assume you’ve setup DNS … that’s also beyond this how-to – I’ve just moved to AWS’s Route53.) First install just about everything we need.

sudo pkg install www/apache24 \
  archivers/php72-zip \
  archivers/php72-zlib \
  converters/php72-iconv \
  converters/php72-mbstring \
  databases/php72-mysqli \
  databases/php72-pdo \
  devel/php72-gettext \
  devel/php72-json \
  devel/php72-tokenizer \
  ftp/php72-curl \
  ftp/php72-ftp \
  graphics/php72-exif \
  graphics/php72-gd \
  lang/php72 \
  misc/php72-calendar \
  net/php72-sockets \
  print/pecl-pdflib \
  security/pecl-mcrypt \
  security/php72-filter \
  security/php72-hash \
  security/php72-openssl \
  sysutils/php72-posix \
  textproc/php72-ctype \
  textproc/php72-dom \
  textproc/php72-simplexml \
  textproc/php72-xml \
  textproc/php72-xmlreader \
  textproc/php72-xmlwriter \
  www/mod_php72 \
  www/php72-session \
  security/acme.sh \
  www/wordpress

Now, configure apache. My personal preference is to NOT edit the /usr/local/etc/apache24/httpd.conf  and put all my configurations as stub files in /usr/local/etc/apache24/Includes . Here’s some useful stubs and the initial bootstrap configuration we’ll need to get the certificates. Seriously, it pisses me off if you edit the files that come in the packages! COPY the files from extras into Includes. ( You may have to edit httpd.conf and uncomment the last line of “Include etc/apache24/Includes/*.conf” ). I’d like to see all the use of LoadModule’s in Includes directory … but there are still some post-install script for apache modules that change this file. You’ll thank me for this structure later.

/usr/local/etc/apache24/Includes/0-servername.conf

ServerName ec2.dpdtech.com
LoadModule rewrite_module libexec/apache24/mod_rewrite.so
Header add X-HTTP-Host: ec2
# LogLevel debug rewrite:trace2

/usr/local/etc/apache24/Includes/php.conf

AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .phps

<FilesMatch "\.php$">
    SetHandler application/x-httpd-php
</FilesMatch>
<FilesMatch "\.phps$">
    SetHandler application/x-httpd-php-source
</FilesMatch>

<IfModule dir_module>
    DirectoryIndex index.html index.php
</IfModule>

/usr/local/etc/apache24/Includes/vhosts-dpdtech-net.conf

<VirtualHost *:80>
    ServerName dpdtech.net

    ServerAdmin webmaster@dpdtech.com
    RewriteEngine On
    # required for Let's Encrypt #
    RewriteCond %{HTTPS} !=on
    RewriteCond %{REQUEST_URI} !^/.well-known/.*$

    # Rewrite everything to SSL.
    # RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
    DocumentRoot "/usr/local/www/sites/dpdtech"
    <Directory "/usr/local/www/sites/dpdtech">
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    ErrorLog "/var/log/apache/dpdtech-net-error_log"
    CustomLog "/var/log/apache/dpdtech-net-access_log" common
</VirtualHost>

Now make some directories, config php, and kick apache.

sudo cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini
sudo mkdir -p /var/log/apache/
sudo mkdir -p /usr/local/www/sites/dpdtech
sudo mkdir -p /usr/local/www/sites/wp-dpdtech
sudo /usr/local/etc/rc.d/apache24 onerestart

SSL – acme.sh – Let’s Encrypt

Crash course on let’s encrypt using acme.sh … its a single bash script … certbot works too, but it’s a python script that requires several decencies … and if you aren’t using python (or even if you are) … this is just a lower overhead method. And for this function, performance is not relevant. Below for the contab stub, pick some random values for that X minutes. Since log rotation will HUP apache, there is no reason to have acme.sh bounce apache. ( Since the certs are renewed well in advance of the expiration, delaying the apache HUP a day or two shouldn’t matter. )

sudo acme.sh --issue -d dpdtech.net -w /usr/local/www/sites/dpdtech
sudo mkdir -p /usr/local/etc/ssl/dpdtech 
sudo acme.sh --install-cert -d dpdtech.net  --cert-file /usr/local/etc/ssl/dpdtech/cert.pem --key-file /usr/local/etc/ssl/dpdtech/key.pem --fullchain-file /usr/local/etc/ssl/dpdtech/chain.pem
sudo cat >> /etc/cron.d/acme-ssl
X  22 * * *   root /usr/local/sbin/acme.sh --cron  > /dev/null
X  23 * * *   root acme.sh --install-cert -d dpdtech.net  --cert-file /usr/local/etc/ssl/dpdtech/cert.pem --key-file /usr/local/etc/ssl/dpdtech/key.pem --fullchain-file /usr/local/etc/ssl/dpdtech/chain.pem > /dev/null
<< Ctrl-D >>
sudo cat >> cat /etc/newsyslog.conf.d/apache24.conf
/var/log/apache/*log           root:wheel         640     7  *    @T2330 GX     /var/run/httpd.pid      1
<< Ctrl-D >>

Now, update apache for SSL

/usr/local/etc/apache24/Includes/0-ssl.conf

LoadModule socache_shmcb_module libexec/apache24/mod_socache_shmcb.so
LoadModule ssl_module libexec/apache24/mod_ssl.so
LoadModule socache_shmcb_module libexec/apache24/mod_socache_shmcb.so
Listen 443
SSLCipherSuite HIGH:MEDIUM:!SSLv3:!kRSA
SSLProxyCipherSuite HIGH:MEDIUM:!SSLv3:!kRSA
SSLHonorCipherOrder on
SSLProtocol all -SSLv3
SSLProxyProtocol all -SSLv3
SSLPassPhraseDialog  builtin
SSLSessionCache        "shmcb:/var/run/ssl_scache(512000)"
SSLSessionCacheTimeout  300

/usr/local/etc/apache24/Includes/vhosts-dpdtech-net.conf

You’ll want to update both the port 80 and 443 configurations … re-write everything to SSL. Because no point to point communications on the internet should be un-encrypted.

<VirtualHost *:80>
    ServerName dpdtech.net
    ServerAdmin webmaster@dpdtech.com
    RewriteEngine On
    RewriteCond %{HTTPS} !=on
    RewriteCond %{REQUEST_URI} !^/.well-known/.*$
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]

    DocumentRoot "/usr/local/www/sites/dpdtech"
    <Directory "/usr/local/www/sites/dpdtech">
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>
    ErrorLog "/var/log/apache/dpdtech-net-error_log"
    CustomLog "/var/log/apache/dpdtech-net-access_log" common
</VirtualHost>

<VirtualHost *:443>
   ServerName dpdtech.net
   ServerAdmin webmaster@dpdtech.com
    DocumentRoot "/usr/local/www/sites/wp-dpdtech"
    <Directory "/usr/local/www/sites/wp-dpdtech">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    ErrorLog "/var/log/apache/dpdtech-net-error_log"
    CustomLog "/var/log/apache/dpdtech-net-access_log" common

    SSLEngine on
    SSLCertificateFile "/usr/local/etc/ssl/dpdtech/cert.pem"
    SSLCertificateKeyFile "/usr/local/etc/ssl/dpdtech/key.pem"
    SSLCertificateChainFile "/usr/local/etc/ssl/dpdtech/chain.pem"

# I'm unclear if these are needed ... 
<FilesMatch "\.(cgi|shtml|phtml|php)$">
    SSLOptions +StdEnvVars
</FilesMatch>
<Directory "/usr/local/www/apache24/cgi-bin">
    SSLOptions +StdEnvVars
</Directory>
BrowserMatch "MSIE [2-5]" \
         nokeepalive ssl-unclean-shutdown \
         downgrade-1.0 force-response-1.0

</VirtualHost>

Bounce apache. Maybe update rc.conf too …

sudo cat >> /etc/rc.conf.d/apache24 
apache24_enable="YES"
<< Ctrl-D >>

sudo /usr/local/etc/rc.d/apache24 restart

Probably give it a test. wp-dpdtech is empty and directory listings are on … so you should see something, and the the SSL Certificate form Let’s Encrypt should be valid in pretty much all browsers … so you shouldn’t get any warnings.

WordPress

We installed WordPress above … this installs into /usr/local/www/wordpress on freebsd.

> tree -d -L 2 /usr/local/www/wordpress/

/usr/local/www/wordpress/
|-- wp-admin
|   |-- css
|   |-- images
|   |-- includes
|   |-- js
|   |-- maint
|   |-- network
|   `-- user
|-- wp-content
|   |-- plugins
|   `-- themes
`-- wp-includes
    ...

For this example, will use a real world domain, breaking dpdtech.net out of my group of dpdtech domains. Now lets create site directory, and this is done like this …

> mkdir -p /usr/local/www/sites/wp-dpdtech
> cd /usr/local/www/sites/wp-dpdtech
> ln -s /usr/local/www/wordpress
> ln -s wordpress/index.php
> cp -r wordpress/wp-content . 
> mkdir -p wp-content/uploads 
> cp  wordpress/wp-config-sample.php wp-config.php 


> tree -L 2 /usr/local/www/sites/wp-dpdtech
/usr/local/www/sites/wp-dpdtech
|-- index.php -> ./wordpress/index.php
|-- wordpress -> /usr/local/www/wordpress
|-- wp-config.php
`-- wp-content
    |-- index.php
    |-- plugins
    |-- themes
    `-- uploads

5 directories, 3 files

Depending on your style and security needs, please set Users and Groups and Permissions (chown, chgrp, chmod) as appropriate. Let’s setup up the database now.

> cd /usr/local/www/sites/wp-dpdtech
> sudo chgrp -R www wp-content
> sudo chmod -R g+w wp-content

> mysql -u root -p 
CREATE DATABASE `wp_dpdtech` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;
CREATE USER 'dpdtech'@'%' IDENTIFIED BY '****';
GRANT ALL PRIVILEGES ON `wp_dpdtech`.* TO 'dpdtech'@'%';
FLUSH PRIVILEGES;

Now, edit the /usr/local/www/sites/wp-dpdtech/wp-config.php file, adding in the database credential. Also read the comments and fill in the ‘Authentication Unique Keys and Salts‘ block of constants and add those in. Then, we are not done with wp-config.php … edit/create /usr/local/www/wordpress/wp-config.php here is one of the pieces of secret sauce.

<?php
if ( is_file ( $_SERVER['DOCUMENT_ROOT'] . '/wp-config.php' )) {	
 require_once ( $_SERVER['DOCUMENT_ROOT'] . '/wp-config.php' );
} elseif ( is_file ( $_SERVER['CONTEXT_DOCUMENT_ROOT'] . '/wp-config.php' ) ){
 require_once ( $_SERVER['CONTEXT_DOCUMENT_ROOT'] . '/wp-config.php' );
}
?>

This loads the site specific config, restricted to the APACHE environmental variables of the configured site. Why two? The DOCUMENT_ROOT works if this is the root web site … so for example https://dpdtech.net/ … but if you had your own custom web site there (I used to for my personal blog), and wanted WordPress in a subdirectory, like https://dpdtech.net/blog/ – but have the document root for the root site as /usr/local/www/sites/dpdtech but the WordPress blog as /usr/local/www/sites/wp-dpdtech , then you need both. Optionally, I think you could drop the wp-config.php in the docroot … but that just didn’t seem like a good idea.

Once that is complete, now go to, of course on your domain, https://dpdtech.net/wordpress/wp-admin/ and complete the initial setup. Now, some cleanup. This will likely temporally break your setup. But go to Settings -> General and change the URL … remove “/wordpress” from the URL and save. You’ll get a 404 on the save. Continue here … now create /usr/local/www/sites/wp-dpdtech/.htaccess (maybe, do this .htaccess before the initial config … and drop “/wordpress” from the URL … but this is the order I did it was I wrote this blog … )

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wordpress/
RewriteRule ^index\.php$ /wordpress/index.php [L]
RewriteRule ^wp-content/(.*)$ - [L]
RewriteRule ^wp-(login|admin|includes)(/?.*)$ wordpress/wp-$1$2 [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /wordpress/index.php [L]
</IfModule>

<Files "wp-config.php">
	Order allow,deny
	Deny from all
</Files>

Options -Indexes

Next, we need to clean up the wp-content and uploads paths. Add the following to you site’s wp-config.php file … right after the database configuration.

    define('DOMAIN_CURRENT_SITE', 'dpdtech.net');
    define('PATH_CURRENT_SITE', '/');
    define('WP_CONTENT_URL', 'https://dpdtech.net/wp-content');
    define('WP_CONTENT_DIR', '/usr/local/www/sites/wp-dpdtech/wp-content');

Then, we need to make an adjustment in the database. I poked around at the WordPress code, and there is no PHP constant that can override these two locations correctly.

update wp_options set option_value='/wp-content/uploads' where option_name='upload_url_path';
update wp_options set option_value='/usr/local/www/sites/wp-dpdtech/wp-content/uploads' where option_name='upload_path';

Now, go to https://dpdtech.net/wp-admin/ … and it should all be working. Go to the appearance and update the themes. If WordPress prompts for FTP access, then you need to adjust permissions on the wp-content directory or you forgot one of the settings above.

Now, if you want the DocRoot to be a different directory, and the WordPress as a sub directory, this is mainly accomplished completely with Apache configuration. So for example, https://dpdtech.net/blog/ ; modify the .htaccess like this :

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /blog/
RewriteRule ^wordpress/.*$ - [L]
RewriteRule ^index\.php$ - [L]
RewriteRule ^wp-content/(.*)$ - [L]
RewriteRule ^wp-(login|admin|includes)(/?.*)$ wordpress/wp-$1$2 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . wordpress/index.php [L]
</IfModule>

Ok, there is some clean here – but this was actually working, and I’m not going to re-test at the moment. This is based on two major changes in the apache Vhost config. First, make the docroot to your custom web development directory:

    DocumentRoot "/usr/local/www/sites/dpdtech"
    <Directory "/usr/local/www/sites/dpdtech">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

Next, add in the /blog path as an alias

    Alias "/blog" "/usr/local/www/sites/wp-dpdtech"
    <Directory "/usr/local/www/sites/wp-dpdtech">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

Go to Settings -> General and change the URL to /blog. And that should be it.

Restricting wp-admin

So, the evil of WordPress, is it’s history of being the most hacked. A minor tweak, you may want to IP restrict these areas. Can do so like this below:

# Block WordPress xmlrpc.php requests
<FilesMatch ".*(wp-login|xmlrpc)\.php$">
  Require all denied
  Require ip 127.0.0.1/32
</FilesMatch>

<Directory "/usr/local/www/sites/wp-dpdtech/wordpress/wp-admin">
  Options Indexes FollowSymLinks
  AllowOverride All
  Require all denied
  Require ip 127.0.0.1/32
</Directory>

Now, this particular might not be practical. You could use this in combination with adding some /etc/hosts rules and an ssh sock proxy tunnel ( ex: ssh -D 8080 … ) … but again beyond this how-to. I have static IPs at home, some I use my /29 in this place.