Restrict Access to WordPress with Nginx and GeoIP

The goal of this post is to harden your WordPress dashboard by preventing logins from countries where you know you will never be connecting. Since brute-force login attempts may still originate from an allowed country, it would be wise to combine this with other tools like fail2ban or one of the numerous plug-ins that add login rate-limiting restrictions to WordPress.

This example targets Nginx on CentOS 7 using PHP 7 from the Remi repo. Settings may vary depending on your exact setup, so watch out for differences from your config, and back up your /etc/nginx first!

First make sure you keep an up-to-date GeoIP database by running a script like the example below via a nightly cron job.

#!/bin/bash

/bin/curl https://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz -o /tmp/GeoIP.dat.gz

/bin/gunzip -c /tmp/GeoIP.dat.gz > /etc/nginx/GeoIP.dat

/bin/rm -f /tmp/GeoIP.dat.gz

Make sure the GeoIP module is installed:

yum install nginx-mod-http-geoip

Make sure the module is loaded in /etc/nginx/nginx.conf:

load_module modules/ngx_http_geoip_module.so;

Inside of http { } in nginx.conf, add this, modifying country and default as desired:

geoip_country /etc/nginx/GeoIP.dat;
map $geoip_country_code $allowed_country {
   default no;
   US yes;
}

And finally add the necessary location statement in your virtual host’s server { }. In this example I’m using PHP 7.1 from the Remi repo, so your configuration may vary.

location ~ ^/(wp-admin|wp-login.php) {
 try_files $uri $uri/ /index.php?$args;
 # try_files $uri =404;
 fastcgi_pass unix:/var/run/php-fpm/php71-fpm.sock;
 fastcgi_index index.php;
 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
 include fastcgi_params;
 index index.html index.htm index.php;
 if ($allowed_country = no) {
 return 444;
 }
}

Automatically update CentOS 7 with yum-cron

  1. Install yum-cron:
    sudo yum -y install yum-cron
  2. Open /etc/yum/yum-cron.conf in your favorite editor and make sure each of the following values are set to ‘yes’.
    update_messages = yes
    download_updates = yes
    apply_updates = yes
  3. Other optional settings in yum-cron.conf
    # Change from 'default' to 'security'
    # if you only want security fixes.
    update_cmd = security
    # For email alerts (recommended):
    email_to = you@domain.com 
    # If you use a different mail host:
    email_host = smtp.domain.com
  4. Start the service:
    sudo systemctl start yum-cron
  5. Enable the service (makes sure it will start again after a reboot.)
    sudo systemctl enable yum-cron

Automatically Update WordPress, Themes, and Plugins using WP-CLI

WordPress is a hugely popular blog/CMS platform, but with widespread adoption comes risk: It is a common target for hackers, exploits, etc. Accordingly, you should make sure it gets regular updates.

WordPress has a built-in update mechanism but this also requires that its PHP files be writable by the web server, introducing a new set of security risks.

Luckily there is another option. Instead you can use a command-line tool called WP-CLI, which enables us to script WordPress updates.

These instructions will outline the steps necessary to install WP-CLI, create a script to update multiple sites at once, and install that script as a cron job to ensure updates happen on a regular schedule.


Before You Begin

As with any WordPress maintenance tasks, I recommend making regular backups of your database and files.

