Fixing WordPress Security Recommendations

Fixing WordPress Security Recommendations

So.. I log into my WordPress server, and under site health on the Dashboard, the yellow… basically sayin…. “Do better”

Anyway, I asked Co-Pilot for help on this (Specifically about the warning about a dated PHP version [7.4]), and it basically said:

🛠️ Update PHP for Your WordPress Site

1. Check Your Current PHP Version

  • Go to your WordPress dashboard
  • Navigate to Tools > Site Health > Info tab
  • Expand the Server section to see your PHP version

2. Back Up Your Site First

  • Use a plugin like UpdraftPlus or Duplicator
  • Save both your files and database—just in case something breaks

3. Update PHP via Hosting Control Panel

Depending on your host, the process varies slightly:

Hosting Provider How to Update PHP
Bluehost Go to Websites > Settings > PHP Version, choose the latest version
SiteGround Use Site Tools > Devs > PHP Manager, select manual update
Hostinger Go to Websites > Manage > PHP Configuration, pick a new version
cPanel Hosts Use MultiPHP Manager under the Software section

If you’re unsure where to find this, your host’s support team can guide you.

4. Test Compatibility

  • Use the PHP Compatibility Checker plugin to scan for issues
  • Or create a staging site to test the update safely

5. Update WordPress, Plugins, and Themes

  • Make sure everything is up to date before switching PHP versions
  • This reduces the chance of conflicts

Great, except that I’m self hosting… I have no interest in my access to a service pay walled and managed via poorly coded web sites.  So I followed up with Co-Pilot which it then wanted me to basically add a third party repo, in this specific case, a PPA “ondreJ”. I had a couple issues with this.

  1. This is known as a Frankstein build.
  2. Managing third party repo’s can cause issues, and is usually frown’d apon.

These are mentioned in this reddit post where a sysadmin was confused and this type of setup was instantly caught by others with experience. Which is what AI was telling me to do, it’s reasoning was as follows:

“Ubuntu’s default repositories only include certain PHP versions—usually the ones that were current when that Ubuntu release came out. So if you’re on, say, Ubuntu 22.04 or 24.04, you might only get PHP 8.1 or 8.2 from the official repos. Anything newer (like PHP 8.3 or 8.4)? Not there yet.

That’s why developers lean on Ondřej Surý’s PPA (ppa:ondrej/php). He’s a trusted Debian developer who maintains up-to-date PHP packages for Ubuntu and Debian.”

When asked why someone woudl have to install the actual maintainers repo, instead of those builds being pushed to the OS default repo’s, AI stated this:

“Here’s the short version: distro maintainers often freeze package versions when they release a new OS version—for stability and compatibility across thousands of other packages. So even if the upstream maintainer (like Ondřej) has newer PHP versions ready to go, the official Ubuntu repo won’t touch them unless there’s a security patch or they release a new Ubuntu version that includes the updated language package.”

Anyway… instead of all this rubbish, it makes more sense to upgrade the base OS at this point. So let’s do that.

🛠️ Upgrading Base OS Debain 11 to 12

1. Back Up Your System

You’ve already got backups—perfect. Just make sure you’ve got /etc, /var/lib/dpkg, and /var/lib/apt/extended_states covered.

2. Update Current System

sudo apt update
sudo apt upgrade
sudo apt full-upgrade
sudo apt --purge autoremove

3. Edit Your APT Sources

Replace all instances of bullseye with bookworm in your sources list:

sudo sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list

If you use additional repos in /etc/apt/sources.list.d/, update those too:

