<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nick</title>
    <description>The latest articles on DEV Community by Nick (@wefixitgr).</description>
    <link>https://dev.to/wefixitgr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F672105%2F3de91fa7-373d-4d1e-a126-a0b415490534.png</url>
      <title>DEV Community: Nick</title>
      <link>https://dev.to/wefixitgr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wefixitgr"/>
    <language>en</language>
    <item>
      <title>Solving WordPress Security: What If We Just Didn't Trust Admins?</title>
      <dc:creator>Nick</dc:creator>
      <pubDate>Tue, 28 Oct 2025 15:52:40 +0000</pubDate>
      <link>https://dev.to/wefixitgr/solving-wordpress-security-what-if-we-just-didnt-trust-admins-356n</link>
      <guid>https://dev.to/wefixitgr/solving-wordpress-security-what-if-we-just-didnt-trust-admins-356n</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Your Clients Are Why You Can't Have Nice Things
&lt;/h2&gt;

&lt;p&gt;Let me paint you a picture. It's 2:47 AM on a Saturday. Your phone buzzes. It's &lt;em&gt;that&lt;/em&gt; client. You know the one. Subject line: "URGENT: SITE IS HACKED!!!"&lt;/p&gt;

&lt;p&gt;You roll out of bed, fire up your laptop, and sure enough—the site is serving Viagra ads to Google. Again. This is the third time this quarter.&lt;/p&gt;

