Securing WordPress: The Basics

This is the first in an occasional series of documents on WordPress.


WordPress is ubiquitous but fragile.  There are few alternatives that provide the easy posting, wealth of plugins, and integration of themes, while also being (basically) free to use.

It’s also a nerve-wracking exercise in keeping bots and bad actors out.  Some of the historical security holes are legendary.  It doesn’t take long to find someone who experienced a site where the comments section was bombed by a spammer, or even outright defacement.  (I will reluctantly raise my own hand, having experienced both in years past.)

Most people that use WordPress nowadays rely on 3rd parties to host it.  This document isn’t for them; hosted security is mostly outside of your control.  That’s generally a good thing: professionals are keeping you up to date and covered by best practices.

The rest of us muddle through security and updates in piece-meal fashion, occasionally stumbling over documents like this one.

Things To Look Out For

As a rule, good server hygiene demands that you keep an eye on your logs.  Tools like goaccess help you analyze usage, but nothing beats a peek at the raw logs for noticing issues cropping up.

The Good Bots

Sleepy websites like mine show a high proportion of “good” bots like Googlebot, compared to human traffic.  They’re doing good things like crawling (indexing) your site.

In my case they are the primary visitor base to my site, generating hundreds or even thousands of individual requests per day.  Hopefully your own WordPress site has a better visitor-to-bot ratio than mine.

We don’t want to block these guys from their work, they’re actually helpful.

The Bad Bots

You’ll also see bad bots, possibly lots of them.  Most are attempting to guess user credentials so they can post things on your WordPress site.

Some are fairly up-front about it:

...
132.232.47.138 [07:51:14] "POST /xmlrpc.php HTTP/1.1"
132.232.47.138 [07:51:14] "POST /xmlrpc.php HTTP/1.1"
132.232.47.138 [07:51:15] "POST /xmlrpc.php HTTP/1.1"
132.232.47.138 [07:51:16] "POST /xmlrpc.php HTTP/1.1"
132.232.47.138 [07:51:16] "POST /xmlrpc.php HTTP/1.1"
132.232.47.138 [07:51:18] "POST /xmlrpc.php HTTP/1.1"
...

They’ll hammer your server like that for hours.

Blocking their individual IP addresses at the firewall is devastatingly effective… for about five minutes.  Another bot from another IP will pop up soon.  Blocking individual IPs is a game of whack-a-mole.

Some are part of a “slow” botnet, hitting the same page from unique a IP address each time.  These are part of the large botnets you read about.

83.149.124.238 [05:01:06] "GET /wp-login.php HTTP/1.1" 200
83.149.124.238 [05:01:06] "POST /wp-login.php HTTP/1.1" 200
188.163.45.140 [05:03:38] "GET /wp-login.php HTTP/1.1" 200
188.163.45.140 [05:03:39] "POST /wp-login.php HTTP/1.1" 200
90.150.96.222 [05:04:30] "GET /wp-login.php HTTP/1.1" 200
90.150.96.222 [05:04:32] "POST /wp-login.php HTTP/1.1" 200
178.89.251.56 [05:04:42] "GET /wp-login.php HTTP/1.1" 200
178.89.251.56 [05:04:43] "POST /wp-login.php HTTP/1.1" 200

These are more insidious: patient and hard to spot on a heavily-trafficked blog.

Keeping WordPress Secure