sudo sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list.d/*

Optionally, add the new non-free-firmware section:

sudo sed -i 's/non-free/non-free non-free-firmware/g' /etc/apt/sources.list

4. Refresh Package Index

sudo apt update

5. Minimal Upgrade First

sudo apt upgrade --without-new-pkgs

6. Full System Upgrade

sudo apt full-upgrade

Follow any prompts carefully—especially ones about restarting services or replacing config files.

7. Reboot

sudo reboot

8. Verify Upgrade

bash
lsb_release -a
cat /etc/debian_version
After the reboot I was back in to my wordpress admin portal, and the site warning was gone. Yay… but some still remain.. lets see if we can clean these up.. on at a time:
right inactive plugins, that should be easy enough. Deleted 2 unused plugins, and sure enough the warning is gone. Same with themes, let’s do that next.
Appearance -> Themes -> Theme Details -> Delete.
Older version of PHP… well here we go again, so we went from a site warning to site good, but it still wants php8.3… which was discussed above. Frankenstein the build or wait for a new base OS, I’ll await Debian 13 before introducing a Frankenstein Debian build. We’ll ignore this warning.

Recommended Module: ZIP

This one is a bit annoying cause it can go two ways:
1) Sure, I can install this module, but if I don’t need it why would I? Just to remove this warning, I suppose.

Fair question—and the short answer is: probably not essential, but it’s handy to have. 👍

The zip module isn’t required for WordPress to function at a basic level. If your site is running smoothly, you’re likely not using any plugins or features that rely on it yet. But here’s why WordPress flags it:

📦 What the zip Module Is Used For

  • Plugin installation and updates (especially from ZIP files)
  • Theme import/export
  • Backup plugins (e.g., Duplicator, UpdraftPlus)
  • Migration tools

If you’re manually installing things via FTP or don’t use plugins that automate packaging and extraction, you’re fine without it. WordPress just lists it because a lot of plugins assume it’s present.

🧠 TL;DR

  • ✅ Your site can run without it
  • ⚠️ If you install or update ZIP-based plugins through the admin interface, you might hit errors
  • 🛠️ It’s a tiny module and safe to install if you’re future-proofing

How to?

🛠️ Enable the zip PHP Module on Debian 12

1. Install the Extension

Make sure you install the correct version for your PHP setup. If you’re using PHP 8.2 (the default in Debian 12):

sudo apt update
sudo apt install php8.2-zip

If you’ve upgraded to PHP 8.3 via SURY:

sudo apt install php8.3-zip

2. Restart Your Web Server

Depending on your setup:

sudo systemctl restart apache2

3. Verify Installation

Run:

php -m | grep zip

If it returns zip, you’re good to go.

2) Don’t install it from a security stand point:

Security Angle: Why Not Installing zip Might Be Safer

  • Less attack surface: Fewer PHP extensions mean fewer potential vulnerabilities, especially from bugs in parsing zip headers or malicious zip payloads.
  • Blocks auto-installs: Without ZipArchive, plugins/themes that use zipped packages via the WordPress admin can’t self-extract. That forces all installs to be manual—good for auditability.
  • Script-based exploits are blunted: Malware that attempts to download, unpack, and activate zip files through vulnerable plugins or upload endpoints will hit a wall.
  • Prevents sneaky plugin drops: No zip means plugins can’t unpack themselves unnoticed during malicious file uploads or improperly sanitized form inputs.

⚠️ Trade-Offs

  • Some legit backup or migration plugins won’t work—or will throw warnings
  • Manual updates and installs are more tedious
  • You may need to temporarily install it for specific maintenance tasks, then uninstall afterward

Object Cache

This one was a bit more annoying than I wanted it to be simply because it’s got many options. beside 1) Do it vs 2) Don’t do it.

Right now, my site runs fine but doesn’t want a faster site… right… Right?

🔴 Redis

Pros

  • Very fast and widely adopted
  • Works across multiple servers (great for scaling)
  • Excellent support from plugins like Redis Object Cache
  • Stores complex data types (not just key-value pairs)
  • Can be configured for persistence (disk backup of cache)

⚠️ Cons

  • Uses more memory than simpler caches
  • Requires a background daemon (redis-server)
  • Overkill for tiny or low-traffic sites

🔵 Memcached

Pros

  • Lightweight and blazing fast
  • Great for simple key-value object caching
  • Minimal resource usage—ideal for single-server setups

⚠️ Cons

  • Doesn’t support complex data types
  • No persistence: cache is lost if the server reboots
  • Fewer modern plugin options compared to Redis

🟣 APCu

Pros

  • Fast, simple, and bundled with PHP
  • No external services required—runs in-process
  • Perfect for single-server, low-footprint setups

⚠️ Cons

  • Only works per process: no shared cache across servers
  • Not ideal for large or complex sites
  • Might get flushed more often depending on your PHP configuration

In my case I’m going to try memcached, why I unno….

🧰 Install Memcached + WordPress Integration

1. Install Memcached Server + PHP Extension

bash
sudo apt update
sudo apt install memcached php8.2-memcached
sudo systemctl enable memcached
sudo systemctl start memcached

Replace php8.2 with your actual PHP version if needed.

2. Verify Memcached Is Running

echo "stats settings" | nc localhost 11211

You can use Bash’s built-in TCP support:

exec 3<>/dev/tcp/127.0.0.1/11211
echo -e "stats\r\nquit\r\n" >&3
cat <&3

This opens a raw TCP connection and sends the stats command directly.

You should see a list of stats—if not, Memcached isn’t active.

3. Install a WordPress Plugin

The most common plugin for Memcached integration is W3 Total Cache:

  • Go to your WordPress dashboard
  • Navigate to Plugins > Add New
  • Search for W3 Total Cache
  • Click Install, then Activate

4. Configure W3 Total Cache for Memcached

  • Go to Performance > General Settings
  • Under Object Cache, enable it and select Memcached as the method
    *If memcached is not selectable restart web service (apache2)
  • Do the same for Database Cache if desired
  • Save settings and purge cache

✅ Optional: Use a Drop-In Instead of a Plugin

If you want to skip plugins entirely:

  • Download object-cache.php from Memcachy
  • Place it in /wp-content/
  • Add this to wp-config.php:
    php
    define('WP_CACHE', true);
    $memcached_servers = array(
      array('127.0.0.1', 11211)
    );
    

This gives you Memcached support without a full plugin UI.

After installing the backend modules and the plugin, and configuring memcached as the object cache, site health finally removed that warning too.. now the final one.. HTTPS.

HTTPS

Now it’s not possible to access my site via HTTP, not externally anyway. So, why the warning?

Ah, that’s a classic WordPress quirk—your browser’s using HTTPS, but WordPress itself still thinks it’s living in the insecure past. 😅

Here’s how to fix it since you’re self-hosting:

🔐 Update WordPress URLs to Use HTTPS

  1. Log into your WordPress Admin Dashboard
  2. Go to Settings > General
  3. Update both:
    • WordPress Address (URL)
    • Site Address (URL) to use https://yourdomain.com instead of http://
  4. Scroll down and click Save Changes

But it’s greyed out…

🛠️ How to Fix It

  1. Open your wp-config.php file It’s located in the root of your WordPress installation (same folder as wp-load.php and wp-settings.php).
  2. Look for these lines:
    define('WP_HOME','http://yourdomain.com');
    define('WP_SITEURL','http://yourdomain.com');
    

    Or they might be using https:// if you’ve already set up SSL.

  3. Delete or comment them out:
    php
    // define('WP_HOME','https://yourdomain.com');
    // define('WP_SITEURL','https://yourdomain.com');
    
  4. Save the file and reload your WordPress dashboard The fields under Settings > General should now be editable.

mhmm, uhhh commenting them out caused my site to simple redirect to local loop back IP which of course the system I’m navigating my site on isn’t hosting a web server thus the site timeouts.

yeah, that redirect to 127.0.0.1 or localhost is a classic symptom of WordPress falling back to default behavior when it can’t find proper site URLs. Here’s what likely happened:

🧠 Why It Redirected to Localhost

When you commented out WP_HOME and WP_SITEURL in wp-config.php, WordPress tried to pull the site URL from the database (wp_options table). If those values are still set to http://localhost or http://127.0.0.1, it’ll redirect there—even if you’re accessing the site from a public domain.

🛠️ How to Fix It

✅ Option 1: Restore the Correct URLs in wp-config.php

Uncomment and update the lines like this:

define('WP_HOME', 'https://yourdomain.com');
define('WP_SITEURL', 'https://yourdomain.com');

Replace yourdomain.com with your actual domain name.

✅ Option 2: Update URLs Directly in the Database

If you want to manage URLs from the dashboard again, you’ll need to fix the database values:

  1. Access phpMyAdmin or use the MySQL CLI
  2. Run:
    sql
    UPDATE wp_options SET option_value = 'https://yourdomain.com' WHERE option_name IN ('siteurl', 'home');
    
  3. Clear your browser cache and reload the site

You can then safely remove the hardcoded lines from wp-config.php and regain control via the WordPress admin panel.

FINALLY What a PITA… Hope this helps someone…

Small Rabbit Hole

Allllllright, I wanted to check if I was even running phpMyAdmin (I vaguely remembering that it was part of turnkey Linux setup, which I THINK is what this was originally spun up from, which had such services baked in for ease of use)… ANYWAY, I went to try n figure this out my simply checking my servers listening ports… when I found IPV6

🚫 IPv6

I dislike it, others disagree. if it be supported (by now it’s pretty widely adopted), or if it’s something you need.. ughhh, then give er… let  the world be your oyster or some dumb shit. I personally don’t like the idea of everything having a fully publicly routeable IP address.. if it even works that way.. unno… I still stick to IPv4 where, yes I use NAT… ooo nooooo…

Anyway long story short I wanted to disable IPv6 on my WordPress server…

🧱 Method 1: Disable via sysctl (Persistent)

Edit the system config file:

sudo nano /etc/sysctl.conf

Add these lines at the end:

net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1

Apply changes:

sudo sysctl -p

I did this but still found listening port on IPv6 (specifically SSH and NTP), I could have reconfigured this services butt, instead..

🧨 Method 2: Blacklist the IPv6 Kernel Module

Create a blacklist file:

sudo nano /etc/modprobe.d/blacklist-ipv6.conf

Add:

blacklist ipv6

Then update initramfs:

sudo update-initramfs -u
sudo reboot

This didn’t work for me.

🧪 Method 3: Disable via GRUB Boot Parameters

Edit GRUB config:

sudo nano /etc/default/grub

Find the line starting with GRUB_CMDLINE_LINUX_DEFAULT and add:

ipv6.disable=1

Example:

GRUB_CMDLINE_LINUX_DEFAULT="quiet ipv6.disable=1"

Update GRUB:

sudo update-grub
sudo reboot

This finally worked!

Rabbit Hole #2: stunnel4

what is stunnel4

Stunnel4 is a proxy tool that wraps unencrypted traffic in SSL/TLS encryption, allowing legacy or non-secure services to communicate securely without modifying their original code. Think of it as a secure tunnel that sits between a client and a server, encrypting everything that passes through.

🔐 What Stunnel4 Does

  • Encrypts plaintext protocols like HTTP, SMTP, FTP, or Telnet
  • Acts as a TLS/SSL wrapper for services that don’t natively support encryption
  • Can run in client or server mode
  • Supports load balancing, certificate validation, and FIPS-compliant cryptography
  • Uses OpenSSL under the hood for encryption algorithms

🧠 How It Works

Imagine you have a mail server that only speaks plain SMTP on port 25. You can configure Stunnel to:

  • Accept encrypted connections on port 465
  • Decrypt the traffic
  • Forward it to port 25 locally

This way, clients connect securely—even though the mail server itself is oblivious to SSL.

🛠️ Typical Use Cases

  • Securing legacy services without rewriting them
  • Tunneling blocked protocols (e.g. SSH over HTTPS)
  • Adding SSL to VPNs (like OpenVPN)
  • Acting as a secure gateway for internal services

Want help setting up a config file or using it to secure a specific service like SMTP or Redis? I can walk you through it.

“Tunneling blocked protocols (e.g. SSH over HTTPS)” This is exactly what I see my system doign on port 12320.. and it shows as service stunel4. but I don’t know if I want to stop the service as a whole…

You’re spot-on in diagnosing that behavior—port 12320 with stunnel4 almost certainly means your system is tunneling SSH over HTTPS, or at least exposing a secure channel for remote access via SSL. This is commonly used in stacks like Webmin/Virtualmin/Usermin, where stunnel4 wraps non-HTTPS services to provide secure access through a browser

🔍 What Port 12320 Usually Does with stunnel4

  • Acts as a gateway to Webmin or Usermin shell access
  • Allows remote file management, terminal, or SSH-like features over HTTPS
  • Wrapped securely so that firewalls see it as “harmless” SSL traffic

You can confirm this by checking:

sudo systemctl status stunnel4
cat /etc/stunnel/stunnel.conf

Or:

grep -r 12320 /etc/stunnel/

That config file should reveal exactly which service is being tunneled.

⚠️ Should You Stop It?

That depends on:

  • Do you use Webmin/Virtualmin remotely?
  • Are you managing your server through a web browser interface?
  • Is 12320 exposed to the public internet or locked behind a firewall?

If you don’t use it and it’s open to the world, you could:

  • Disable it safely:
    sudo systemctl disable --now stunnel4
    
  • Or edit the config to disable only port 12320, leaving other tunnels active

But if you rely on browser-based admin tools, killing stunnel4 might break access to file manager or terminal tools from the web UI.

🧠 Pro Tip

Rather than killing the whole service, you can:

  • Comment out the 12320 tunnel in stunnel.conf
  • Reload the config:
    bash
    sudo systemctl restart stunnel4
    

That gives you selective control—without losing secure access to services you still need.

Want help editing that config file or running a quick audit on what’s listening? I can help you tailor your server’s security posture to your exact setup.

stunnel4.service, Loaded: masked (Reason: unit stunnel4.service is maked.), Active: inactive (dead) grep -r 12320 /etc/stunnel/ /etc/stunnel/shellinabox.conf:12320 So, there it is the shell (SSH) service using stunnel, which is why I see a consoel when I access https://wordpress:12320. But why is the service saying dead when I clearly see it bound via ss command?

Excellent deduction — you’re practically reverse-engineering the system just by reading its sockets. 🧠🔧 Let’s break down what’s happening:

🧩 Why stunnel4.service Says Dead but the Port Is Active

You’re seeing a listening socket on port 12320, likely from ShellInABox wrapped by stunnel, but stunnel4.service reports:

Loaded: masked
Active: inactive (dead)

Here’s the likely cause:

⚙️ Stunnel is running, but not via the stunnel4.service unit

  • The service is masked, which means it’s intentionally disabled from being started or stopped using systemctl.
  • This doesn’t stop you (or another app) from manually launching Stunnel as a background process, or from invoking it via a custom systemd unit, init script, or even Webmin/Virtualmin.

🧠 ss shows the port because:

  • The process was started outside the official service file, possibly via:
    • A different systemd unit (e.g., webmin-stunnel.service)
    • A direct call in /etc/rc.local or cron or Webmin’s internal startup sequence
    • A bundled wrapper from ShellInABox or another tool

🔍 How to Find the Real Parent Process

Let’s track down what’s owning port 12320:

sudo lsof -i :12320

Or:

sudo ss -tulpn | grep 12320

This will show you the actual PID and the command or executable responsible. Likely output will say something like:

LISTEN  ... 12320  ... /usr/bin/stunnel

Once you get the PID, trace it:

ps -fp <PID>

🛠️ How to Disable That Specific Tunnel (Without Killing Stunnel Globally)

You can comment out or delete the tunnel block in /etc/stunnel/shellinabox.conf. Try this:

  1. Edit the config:
    sudo nano /etc/stunnel/shellinabox.conf
    
  2. Comment out all lines by adding #:
    conf
    # accept = 12320
    # connect = 127.0.0.1:12319
    # TIMEOUTclose = 0
    
  3. Save and exit, then reboot.

Rabbit Hole #3: Master

using ss -plunt it shows as service “master” I can’t see that in webmin.. what’s going on here? Port 25

Ah, the mysterious "master" process — that’s actually Postfix, your mail transfer agent (MTA). 📨

When you run:

sudo ss -plunt | grep :25

and see something like:

LISTEN 0 100 0.0.0.0:25 0.0.0.0:* users:(("master",pid=1234,fd=12))

the "master" refers to the Postfix master daemon, which coordinates all the individual Postfix services (like smtp, pickup, qmgr, etc.).

I disabled postfix and sure enough port 25 listener went away.

I also disabled NTP service, to drop listening on port 123.

OK, final one, I saw system listening on port  UDP 10000. I saw one that was TCP based but it was only bound to loopback so I didn’t care about that much, and research showed it’s used by webmin. however I didn’t like the UDP cause it was listening to all interfaces on the system (0.0.0.0).

After enough searching I found it configured by webmin itself.

webmin -> Webmin Configurtaions -> ports and addresses -> “Listen for broadcasts on UDP port” but what is this used for?

The “Listen for broadcasts on UDP port” option in Webmin is used for auto-discovery of other Webmin servers on the local network. Here’s what it’s doing:

📡 What It’s For

  • Webmin listens on UDP port 10000 to receive broadcast packets from other systems running Webmin.
  • These packets are part of the Webmin Servers Index module, which lets you:
    • Scan your LAN for other Webmin instances
    • Automatically add them to a central dashboard
    • Optionally control them via RPC (Remote Procedure Calls)

🧠 Why It’s Optional

  • If you’re not using clustered Webmin setups or don’t need to discover other servers, it’s safe to disable.
  • Disabling it stops Webmin from binding to UDP 10000, which:
    • Reduces your attack surface
    • Cleans up your ss output
    • Avoids confusion when auditing open ports

✅ What Happens When You Turn It Off

  • Webmin will no longer respond to broadcast discovery requests
  • You can still manually add other servers to the Webmin Servers Index if needed
  • The TCP listener on port 10000 (for HTTPS access) remains unaffected

Hope all these tweaks helps someone.

Leave a Reply

Your email address will not be published. Required fields are marked *