&lt;p&gt;You know what happened. The same thing that always happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client demanded administrator access (they're paying you, after all!)&lt;/li&gt;
&lt;li&gt;Client used "admin123" as their password (despite your strongly worded email)&lt;/li&gt;
&lt;li&gt;Client clicked on a phishing link that looked &lt;em&gt;totally legitimate&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Attacker logged in with those credentials (no alarms, everything looks normal)&lt;/li&gt;
&lt;li&gt;Attacker quietly installed a backdoor plugin called "Totally Legitimate SEO Booster Pro"&lt;/li&gt;
&lt;li&gt;Attacker came back later to wreak absolute havoc&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your brilliant solution of "please use a stronger password" hasn't worked for the last 47 times you've suggested it. Time for a different approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nuclear Option: What If Admin Credentials Were Useless?
&lt;/h2&gt;

&lt;p&gt;Here's a radical thought experiment: What if an attacker could steal perfectly valid admin credentials and still couldn't do anything meaningful with them?&lt;/p&gt;

&lt;p&gt;I'm not talking about 2FA or rate limiting or any of those sensible, well-adjusted security measures. I'm talking about the scorched-earth approach: &lt;strong&gt;Strip admin users of every dangerous capability, and make them go through YOU for everything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/ngalatis/wp-fort-knox" rel="noopener noreferrer"&gt;WP Fort Knox&lt;/a&gt;: a WordPress security plugin for the delightfully paranoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophy: Paranoia Is Just Good Planning
&lt;/h2&gt;

&lt;p&gt;WP Fort Knox operates on a simple principle: &lt;strong&gt;If you can't trust your clients with their credentials (and you can't), don't give them the capabilities that matter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What it blocks through wp-admin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ No plugin installation, updates, or deletions&lt;/li&gt;
&lt;li&gt;❌ No file editing (themes, plugins, anything)&lt;/li&gt;
&lt;li&gt;❌ No creating admin users&lt;/li&gt;
&lt;li&gt;❌ No promoting users to admin&lt;/li&gt;
&lt;li&gt;❌ The admin role doesn't even &lt;em&gt;show up&lt;/em&gt; in dropdowns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What still works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ WP-CLI does everything (because YOU control WP-CLI)&lt;/li&gt;
&lt;li&gt;✅ Content management (they can still do their actual job)&lt;/li&gt;
&lt;li&gt;✅ User management (non-admin users)&lt;/li&gt;
&lt;li&gt;✅ Theme customization (that doesn't require file writes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when an attacker inevitably gets admin credentials, they log in and... can't do anything useful. No backdoor plugins. No malicious code injection. No new admin accounts to hide their tracks.&lt;/p&gt;

&lt;p&gt;They're just sitting there with an admin login, unable to cause any real damage, probably questioning their life choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Bit: Runtime Filtering &amp;gt; Database Vandalism
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting for you developer types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version 1.0.0&lt;/strong&gt; (The Sledgehammer Era) actually &lt;em&gt;deleted&lt;/em&gt; capabilities from the WordPress database. Effective? Sure. Reversible? Only if you enjoyed typing WP-CLI incantations at 3 AM. Disable the plugin and your admins were still crippled. Permanent consequences are so 2023.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version 2.0.0&lt;/strong&gt; (The Velvet Rope Era) uses runtime filtering instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;filter_user_capabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$allcaps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$caps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_disabled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$allcaps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// If this is a WP-CLI context, don't filter anything&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_CLI'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;WP_CLI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$allcaps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove dangerous capabilities at runtime&lt;/span&gt;
    &lt;span class="nv"&gt;$blocked_caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'install_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'upload_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="s1"&gt;'update_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'delete_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$blocked_caps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$cap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$allcaps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$cap&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$allcaps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The capabilities are still in the database, untouched. We just filter them out when WordPress checks them. Disable the plugin? Everything returns to normal automatically. No database restoration ceremonies required.&lt;/p&gt;

&lt;p&gt;It's the same paranoia, but with fewer support tickets from yourself at 4 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: Command Line Master Race
&lt;/h2&gt;

&lt;p&gt;Since everything file-related goes through WP-CLI now, here's your new life:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Client: "Can you install this SEO plugin?"&lt;/span&gt;
wp plugin &lt;span class="nb"&gt;install &lt;/span&gt;wordpress-seo &lt;span class="nt"&gt;--activate&lt;/span&gt;

&lt;span class="c"&gt;# Client: "I need admin access for my nephew who 'knows computers'"&lt;/span&gt;
wp user create nephew nephew@example.com &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;editor
&lt;span class="c"&gt;# (Notice we gave him Editor, not Administrator. Oops.)&lt;/span&gt;

&lt;span class="c"&gt;# Client: "Can you update everything?"&lt;/span&gt;
wp plugin update &lt;span class="nt"&gt;--all&lt;/span&gt;
wp theme update &lt;span class="nt"&gt;--all&lt;/span&gt;  
wp core update

&lt;span class="c"&gt;# Client: "I can't do anything anymore!"&lt;/span&gt;
&lt;span class="c"&gt;# You: "Working as intended 😌"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Leverage Play (The Quiet Part Out Loud)
&lt;/h2&gt;

&lt;p&gt;Look, I'm not saying this is &lt;em&gt;the&lt;/em&gt; reason to use this plugin, but it's certainly an added bonus: clients who mysteriously forget to pay their hosting bills also mysteriously can't maintain their own sites without you.&lt;/p&gt;

&lt;p&gt;No payment? No updates. Updates piling up? Security vulnerabilities stacking up. Site getting slow? Can't install that caching plugin themselves.&lt;/p&gt;

&lt;p&gt;Hard to leave your service provider without paying when you literally can't manage your own site. Just saying.&lt;/p&gt;

&lt;p&gt;(This is a joke. Mostly. Deploy responsibly. Maybe.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation: Must-Use Plugins Only
&lt;/h2&gt;

&lt;p&gt;This lives in &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt; because putting it in regular plugins would be like installing a lock &lt;em&gt;inside&lt;/em&gt; the house. Defeats the whole purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The civilized way (WP-CLI)&lt;/span&gt;
wp &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'
$mu_dir = WP_CONTENT_DIR . "/mu-plugins";
if (!is_dir($mu_dir)) { mkdir($mu_dir, 0755, true); }
file_put_contents(
    $mu_dir . "/wp-fort-knox.php",
    file_get_contents("https://raw.githubusercontent.com/ngalatis/wp-fort-knox/v2.0.0/wp-fort-knox.php")
);
echo "WP Fort Knox installed. Paranoia activated.\n";
'&lt;/span&gt;

&lt;span class="c"&gt;# Verify it's working&lt;/span&gt;
wp &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'var_dump(class_exists("WP_Fort_Knox"));'&lt;/span&gt;
&lt;span class="c"&gt;# Should output: bool(true)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;This plugin is &lt;em&gt;NOT&lt;/em&gt; for everyone. It's for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers managing client sites who are tired of 3 AM emergencies&lt;/li&gt;
&lt;li&gt;Agencies who've cleaned up malware one too many times&lt;/li&gt;
&lt;li&gt;Anyone who's had "the site is hacked" call interrupt a Friday night&lt;/li&gt;
&lt;li&gt;Paranoid sysadmins (the best kind)&lt;/li&gt;
&lt;li&gt;People who've accepted that clients cannot be trusted with power&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Is This &lt;em&gt;NOT&lt;/em&gt; For?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Casual WordPress users who just want a blog&lt;/li&gt;
&lt;li&gt;Anyone without SSH access and WP-CLI proficiency&lt;/li&gt;
&lt;li&gt;People who think "admin123" is a perfectly good password&lt;/li&gt;
&lt;li&gt;Sites with responsible, security-conscious clients (do those exist?)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Trade-Off
&lt;/h2&gt;

&lt;p&gt;Yes, this is aggressive. Yes, your clients will complain they can't install that "FREE SEO BOOSTER 10000" plugin anymore. Yes, you become a bottleneck for updates.&lt;/p&gt;

&lt;p&gt;But you know what you won't be? Up at 3 AM cleaning malware. Again.&lt;/p&gt;

&lt;p&gt;The plugin is open source (WTFPL license—seriously), battle-tested on 100+ production WordPress installs, and aggressively maintained because I got tired of cleaning up hacked sites.&lt;/p&gt;

&lt;p&gt;Same paranoia. Better execution. Zero regrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/ngalatis/wp-fort-knox" rel="noopener noreferrer"&gt;ngalatis/wp-fort-knox&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH/SFTP access&lt;/li&gt;
&lt;li&gt;WP-CLI installed and working&lt;/li&gt;
&lt;li&gt;A healthy distrust of humanity&lt;/li&gt;
&lt;li&gt;The ability to ignore client complaints about "restrictive security"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it. Lock it down. Sleep peacefully.&lt;/p&gt;

&lt;p&gt;Because paranoia is just good planning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you dealt with a similar situation? What's your most creative solution to the "clients with admin credentials" problem? Drop a comment below. Bonus points if it involves running all WordPress admin through a Discord bot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
    </item>
  </channel>
</rss>