You (hopefully) installed WordPress to a location outside of your “htdocs” document tree.  If not, you should fix that right away!  (Consider this “security tip #0” because without this you’re basically screwed.)

Security tip #1 is to make sure auto updates are enabled.  The slight risk of a botched release being automatically applied is much lower than that of having an critical security patch that is applied too late.

Like medieval door locks on your front door, there is little security advantage to running old software.

Once an exploit is patched, the prior releases are vulnerable as people deconstruct the patch and reverse-engineer the exploit(s) – assuming a exploit wasn’t published before the patch was released.

Locking WordPress Down

Your Apache configuration probably contains a section similar to this:

<Directory "/path/to/wordpress">
    ...
    Require all granted
    ...
</Directory>

We’re going to add some items between <Directory></Directory> tags to restrict access to the most vulnerable pieces.

You Can’t Attack Things You Can’t Reach

We’ll start by invoking the Principle of Least Privilege: people should only be able to do the things they must do, and nothing more.

xmlrpc.php is an API for applications to talk to WordPress.  Unfortunately it doesn’t carry extra security, so if you’re a bot it’s great to hammer with your password guesses – you won’t be blocked, and no one will be alerted.

Most people don’t need it.  Unless you know you need it, you should disable it completely.

<Directory "/path/to/wordpress">
    ...
    <Files xmlrpc.php>
        <RequireAll>
            Require all denied
        </RequireAll>
    </Files>
</Directory>

There are WordPress plugins that purport to “disable” xmlrpc.php, but they deny access from within WordPress.  That means that you’ve still paid a computational price for executing xmlrpc.php, which can be steeper than you expect, and you’re still at risk of exploitable bugs within it.  Denying access to it at the server level is much safer.

You Can’t Log In If You Can’t Reach the Login Page

This next change will block anyone from outside your LAN from logging in.  That means that if you’re away from home you won’t be able to log in, either, without tunneling back home.

<Directory "/path/to/wordpress">
    ...
    <Files wp-login.php>
        <RequireAll>
            Require all granted
            # remember that X-Forwarded-For may contain multiple
            # addresses, don't just search for ^192...
            Require expr %{HTTP:X-Forwarded-For} =~ /\b192\.168\.1\./
        </RequireAll>
    </Files>
</Directory>

If you’re not using a public-facing proxy, and don’t need to look at X-Forwarded-For, you can simplify this a little:

<Directory "/path/to/wordpress">
    ...
    <Files wp-login.php>
        <RequireAll>
            Require all granted
            Require ip 192.168.1
        </RequireAll>
    </Files>
</Directory>

This will prevent 3rd parties from signing up on your blog and submitting comments.  This may be important to you.

Restart Apache

After inserting these blocks, you should execute Apache’s ‘configtest’ followed by reload:

$ sudo apache2ctl configtest
apache2      | * Checking apache2 configuration ...     [ ok ]
$ sudo apache2ctl reload
apache2      | * Gracefully restarting apache2 ...      [ ok ]

Now test your changes from outside your network:

xmlrpc.php forbidden

Apache’s access log should show a ‘403’ (Forbidden) status:

... "GET /xmlrpc.php HTTP/1.1" 403 ...

And just like that, you’ve made your WordPress blog a lot more secure.

Interestingly, by making just these changes on my own site the attacks immediately dropped off by 90%.  I guess that the better-written bots realized that I’m not a good target anymore and stopped wasting their time, preferring lower-hanging fruit.

A Random Flamingo

A quick note about the blog’s new name.

While re-watching The Abominable Bride with a clogged ear, I mis-heard a reference to the Obliquity of the Ecliptic.  The resulting phrase, “The Ubiquity of the Ecliptic,” was confusingly vague and mysterious, just like “A Random Flamingo” was.  Perfect for a family journal.

Mystery Man In My Attic

While demolishing Alpha’s bedroom last month I uncovered a photo tucked into the insulation.

mystery man in rickshaw

It seems to have been deliberately placed, as it was deep in the attic crawlspace between two batts of insulation.

This is one of those photos that spawns more questions than it could possibly hope to answer.

  • When was it taken?  (there are no dates, not even a printers date on the back)
  • Where was it taken?
  • Who is this?
  • Was this someone who used to live here?
  • Why is he sitting in a rickshaw?
  • Why was it in the attic?
  • Who put it there?

If you know anything about this photo, please email me or leave a comment.

Stir-Fried Udon Noodles with Pork

Pan-fried Udon Noodles with Pork

Delicious noodles
Prep Time 10 minutes
Cook Time 20 minutes
Course Main Course
Cuisine Asian
Servings 4 people
Calories 570 kcal

Equipment

  • 1 Skillet large, like 14" cast iron
  • 1 bowl heat-safe

Ingredients
  

  • 3 tbsp Sesame oil
  • 1 bag Slaw mix, dry or about 4 cups chopped cabbage
  • 14 oz Instant udon noodles discard flavor packets, if included
  • 1 pound Ground pork
  • 1 bunch Scallions roughly half-dozen stalks, chopped and separated green from pale
  • 2 tsp Ginger Finely grated or minced
  • 1 tsp Crushed red pepper flakes
  • 1/3 cup Mirin
  • 1/3 cup Soy sauce
  • 1 tbsp Sesame seeds optional

Instructions
 

  • Put on a pot of water to boil, for the noodles later
  • Heat a tablespoon of oil in your skillet over medium-high heat
  • Add cabbage/slaw to the skillet, tossing often, until edges are brown. Reduce heat and continue cooking until thickest parts of the cabbage are tender.
    Remove from heat and transfer cabbage to bowl
  • Wipe out skillet, add a tablespoon of oil, and bring back to medium heat
  • Add pork to skillet, break it up, and cook until browned. Once the meat is broken up, don't keep fussing with it, give it a chance to get browner bits.
  • Once water from step 1 is boiling, turn off heat and add noodles.
    Let noodles sit for 1 minute, then drain. Toss with 1 tablespoon of oil and transfer to bowl with cabbage. Mix together.
  • To the pork, add the pale scallion bits, ginger, and red pepper flakes. Toss for a minute or so, until scallions start to soften.
  • Add noodles and cabbage, mirin, and soy sauce to skillet. Mix until noodles are well-coated with sauce.
  • Remove from heat and toss in green scallion bits and sesame seeds (if desired).

Notes

  • Requires a large skillet.  Our 14″ cast iron is the perfect size.  It’s not quite a one-pot meal, because there’s a swap of ingredients in the middle, but it’s close.
  • Mirin is like sweet sake syrup.  The Japanese equivalent of cooking sherry, you should be able to find bottles of it in the grocery store.
  • The original recipe was pretty strict about amounts, but we’ve found that this recipe is pretty tolerant of variation.
Keyword cabbage slaw, ground pork, noodles

Adapted from https://www.bonappetit.com/recipe/stir-fried-udon-with-pork

Quarantine: Reflections From Week 1

After the first full week of quarantine, some observations.

  1. The public has gone completely crazy.

    By last weekend people had purchased all available stocks of toilet paper, paper towels, kleenex, and ibuprofen. Store shelves were completely bare across the nation.There was no real shortage. Panic buying and speculation rules the day. Stores have mercifully instituted per-person maximum purchases to ensure availability for the unlucky or slow-to-act, so paper products are starting to trickle back onto the shelves.

    whateverToday the shortages are pasta, rice, french fries, and pepperoni. We couldn’t find any presliced pepperoni in Market Basket.

    The veggie aisle continues to be well-stocked, except bananas. (but that’s not completely out of the ordinary.)
  2. Unemployment claims are rising precipitously.  Experts are warning that we could reach 20% unemployment this year.
  3. Street traffic has ticked up a bit.  Presumably people are starting to venture out, but not soon enough to save local small businesses.
  4. Restaurants are still closing, but takeout pizza joints are booming.

    We decided to relax and order pizza from Tremezzo’s Pizza last night.  Megh called in an order at 4:40 pm.  It took nearly an hour for pickup.
  5. Starbucks, as one of the last remaining food service businesses open, is at least as busy as before.  It’s limited to drive-thru and pre-order service (nobody allowed inside) and the line of cars just about reaches the main road.
  6. The kids actually wanted to go out for a drive.

    Last night we went across the street with our pizza and salad for a very fun dinner with Debbie and Tom, followed by a round of cribbage.

    mild shockWhen we got back home around 8 pm the kids asked us to go out for a drive.

    They haven’t been in a car for over a week.  They’ve been outside, but there’s nowhere to go so none of us have been further than the grocery store.  Their friends can’t come out.  It’s weird to go so long without going anywhere, I think it’s comforting to do something familiar like sit in the car.

    We swung by McDonald’s for a treat and just… drove around, the four of us.  We went out to North Reading, swung through Reading, and came home.  It’s weird, but I have to admit that it was relaxing to drive.

    Bonus: there were hardly any cars on the road.

Looking ahead, it seems that we might have to collectively hunker down for months, perhaps a year, perhaps more.

Family Chronicle: COVID-19

“The real winner of this pandemic are the nation’s dogs, who are experiencing unprecedented levels of People Being Home”

If you’re reading this far enough in the future, a bit of context may be needed.

As SARS-CoV-2 entered the United States a few weeks ago, we collectively looked at the ongoing experiences of China and Italy and jokingly compared it to Captain Trips.  Meghan and I studied the history of the Spanish Flu looking for parallels and worst-case scenarios.

The lessons learned from 1918 are being applied by health officials right now, in an effort to avoid a healthcare-system-crushing pandemic.  We can’t avoid contracting the virus, that is clear, but perhaps we can prevent everyone from catching it all at once.

In the middle of last week schools in the Commonwealth of Massachusetts started closing as a preemptive measure.  Many businesses did as well, including my own.  A few did not until they were ordered to. This all mirrors the experiences (and failures) in other countries that were hit by the virus first.

dogs experiencing unprecendented levels of humans being home

As I write this, the governor has ordered all schools closed for at least three weeks.  Large gatherings are prohibited, originally capped at 250 people and now capped at 25.

“These gatherings include all community, civic, public, leisure, faith-based events, sporting events with spectators, concerts, conventions and any similar event or activity that brings together 25 or more people in single room or a single space at the same time.”

— Governor Charlie Baker, March 15 2020

The ban also prohibits eating at restaurants (take-out and delivery are still allowed).  By extension that essentially closes most bars, since you can’t take drinks to go.  Bars garnered a lot of bad press over the weekend as people noted lines “out the door” at many downtown Boston establishments.

So basically we could go out if we really wanted to, but there’s no where to go right now.

Grocery stores are still allowed to be open, so people can buy things eat, but the doomsday preppers have effectively cleaned the shelves.  Stores have struggled to keep essentials in stock, including (oddly) paper products like toilet paper, kleenex, and paper towels, as well as the true essentials that never spoil, like bread, milk, and eggs.  Meghan witnessed someone buying five gallons of milk on Saturday. It’s like snow is coming.

french toast alert system updates for corona virus

Some businesses are instituting, or are relying on, work-from-home policies; unfortunately others, especially service-oriented jobs, are sending people home without pay.

I’m fortunate that I can work from home.  We’ve cleaned out the office so I can get real work done, and made a spot for Butter to curl up.  Meghan’s situation is a little murky, but so far as we can tell she will continue to be paid for the duration.

The kids are starting to get remote assignments from school.  I expect the pace will pick up now that a longer, mandatory stay-at-home order is in place.  Some schools in harder-hit areas have stayed open because they support homeless and needy children, providing much-needed meals and warm places to wash up.

Baba has been asking for advice on what social events to attend.  (answer: zero.)  My own parents have continued to live like nothing has changed, though they’re a bit less social than Baba.  All three grand-parental-units are in multiple high-risk groups.  Connecticut has been less affected by the outbreak so far.  I’ve got my fingers crossed that they’ll come through without contracting it.

Bypassing a Tunnel-Broker IPv6 Address For Netflix

Surprisingly, it worked beautifully… that is, until I discovered an unintended side effect

My ISP is pretty terrible but living in the United States, as I do, effectively makes internet service a regional monopoly.  In my case, not only do I pay too much for service but certain websites (cough google.com cough) are incredibly slow for no reason other than my ISP is a dick and won’t peer with them properly.

This particular ISP, despite being very large, has so far refused to roll out IPv6.  This was annoying until I figured out that I could use this to my advantage.  If they won’t peer properly over IPv4, maybe I can go through a tunnel broker to get IPv6 and route around them.  Surprisingly, it worked beautifully.  GMail has never loaded so fast at home.

It was beautiful, that is, until I discovered an unintended side effect: Netflix stopped working.

netflix error: you seem to be using an unblocker or proxy
Despite my brokered tunnel terminating inside the United States, Netflix suspects me of coming from outside the United States.

A quick Google search confirmed my suspicion.  Netflix denies access to known proxies, VPNs, and, sadly, IPv6 tunnel brokers.  My brave new world was about to somewhat less entertaining if I couldn’t fix this.

Background

Normally a DNS lookup returns both A (IPv4) and AAAA (IPv6) records together:

$ nslookup google.com
Server:     192.168.1.2
Address:    192.168.1.2#53

Non-authoritative answer:
Name:   google.com
Address: 172.217.12.142
Name:   google.com
Address: 2607:f8b0:4006:819::200e

Some services will choose to provide multiple addresses for redundancy; if the first address doesn’t answer then your computer will automatically try the next in line.

Netflix in particular will return a large number of addresses:

$ nslookup netflix.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53

Non-authoritative answer:
Name: netflix.com
Address: 54.152.239.3
Name: netflix.com
Address: 52.206.122.138
Name: netflix.com
Address: 35.168.183.177
Name: netflix.com
Address: 54.210.113.65
Name: netflix.com
Address: 52.54.154.226
Name: netflix.com
Address: 54.164.254.216
Name: netflix.com
Address: 54.165.157.123
Name: netflix.com
Address: 107.23.222.64
Name: netflix.com
Address: 2406:da00:ff00::3436:9ae2
Name: netflix.com
Address: 2406:da00:ff00::6b17:de40
Name: netflix.com
Address: 2406:da00:ff00::34ce:7a8a
Name: netflix.com
Address: 2406:da00:ff00::36a5:f668
Name: netflix.com
Address: 2406:da00:ff00::36a5:9d7b
Name: netflix.com
Address: 2406:da00:ff00::23a8:b7b1
Name: netflix.com
Address: 2406:da00:ff00::36d2:7141
Name: netflix.com
Address: 2406:da00:ff00::36a4:fed8

The Solution

The key is to have your local DNS resolver return A records, but not AAAA, if (and only if) it’s one of Netflix’s hostnames.

Before I document the solution, it helps to know my particular setup and assumptions:

  • IPv6 via a tunnel broker
  • BIND’s named v9.14.8

Earlier versions of BIND are configured somewhat differently: you may have different options, or (if it’s a really old build) you may need to run two separate named instances.  YMMV.

Step 0: Break Out Your Zone Info (optional but recommended)

If your zone info is part of named.conf you really should put it into it’s own file for easier maintenance and re-usability. The remaining instructions won’t work, without modification, if you don’t.

# /etc/bind/local.conf
zone "." in {
        type hint;
        file "/var/bind/named.cache";
};

zone "localhost" IN {
        type master;
        file "pri/localhost.zone";
        notify no;
};

# 127.0.0. zone.
zone "0.0.127.in-addr.arpa" {
        type master;
        file "pri/0.0.127.zone";
};

Step 1: Add a New IP Address

You can run a single instance of named but you’ll need at least two IP addresses to handle responses.

In this example the DNS server’s “main” IP address is 192.168.1.2 and the new IP address will be 192.168.1.3.

How you do this depends on your distribution. If you’re using openrc and netifrc then you only need to modify /etc/conf.d/net:

# Gentoo and other netifrc-using distributions
config_eth0="192.168.1.2/24 192.168.1.3/24"

Step 2: Listen To Your New Address

Add your new IP address to your listen-on directive, which is probably in /etc/bind/named.conf:

listen-on port 53 { 127.0.0.1; 192.168.1.2; 192.168.1.3; };

It’s possible that your directive doesn’t specify the IP address(es) and/or you don’t even have a listen-on directive – and that’s ok. From the manual:

The server will listen on all interfaces allowed by the address match list. If a port is not specified, port 53 will be used… If no listen-on is specified, the server will listen on port 53 on all IPv4 interfaces.

https://downloads.isc.org/isc/bind9/9.14.8/doc/arm/Bv9ARM.ch05.html

Everything I just said also applies to listen-on-v6.

Step 3: Filter Query Responses

Create a new file called /etc/bind/limited-ipv6.conf and add the following at the top:

view "internal-ipv4only" {
        match-destinations { 192.168.1.3; };
        plugin query "filter-aaaa.so" {
                # don't return ipv6 addresses
                filter-aaaa-on-v4 yes;
                filter-aaaa-on-v6 yes;
        };
};

What this block is saying is, if a request comes in on the new address, pass it through the filter-aaaa plugin.

We’re configuring the plugin to filter all AAAA record replies to ipv4 clients (filter-aaaa-on-v4) and ipv6 clients (filter-aaaa-on-v6).

Now add a new block after the first block, or modify your existing default view:

# forward certain domains back to the ipv4-only view
view "internal" {
        include "/etc/bind/local.conf";

        # AAAA zones to ignore
        zone "netflix.com" {
                type forward;
                forward only;
                forwarders { 192.168.1.3; };
        };
};

This is the default view for internal clients. Requests that don’t match preceding views fall through here.

We’re importing the local zone from step 0 (so we don’t have to maintain two copies of the same information), then forwarding all netflix.com look-ups to the new IP address, which will be handled by the internal-ipv4only view.

Step 4: Include the New Configuration File

Modify /etc/bind/named.conf again, so we’re loading the new configuration file (which includes local.conf).

#include "/etc/bind/local.conf";
include "/etc/bind/limited-ipv6.conf";

Restart named after you make this change.

Testing

nslookup can help you test and troubleshoot.

In the example below we call the “normal” service and get both A and AAAA records, but when we call the ipv4-only service we only get A records:

$ nslookup google.com 192.168.1.2
Server:         192.168.1.2
Address:        192.168.1.2#53

Non-authoritative answer:
Name:   google.com
Address: 172.217.3.110
Name:   google.com
Address: 2607:f8b0:4006:803::200e

$ nslookup google.com 192.168.1.3
Server:         192.168.1.3
Address:        192.168.1.3#53

Non-authoritative answer:
Name:   google.com
Address: 172.217.3.110