# WordPress security basics without a security plugin

> Ten habits and a dozen lines in wp-config.php protect a WordPress site better than most security plugins: updates, 2FA, roles, permissions, XML-RPC, backups, HTTPS, and what a firewall adds on top.

Updated 2026-09-25 · WordPress & WooCommerce · HTML version: https://getreport.app/guides/wordpress-security-basics-without-a-plugin

WordPress sites get broken into through old plugins, weak passwords and forgotten copies, not through a missing security plugin. A plugin can help with two things (rate-limiting logins and a firewall in front of PHP), but it cannot update your themes, choose your passwords or restore a backup. This guide is the list of things that actually decide whether a site survives a year: ten habits, most of them a one-time change, plus the `wp-config.php` constants that lock the setup in. Plan an afternoon for the first pass; after that it is a monthly routine.

## Quick answer

| # | What | Where |
| --- | --- | --- |
| 1 | Update core, plugins, themes and PHP monthly; auto-update the boring ones | Dashboard → Updates, host panel |
| 2 | Long unique passwords plus two-factor login for every administrator | Users → Profile, the Two Factor plugin |
| 3 | Fewest administrators possible; Editor for everyone who writes | Users → All Users |
| 4 | No user called "admin"; author archives hidden or renamed | Users, SEO plugin settings |
| 5 | Login attempts limited at the host, CDN or a small plugin | Host panel, Cloudflare, or a plugin |
| 6 | File editing in the admin switched off | `DISALLOW_FILE_EDIT` in wp-config.php |
| 7 | XML-RPC off unless Jetpack or an app needs it | A filter or a server rule |
| 8 | File permissions 755 / 644, wp-config.php 600 | SFTP or SSH |
| 9 | Backups that you have restored once, stored off the server | Host snapshots or a backup plugin |
| 10 | Staging copies password-protected; HTTPS and security headers everywhere | Host panel, server config |