For this process to succeed, you’ll need to run your script as a user who has permission to modify the WordPress files. This could be your regular user account, but you might also want to create a dedicated user such with a name like ‘scripts’, and give it write permissions to your WordPress files. It is not recommended to run this as root.


  1. Install WP-CLI
    Install WP-CLI (adapted from http://wp-cli.org/#installing)

    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
    chmod +x wp-cli.phar
    sudo mv wp-cli.phar /usr/local/bin/wp
  2. Test WP-CLI
    Run as a user who has write privileges to your WordPress site. If everything works you should get a series of “Success” messages, and/or a list of updated items.

    cd /var/www/html   # replace with path to your site 
    /usr/local/bin/wp core update
    /usr/local/bin/wp core update-db
    /usr/local/bin/wp theme update --all
    /usr/local/bin/wp plugin update --all
  3. Create an Update Script
    Use your favorite text editor to create a new shell script. In that script, put the following code:

    #!/bin/bash
    
    # Absolute paths of WordPress sites. Space-separated.
    sites="/var/www/html/site1 /var/www/html/site2 /var/www/html/site3"
    
    for site in $sites; do
    
    echo $site
    
    /usr/local/bin/wp core update --path=$site --quiet
    /usr/local/bin/wp core update-db --path=$site --quiet
    /usr/local/bin/wp theme update --all --path=$site --quiet
    /usr/local/bin/wp plugin update --all --path=$site --quiet
    
    done
  4. Make The Script Executable
    chmod 700 wp-update
  5. Test The Update Script
    ./wp-update

    If everything works you’ll see a series of “Success” messages, and/or a list of updated items. If you see errors, double-check that your current user has permission to write to the WordPress site directories.

  6. Install Cron Job
    Make sure you’re still logged in as a user who has write permissions for the WordPress site directories.

    crontab -e

    Now create a new cron entry like this one, including the correct path to your update script. In this example it will run every day at 2:30am.

    30 2 * * * /home/scripts/bin/wp-update

    Close and save your crontab file.

  7. If everything worked correctly, your WordPress sites will now auto-update every night.

Monitoring Apple Caching Server Status via MunkiReport

Occasionally the Caching service on macOS will stop working due to a network outage or a glitch on Apple’s end.

It’s a simple enough fix — all you have to do is turn the service off and on again. But how do you know that it’s offline? It’s easy if you use MunkiReport with the Caching module enabled.

Just save the code below as a script on your MunkiReport database server and set it up as an hourly cron job. It looks for caching activity reported in the last two hours, and kicks off an alert if no results are found. (Be sure to read the notes below.)

#!/bin/bash

cachingactivity=`/bin/mysql munkireport -e "select \
collectiondate from caching where collectiondate > \
timestampadd(hour, +6, now()) order by \
collectiondate desc;"`;

if [[ $cachingactivity == "" ]]; then
     echo "Caching service may be offline."
fi

Considerations:

  • The user who runs the script needs to have a MySQL credential stored in their .my.cnf file.
  • Make sure cron is able to send you alerts. (Working MTA, and MAILTO=”user@domain” appears before this job is listed.)
  • You may need to modify the timezone offset (+6) to something suitable for your location. I’m at UTC-8, so +6 means my script looks for caching server activity reported within the last two hours.
  • Depending on how many clients there are, and whether any of them are on 24/7, you may need to adjust how far back your cron job is looking for records to avoid false error reports. Or only schedule it to check weekdays but not weekends.
  • Obviously this doesn’t provide up-to-the-minute alerting, but it should still help you notice if something is wrong before anyone else does.
  • With multiple caching servers, one could go down and you would not be alerted. The true purpose of this script is make sure that the caching service is working somewhere. It’s not concerned about calling out individual servers.

Munki, Monolithic Imaging, and Microsoft Office 2016 Volume License Serializer

If you have Macs that were cloned from an already-activated system (either via monolithic imaging, or one-off machines that were migrated or experienced hardware failure) you will find that your Office 2016 activation is broken. You will also find that Munki doesn’t know it needs to re-run the Office 2016 serializer since a receipt already exists for it.

I was able to work around this by adding a couple scripts to the serializer’s pkgsinfo. First I added an installcheck_script that looks for a file in /Users/Shared which contains the serial of the computer on which Office was activated. If it doesn’t match the computer’s serial, or if it doesn’t exist, then the serializer package will run. Then we use a postinstall_script to write the serial number of the current computer to this file so we know it’s been activated on this machine and doesn’t need to run again.

installcheck_script:

#!/bin/bash

machineserial=`system_profiler SPHardwareDataType | awk '/Serial/ {print $4}'`

activatedserial=`cat /Users/Shared/.office2016activatedserial`

if [[ $machineserial == $activatedserial ]]; then

exit 1

fi

postinstall_script:

#!/bin/bash

system_profiler SPHardwareDataType | awk '/Serial/ {print $4}' > /Users/Shared/.office2016activatedserial

Deploying Epson EasyMP Multi PC Projection for Mac

When adding Epson EasyMP Multi PC Projection 2.1.0 to Munki, I found that the built-in postinstall script fails.

Some digging revealed that the script was expecting a file to exist at </tmp/mpp-72B3FDFF-9513-4CED-96C3-34881FC77AB8>. It reads that file and uses the value contained for the “ClientMode” preference in </Library/Preferences/com.epson.EasyMP_Multi_PC_Projection.Settings/mppsettings.xml>

On a successful install the the GUI, that preference is just the integer “0”. So I added the following preinstall script to my Munki pkgsinfo.

#!/bin/bash
echo "0" > /tmp/mpp-72B3FDFF-9513-4CED-96C3-34881FC77AB8

The postinstall script is appeased and EasyMP Multi PC Projection 2.1.0 installs successfully. No direct modification or repackaging necessary.

Using SSH Keys for Passwordless Authentication

SSH keys can provide an additional layer of security (if you also disable password authentication on the server), or they can simplify the process of connecting to remote servers. For our purposes we’re interested in the latter – connecting to the server without entering a password.

The basic idea is that you create a public/private key pair on your computer, then upload the public key to the remote server. When you connect to the remote server, your computer can use the private key to authenticate instead of entering your password.

First we’ll create the key:

ssh-keygen -f ~/.ssh/id_rsa -N ""

If a key already exists, you’ll be asked if you want to overwrite it. If this happens you can respond by pressing “n” (no) and continue to the next step.

Next we need to upload the key to the server. Replace “user” with your username, and “host” with the address of the SSH server.

ssh-copy-id -i ~/.ssh/id_rsa user@host

If it worked, you should now be able to SSH to the remote host without entering your password.

DesktopPictureProfileCreator

DesktopPictureProfileCreator is a simple bash script that generates mobileconfig profiles for managing the desktop picture on macOS computers.

Run the script followed by the path to the image you want to set as the desktop picture. For example “sh DesktopPictureProfileCreator.sh /Library/Desktop\ Pictures/Aqua\ Blue.jpg”. Whatever path you specify must exist in the same location on the computer(s) you intend to deploy it to.

The profile will be created on the desktop of whichever user runs the script. In the example above, the profile would appear at ~/Desktop/Desktop-Aqua-Blue-jpg.mobileconfig

For more info and to download, check out my mobileconfigs repository on GitHub.

File ownership considerations with Nginx and php-fpm

I recently switched my CentOS 7 web server over to Nginx and php-fpm.

From my experience with Apache I assumed that PHP scripts would be executed by the same user the web server is running as — ‘nginx’ in this case. But this could no longer be taken for granted since php-fpm is a separate process from the web server.

In my configuration php-fpm was actually running as the ‘apache’ user. This meant any files that need to be writable by PHP scripts should still be owned by that user or group rather than ‘nginx’.

A common scenario where this matters is if your users need to be able to install WordPress updates, Plugins, or Themes via the browser without entering additional credentials. In order for this to work, the web server (or in this case, php-fpm) must be able to write to the files in question.

If you are wrestling with file permissions, or are unsure of the correct permissions to set in this scenario, be sure to confirm which user and group are specified in /etc/php-fpm.d/www.conf

# grep "^user\|^group" /etc/php-fpm.d/www.conf 
user = apache
group = apache

Or you can check the actual running process with pstree:

$ pstree -ua | grep "nginx\|php"
  |-nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   |-nginx,nginx
  |   `-nginx,nginx
  |-php-fpm
  |   |-php-fpm,apache              
  |   |-php-fpm,apache              
  |   |-php-fpm,apache              
  |   |-php-fpm,apache              
  |   |-php-fpm,apache              
  |   |-php-fpm,apache              
  |   `-php-fpm,apache