File Permission Audit SOP
Run on any server you inherit or after a suspected breach.
— Step 1: Directories to 755, files to 644. Run
— Step 2: Lock
— Step 3: Verify ownership. Web files owned by the app user, never
— Step 4: Hunt for 777.
— Step 5: Confirm
Run this every time.
Run on any server you inherit or after a suspected breach.
— Step 1: Directories to 755, files to 644. Run
find . -type d -exec chmod 755 {} \; then find . -type f -exec chmod 644 {} \;.— Step 2: Lock
wp-config.php to 640 (or 600 if PHP runs as owner). Never 644 on shared hosting.— Step 3: Verify ownership. Web files owned by the app user, never
root and never the web server user as owner.— Step 4: Hunt for 777.
find . -perm -o+w -type f — anything world-writable is a backdoor waiting to happen.— Step 5: Confirm
uploads/ can't execute PHP. Add a deny rule for .php in that directory.Run this every time.
2FA Enforcement Rollout SOP
Use when mandating two-factor across a team, not just suggesting it.
— Step 1: Pick TOTP or hardware keys. Disable SMS — SIM-swap defeats it.
— Step 2: Set a grace window. 7 days from first login to enroll, enforced by plugin policy.
— Step 3: Force enrollment by role. Require it for Administrator and Editor first; expand to all roles after.
— Step 4: Generate and store recovery codes offline. Test one to confirm it consumes correctly.
— Step 5: Block the grace bypass. After day 7, unenrolled accounts get login-locked, not warned.
— Step 6: Audit monthly. Pull a list of accounts without an active 2FA secret and remediate.
Run this every time.
Use when mandating two-factor across a team, not just suggesting it.
— Step 1: Pick TOTP or hardware keys. Disable SMS — SIM-swap defeats it.
— Step 2: Set a grace window. 7 days from first login to enroll, enforced by plugin policy.
— Step 3: Force enrollment by role. Require it for Administrator and Editor first; expand to all roles after.
— Step 4: Generate and store recovery codes offline. Test one to confirm it consumes correctly.
— Step 5: Block the grace bypass. After day 7, unenrolled accounts get login-locked, not warned.
— Step 6: Audit monthly. Pull a list of accounts without an active 2FA secret and remediate.
Run this every time.
Least-Privilege Role Cleanup SOP
Run quarterly on every WordPress install.
— Step 1: Count your Administrators. More than 2-3 on a normal site is a finding, not a feature.
— Step 2: Demote content people. Writers get Author, reviewers get Editor. Nobody publishes copy from an Admin account.
— Step 3: Remove
— Step 4: Set
— Step 5: Delete dormant accounts. No login in 90 days = disable, then remove after review.
— Step 6: Reassign orphaned content before deletion so nothing breaks.
Run this every time.
Run quarterly on every WordPress install.
— Step 1: Count your Administrators. More than 2-3 on a normal site is a finding, not a feature.
— Step 2: Demote content people. Writers get Author, reviewers get Editor. Nobody publishes copy from an Admin account.
— Step 3: Remove
edit_files, install_plugins, and update_core from any custom role that doesn't deploy.— Step 4: Set
DISALLOW_FILE_EDIT to true in wp-config.php — kill the in-dashboard code editor entirely.— Step 5: Delete dormant accounts. No login in 90 days = disable, then remove after review.
— Step 6: Reassign orphaned content before deletion so nothing breaks.
Run this every time.
Neighbor spotlight: @HandshakePapers. They go deep on SSL / HTTPS — the kind of channel you actually keep notifications on for.
Login Lockout Policy SOP
Configure on every site that has a wp-admin login.
— Step 1: Set the threshold. 5 failed attempts, then a 20-minute lockout. Escalate repeat offenders to 24 hours.
— Step 2: Lock by username AND IP. Attackers rotate IPs, so per-account counting matters more.
— Step 3: Mask the error. Never reveal whether the username or the password was wrong.
— Step 4: Rename or protect
— Step 5: Whitelist your office and VPN egress IPs so you don't lock yourself out.
— Step 6: Alert on 50+ lockouts in an hour — that's an active campaign, not noise.
Run this every time.
Configure on every site that has a wp-admin login.
— Step 1: Set the threshold. 5 failed attempts, then a 20-minute lockout. Escalate repeat offenders to 24 hours.
— Step 2: Lock by username AND IP. Attackers rotate IPs, so per-account counting matters more.
— Step 3: Mask the error. Never reveal whether the username or the password was wrong.
— Step 4: Rename or protect
wp-login.php at the server level to cut bot volume before it hits PHP.— Step 5: Whitelist your office and VPN egress IPs so you don't lock yourself out.
— Step 6: Alert on 50+ lockouts in an hour — that's an active campaign, not noise.
Run this every time.
WAF Rule-Tuning SOP
Do this after deploying any web application firewall.
— Step 1: Start in log/detection mode for 7 days. Never go straight to block on a live store.
— Step 2: Enable the OWASP Core Rule Set at paranoia level 1, then raise it only if false positives stay low.
— Step 3: Review the anomaly-score log. Tune individual rule exclusions, don't disable whole categories.
— Step 4: Add virtual patches for known plugin CVEs you can't update immediately.
— Step 5: Rate-limit
— Step 6: Switch to blocking mode, then re-run a baseline scan to confirm legit traffic still passes.
Run this every time.
Do this after deploying any web application firewall.
— Step 1: Start in log/detection mode for 7 days. Never go straight to block on a live store.
— Step 2: Enable the OWASP Core Rule Set at paranoia level 1, then raise it only if false positives stay low.
— Step 3: Review the anomaly-score log. Tune individual rule exclusions, don't disable whole categories.
— Step 4: Add virtual patches for known plugin CVEs you can't update immediately.
— Step 5: Rate-limit
/wp-admin, /wp-login.php, and the REST API /users endpoint specifically.— Step 6: Switch to blocking mode, then re-run a baseline scan to confirm legit traffic still passes.
Run this every time.
REST API Lockdown SOP
Run on every WordPress site — the API is open by default.
— Step 1: Test exposure. Hit
— Step 2: Require authentication on the
— Step 3: Don't blanket-disable the API — Gutenberg and many plugins need it. Scope restrictions to sensitive routes.
— Step 4: Remove author enumeration from the front end too. Block
— Step 5: Verify usernames differ from display names. Same value = handed attackers half the credential.
Run this every time.
Run on every WordPress site — the API is open by default.
— Step 1: Test exposure. Hit
/wp-json/wp/v2/users — if it returns names and slugs, you're leaking your login usernames.— Step 2: Require authentication on the
users endpoint via a rest_authentication_errors filter for unauthenticated requests.— Step 3: Don't blanket-disable the API — Gutenberg and many plugins need it. Scope restrictions to sensitive routes.
— Step 4: Remove author enumeration from the front end too. Block
?author=1 redirects at the server.— Step 5: Verify usernames differ from display names. Same value = handed attackers half the credential.
Run this every time.
Database & Secrets Hardening SOP
Apply during every new-site provisioning.
— Step 1: Set a custom table prefix at install. Not
— Step 2: Give the WordPress DB user only the rights it needs: SELECT, INSERT, UPDATE, DELETE. Drop DROP, GRANT, and FILE in production.
— Step 3: Rotate all eight
— Step 4: Move
— Step 5: Bind MySQL to localhost. Confirm port 3306 isn't reachable from the public internet.
— Step 6: Verify backups encrypt the dump, not just the transport.
Run this every time.
Apply during every new-site provisioning.
— Step 1: Set a custom table prefix at install. Not
wp_, and not a guessable word — use a random short string.— Step 2: Give the WordPress DB user only the rights it needs: SELECT, INSERT, UPDATE, DELETE. Drop DROP, GRANT, and FILE in production.
— Step 3: Rotate all eight
wp-config.php salts and keys. Pull fresh ones from the official secret-key API.— Step 4: Move
wp-config.php one directory above webroot if your stack allows it.— Step 5: Bind MySQL to localhost. Confirm port 3306 isn't reachable from the public internet.
— Step 6: Verify backups encrypt the dump, not just the transport.
Run this every time.
Uploads Directory Hardening SOP
Do this on every site that accepts file uploads.
— Step 1: Block PHP execution in
— Step 2: For Nginx:
— Step 3: Validate uploads by content, not extension. Check magic bytes, not just
— Step 4: Strip EXIF and rename files to random tokens on upload to kill path-guessing.
— Step 5: Disallow double extensions like
— Step 6: Verify. Drop a test .php in uploads, request it, confirm it downloads as text — never executes.
Run this every time.
Do this on every site that accepts file uploads.
— Step 1: Block PHP execution in
wp-content/uploads/. The classic backdoor is a .php disguised as an image landing here.— Step 2: For Nginx:
location ~* /uploads/.*\.php$ { deny all; }. For Apache: a .htaccess denying .php, .phtml, .phar.— Step 3: Validate uploads by content, not extension. Check magic bytes, not just
.jpg.— Step 4: Strip EXIF and rename files to random tokens on upload to kill path-guessing.
— Step 5: Disallow double extensions like
shell.php.jpg.— Step 6: Verify. Drop a test .php in uploads, request it, confirm it downloads as text — never executes.
Run this every time.
WP-Cron Hardening SOP
Run on every production WordPress site for reliability and DoS resistance.
— Step 1: Disable pseudo-cron. Set
— Step 2: Schedule a real system cron hitting
— Step 3: Block public access to
— Step 4: Audit scheduled events with WP-CLI
— Step 5: Set
Run this every time.
Run on every production WordPress site for reliability and DoS resistance.
— Step 1: Disable pseudo-cron. Set
DISABLE_WP_CRON to true in wp-config.php — every page load triggering cron is a load and abuse vector.— Step 2: Schedule a real system cron hitting
wp-cron.php at a fixed interval, every 5-15 minutes.— Step 3: Block public access to
wp-cron.php at the server, allowing only localhost.— Step 4: Audit scheduled events with WP-CLI
wp cron event list — orphaned plugin hooks pile up here.— Step 5: Set
ALTERNATE_WP_CRON only as a fallback, never as the default.Run this every time.
Server Access Hardening SOP
Run before putting any web server into production.
— Step 1: Disable password SSH. Set
— Step 2: Disable root login.
— Step 3: Move SSH off port 22 to cut log noise — defense in depth, not a real control.
— Step 4: Restrict by source. Firewall SSH to your VPN or a bastion host, never 0.0.0.0/0.
— Step 5: Use per-person keys, not one shared key. Revoking access means removing one line.
— Step 6: Audit
Run this every time.
Run before putting any web server into production.
— Step 1: Disable password SSH. Set
PasswordAuthentication no and use keys only.— Step 2: Disable root login.
PermitRootLogin no, then sudo from a named user.— Step 3: Move SSH off port 22 to cut log noise — defense in depth, not a real control.
— Step 4: Restrict by source. Firewall SSH to your VPN or a bastion host, never 0.0.0.0/0.
— Step 5: Use per-person keys, not one shared key. Revoking access means removing one line.
— Step 6: Audit
authorized_keys quarterly. Remove keys for people who left.Run this every time.
Plugin Vetting SOP
Run before installing any new plugin or theme.
— Step 1: Check last-updated date. Untouched in 12+ months = abandoned, treat as a liability.
— Step 2: Cross-reference the slug against a CVE database. Known unpatched vulnerability is an automatic no.
— Step 3: Verify install count vs. review quality. High installs hide that it's still a single-maintainer project.
— Step 4: Read what capabilities it requests. A contact form asking for
— Step 5: Never install nulled or pirated plugins — they're the single most common malware vector.
— Step 6: Stage it first. Install on a clone, diff the filesystem, then promote to production.
Run this every time.
Run before installing any new plugin or theme.
— Step 1: Check last-updated date. Untouched in 12+ months = abandoned, treat as a liability.
— Step 2: Cross-reference the slug against a CVE database. Known unpatched vulnerability is an automatic no.
— Step 3: Verify install count vs. review quality. High installs hide that it's still a single-maintainer project.
— Step 4: Read what capabilities it requests. A contact form asking for
install_plugins is a red flag.— Step 5: Never install nulled or pirated plugins — they're the single most common malware vector.
— Step 6: Stage it first. Install on a clone, diff the filesystem, then promote to production.
Run this every time.