Run the [WordPress health check](https://getreport.app/tools/wordpress-checker) before and after: it catches the public "admin" user, an open staging copy, exposed `.env` or `.git` files, missing HTTPS and the core version.

## Why the basics matter more than a plugin

Attacks on WordPress are automated and lazy. A bot scans for a version number or a plugin path, matches it to a published hole, and runs the exploit. Another bot tries `admin` with the ten thousand most common passwords against `wp-login.php` and `xmlrpc.php`. A third looks for `staging.` subdomains and `.env` files. None of these needs to be clever, because enough sites leave one of the doors open.

Each of the ten items below closes a door. Updates remove the holes the first bot looks for. Unique passwords and two-factor login stop the second. Locked staging copies and blocked dotfiles stop the third. A security plugin sits in front of all of this as a firewall and a login limiter, which is useful, but a firewall in front of an unpatched plugin with an `admin` / `Summer2024!` login is a fence around an open gate.

The order matters too. Sites that are up to date, on HTTPS, with 2FA and a tested backup, recover from the rare successful attack in an hour. Sites without a backup do not recover; they get rebuilt.

## How getReport checks it

> **Free tool:** [WordPress health check](https://getreport.app/tools/wordpress-checker): Is your WordPress site indexed, up to date and free of the classic launch mistakes? Checks "Discourage search engines", default content and tagline, plain permalinks, attachment pages, the exposed admin user, a public staging copy and the core version, plus security basics.

When the fetched page reveals WordPress, the report adds the WordPress Doctor panel. Detection is passive: `/wp-content/` paths, the generator tag, the `wp-json` link. Then a fixed set of public addresses is requested, at most eight for the rookie scan: `/hello-world/`, `/sample-page/`, `/?author=1`, the `staging.`, `dev.` and `test.` subdomains and two attachment sitemap URLs. The security module separately asks for `/.env` and `/.git/HEAD`. Nothing logs in, nothing guesses usernames or plugin slugs, and usernames other than the defaults are never shown.

![The WordPress Doctor panel on a freshly installed site: the detection line with version and theme, the rookie mistakes list with red dots on the exposed admin user and the public staging copy, and the plugin table below it](https://getreport.app/guides/img/wordpress-security-basics-without-a-plugin/doctor.webp "Each red item in the list links to its finding further down the report, with the fix.")

Four findings cover the items in this guide that can be seen from outside:

> **Check: No default admin author exposed.** /?author=1 redirects to /author/admin/, which confirms a login name called "admin" exists. Brute-force scripts try that name first.
>
> 1. Create a new administrator with a unique username, log in as it, delete "admin" and attribute its content to the new user.
> 2. Optionally disable author archives or the ?author= redirect in your security plugin.

> **Check: No public staging copy found.** A staging or dev copy that Google can index competes with the live site for its own content and often runs older, unpatched code. Anyone can find it.
>
> 1. Put the staging site behind HTTP authentication or an IP allow-list, or delete it when the launch is done.
> 2. At minimum switch on "Discourage search engines" there and add a noindex X-Robots-Tag header.

> **Check: No sensitive files are readable.** /.env holds database passwords and API keys; /.git exposes your source code and every secret ever committed. Both are the first things an automated scanner asks for.
>
> 1. Block dotfiles at the web server (nginx: location ~ /\. { deny all; }; Apache: RedirectMatch 404 /\..*$).
> 2. Move .env outside the web root and rotate every credential in it; assume the file has already been copied.

> **Check: The site is served over HTTPS.** Browsers label HTTP pages "Not secure" and Google uses HTTPS as a small ranking signal. Anyone on the network can read or alter what visitors see.
>
> 1. Get a certificate (Let's Encrypt is free) and enable HTTPS on your host.
> 2. Redirect every http:// URL to https:// with a 301.

Everything else on the list (2FA, roles, file permissions, backups, `DISALLOW_FILE_EDIT`) is invisible to any outside scanner, which is exactly why this guide exists: the report tells you about the visible half, and you check the other half yourself.

## Step by step

### 1. Update everything, monthly

Core, plugins, themes and PHP. Dashboard → Updates lists the first three; the host's control panel sets the PHP version. Minor core releases update themselves since WordPress 3.7; switch on auto-updates for plugins that release often and have never broken the site (Plugins → "Enable auto-updates" per row). Delete deactivated plugins and unused themes: their files stay on disk and stay exploitable. Keep one default theme (Twenty Twenty-Five) as a fallback and remove the rest. The full routine, including what to do about abandoned plugins, is in [Outdated CMS versions](https://getreport.app/guides/outdated-cms-versions-reading-the-generator-tag).

### 2. Passwords and two-factor login

Every account with Administrator or Editor rights gets a generated password of 20+ characters stored in a password manager, and it is not reused from any other service. WordPress's own generator (Users → Profile → Set New Password) is fine.

WordPress core has no built-in two-factor login. It is the one place where a plugin is the only option: the Two Factor plugin on wordpress.org is maintained by core contributors and supports authenticator apps and backup codes; most large security plugins bundle a 2FA feature too. Passkey support for core is being worked on; until it lands, use the plugin. Turn 2FA on for every administrator today and for editors this month.

If a client or an agency needs API access (a mobile app, a deployment script), give them an application password (Users → Profile → Application Passwords) instead of the real one; it can be revoked without changing the login.

### 3. Least privilege

WordPress has five built-in roles. Only Administrator can install plugins, edit themes and change settings. Everyone who writes content is an Editor (all posts) or an Author (their own posts). Agencies get an Administrator account that you delete when the project ends. Go through Users → All Users once a quarter: remove people who left, downgrade anyone who does not need Administrator, and delete accounts that have never logged in.

### 4. No "admin", and author archives under control

A login named `admin` gives a brute-force script half of what it needs. `/?author=1` redirecting to `/author/admin/` tells the script the name is in use, which is what the report checks. Create a new administrator with a unique username, log in as it, delete `admin` and attribute its posts to the new account. If the site has one author, hide author archives in Yoast or Rank Math, or redirect them. The full walkthrough is [The "admin" user: renaming it and hiding author archives](https://getreport.app/guides/wordpress-admin-user-and-author-archives).

### 5. Limit login attempts, honestly

Core does not limit login attempts. Three places do, and any one is enough:

- **The host.** Most managed WordPress hosts rate-limit `wp-login.php` and block known bad networks; check the hosting panel's security section.
- **Cloudflare or another CDN.** A rate-limiting rule on `wp-login.php` and `xmlrpc.php` (for example, more than 5 POST requests per minute from one address → block for 10 minutes) stops brute-force traffic before it reaches PHP.
- **A small plugin.** Limit Login Attempts Reloaded and similar plugins do one thing and do it at the PHP level. This is the one exception to "without a plugin" that is worth making when neither of the first two is available.

### 6. Switch off file editing

The admin's Appearance → Theme File Editor and Plugins → Plugin File Editor let any administrator, or anyone who steals an administrator session, write PHP that runs on your server. Nobody edits production code in a browser textarea. One constant turns both editors off; a second one also blocks installing and updating plugins through the admin, which is right for sites deployed from Git and wrong for sites that update from the dashboard.

### 7. XML-RPC: off unless you need it

`xmlrpc.php` is the old remote-publishing API. Jetpack uses it, and so do some older mobile and desktop publishing apps; nothing else on a typical site does. It accepts login attempts and can batch hundreds of them into one request, so brute-force bots prefer it. If you do not use Jetpack, disable it with a filter (see the code block below) or block it at the server:

```nginx
# nginx, inside the server block
location = /xmlrpc.php {
    return 403;
}
```

```apache
# .htaccess
<Files "xmlrpc.php">
    Require all denied
</Files>
```

### 8. File permissions

On a normal single-account host: folders `755`, files `644`, and `wp-config.php` readable only by the account that runs PHP. Over SSH:

```bash
cd /var/www/example   # the folder that holds wp-config.php
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod 600 wp-config.php
```

If the site stops working after `600` on `wp-config.php`, the web server runs as a different user from the file owner; use `640` with the web server's group, or `440`. Never `777` anywhere, even when a plugin's error message asks for it. Keep `.env`, backups and `.git` outside the web root; the [directory listings and exposed files guide](https://getreport.app/guides/directory-listings-and-exposed-files) covers the server rules.

### 9. Backups that restore

A backup that has never been restored is a hope. Use the host's daily snapshots if they exist and keep a second copy elsewhere (UpdraftPlus to Google Drive or S3, or a `wp db export` plus an rsync of `wp-content` on a schedule). Once, on a staging site, restore the latest backup and click through the result. Note how long it took; that is your recovery time. Backups on the same server as the site do not count: a compromised or failed server takes them with it.

### 10. Lock staging, force HTTPS, add headers

**Staging.** `staging.yoursite.com` usually runs older code with debug output on and the same database dump as production, including customer data. Put it behind HTTP authentication at the host or server, or delete it after launch. The report's staging finding tells you when it is public; the fix is in [WordPress rookie mistakes](https://getreport.app/guides/wordpress-rookie-mistakes).

**HTTPS.** Settings → General must have `https://` in both URLs, `http://` must redirect to `https://` in one hop, and the admin must be HTTPS-only. Site Health offers a one-click switch on WordPress 5.7 and later when the certificate is already in place.

**Headers.** HSTS, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` and `Permissions-Policy` belong at the web server or CDN, not in a plugin, so cached and static responses get them too. Copy-paste configs are in [Security headers from zero to A](https://getreport.app/guides/security-headers-from-zero).

### The wp-config.php constants

Everything from this guide that lives in `wp-config.php`, in one block. Add it above the line `/* That's all, stop editing! */`:

```php
<?php
// wp-config.php – security constants (place above "That's all, stop editing!")

// No theme or plugin editor in the admin.
define('DISALLOW_FILE_EDIT', true);

// Also no plugin/theme installs or updates from the admin.
// Only for sites deployed from Git; otherwise leave it out.
// define('DISALLOW_FILE_MODS', true);

// Admin and login always over HTTPS.
define('FORCE_SSL_ADMIN', true);

// Core: apply minor (security) releases automatically. 'minor' is the default;
// true also applies major releases, false switches auto-updates off.
define('WP_AUTO_UPDATE_CORE', 'minor');

// No debug output on production; if you need a log, keep it outside the web root.
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);

// Fresh salts: paste eight new lines from https://api.wordpress.org/secret-key/1.1/salt/
// whenever you suspect a session or password leak. Existing sessions are logged out.
```

And the XML-RPC filter, in a must-use plugin so it survives theme changes:

```php
<?php
// wp-content/mu-plugins/disable-xmlrpc.php
add_filter('xmlrpc_enabled', '__return_false');
```

## What a plugin or a WAF adds

Once the ten basics are in place, a security plugin or a web application firewall adds three things: a firewall that drops known attack patterns before they reach PHP (a CDN-level WAF such as Cloudflare's does this better, because the request never hits your server), a file-integrity scan that emails you when a core or plugin file changes, and a login limiter if the host does not have one. Those are worth having on a shop or a site with many editors. They do not replace anything above, they add load on every request, and two of them on one site conflict. Pick one, or pick the CDN.

## Verify

- The [WordPress health check](https://getreport.app/tools/wordpress-checker) shows no exposed admin user, no public staging copy, no readable sensitive files, and the site served over HTTPS with the current core version.
- Appearance → Theme File Editor is gone from the admin menu.
- `curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yoursite.com/xmlrpc.php` prints `403` when blocked at the server. With the filter, the endpoint still answers 200, but every method that needs a login returns the fault "XML-RPC services are disabled on this site".
- Every administrator's profile shows two-factor login enabled.
- A restore of yesterday's backup on staging worked, and you know how long it took.
- The [security headers checker](https://getreport.app/tools/security-headers) shows HSTS and the four safe headers passing.

## Common mistakes

- **Installing a second security plugin for extra safety.** Symptom: locked out of the admin, or both firewalls blocking your own editors. Fix: one plugin or the CDN's WAF, not both.
- **`777` permissions to make an upload error go away.** Symptom: any compromised plugin can now write anywhere. Fix: `755`/`644` and the correct owner; ask the host about the PHP user.
- **Disabling XML-RPC with Jetpack active.** Symptom: Jetpack disconnects and its stats and backups stop. Fix: leave XML-RPC on and rate-limit it at the CDN instead.
- **Backups stored in `wp-content/uploads/`.** Symptom: a listing or a guessed file name exposes the whole database. Fix: store backups off-server and delete local archives.
- **Hiding the login URL and calling it 2FA.** Symptom: a renamed `wp-login.php` is found through `/wp-admin/` redirects in a day. Fix: real two-factor login; the URL can stay.
- **One shared administrator account for the agency.** Symptom: nobody knows who changed what, and the password lives in a chat thread. Fix: one account per person, deleted when they leave.
