WordPress .htaccess Broke the Site: Restore a Safe Default Properly

Was this helpful?

You changed .htaccess to “just add a redirect” or “tighten security,” refreshed the page, and now production is returning 500s, 403s, or a redirect loop that burns CPU like a space heater. The phone starts ringing. The marketing team says it’s “just a blog.” Meanwhile, it’s also your checkout, your docs, your SEO, and your weekend.

This is the sane way out: restore a known-good baseline, prove what broke, and put guardrails in place so the next edit doesn’t become an incident. We’ll do it like an SRE who’s had to explain to leadership why “a small config tweak” took down a revenue path.

What .htaccess really does (and why it breaks so dramatically)

.htaccess is per-directory configuration for Apache HTTP Server. If your Apache config allows it (AllowOverride), Apache reads .htaccess on every request to that directory and its subdirectories. That’s the point: you can make changes without touching the global config or reloading Apache. It’s also the trap: you can break routing, permissions, and security instantly, and Apache will faithfully apply the broken logic on every request.

WordPress leans on mod_rewrite to make “pretty permalinks” work. The default WordPress rewrite block is small and boring. Which is exactly what you want in production. When a plugin, a security “hardening” guide, or a well-meaning coworker starts stacking directives—redirects, deny rules, PHP flags, caching hints—you can create interactions that are not obvious until they go live: rewrite loops, request amplification, blocked assets, blocked admin, and sudden 500s.

One more thing: .htaccess is evaluated in the context of a directory, not the whole site. That means a rule that “looks right” at the root can behave differently in /wp-admin/ or /wp-content/. The path matching rules are subtle. Production incidents are not subtle.

Short joke #1: Editing .htaccess live is like doing surgery with a butter knife—technically possible, but you’ll have a lot to explain afterward.

Interesting facts and context (so the behavior makes sense)

  • .htaccess exists because of shared hosting. In the early days of cheap hosting, users needed limited control without root access. Per-directory overrides were the compromise.
  • Apache reads .htaccess at request time. That convenience costs performance; the server must check for the file (and parent directories) repeatedly unless configured otherwise.
  • WordPress permalinks became “the default expectation.” As blogs evolved into CMSs, human-readable URLs stopped being optional and became table stakes.
  • mod_rewrite’s rule ordering is a common footgun. The first matching rule can short-circuit the rest, and a tiny flag change (L, R, QSA) can flip behavior completely.
  • 403 vs 404 is often policy, not existence. A 403 from Apache can be caused by filesystem permissions, access rules, or security modules—not missing content.
  • Many “security snippets” are outdated. Blog posts copied for years still recommend directives that conflict with modern WordPress behavior or PHP-FPM setups.
  • Nginx doesn’t use .htaccess. When teams migrate from Apache to Nginx, they often keep editing .htaccess and wonder why nothing changes.
  • AllowOverride is a control plane decision. Enabling it everywhere is operationally convenient and security-hostile; disabling it everywhere is safe and annoying. Most real systems choose a middle ground.

Fast diagnosis playbook

This is the order that finds the bottleneck fast, not the order that feels polite.

1) Identify the failure class: 500, 403, 404, redirect loop, or blank page

  • 500: Apache couldn’t process the request. Think invalid directive, missing module, permission issue under security module, or PHP handler failures exposed by rewrite.
  • 403: Access denied. Think filesystem permissions, Require/Deny rules, or WAF/security module.
  • 404: Routing. Think rewrite not firing, wrong RewriteBase, wrong DocumentRoot, or WordPress not receiving the request.
  • 301/302 loop: Rewrite or redirect logic conflict. Often scheme/host canonicalization + plugin + proxy headers.
  • Blank page: Could be PHP fatal with display errors off; can still be triggered by rewrite routing to PHP.

2) Look at the web server error log first, not WordPress

If .htaccess is broken, Apache will often tell you exactly which directive and why. If you start inside WordPress, you’re debugging the wrong layer.

3) Confirm that Apache is even honoring .htaccess

If AllowOverride None is set for that path, your changes won’t apply, and you might be chasing ghosts. Conversely, if overrides are allowed, a single bad line can take down the vhost immediately.

4) Roll back to a known-good baseline, then reintroduce changes

In production, you want a quick restore to service. Root cause can follow. But do it cleanly: backup the current file, restore the default, validate logs, then add changes one at a time.

Restore a safe WordPress default .htaccess (properly)

There are two “safe defaults” worth talking about:

  1. WordPress’s default rewrite block (the minimum required for permalinks on Apache with mod_rewrite).
  2. A safe operational baseline: default rewrite plus a small set of non-controversial safety rules that won’t break admin, REST API, or uploads.

Baseline #1: WordPress default rewrite block

This is what WordPress writes when you “Save Permalinks” in the admin. It’s intentionally minimal.

cr0x@server:~$ cat /var/www/example.com/public_html/.htaccess
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Yes, that HTTP_AUTHORIZATION line matters for some setups and plugins. If you delete it, you can break authenticated requests through certain proxies or PHP handlers. Keep it unless you have a reason not to.

Baseline #2: Default rewrite plus safe operational hardening

Hardening needs restraint. If you can’t explain exactly which requests it blocks and why, it doesn’t belong in production. A conservative set includes: preventing directory listing, blocking access to sensitive files, and adding a couple of headers that don’t break WordPress.

Keep rewrite behavior identical, and add only rules that are low-risk. Avoid fancy regex blocks on query strings until you’ve tested them with your plugins and admin workflows.

cr0x@server:~$ cat /var/www/example.com/public_html/.htaccess
# Basic hygiene
Options -Indexes

<FilesMatch "^(\.env|composer\.(json|lock)|wp-config\.php|readme\.html|license\.txt)$">
  Require all denied
</FilesMatch>

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

# Safe headers (avoid breaking admin)
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

Notice what’s missing: aggressive caching directives, blocking xmlrpc.php blindly, complicated hotlink rules, and “deny all in wp-admin except my IP.” Those are not defaults. They’re project decisions.

Practical tasks: commands, output meaning, and decisions

You want repeatable tasks that work under pressure. Here are the ones I actually run. Each includes (a) a command, (b) what the output means, and (c) the decision you make from it.

Task 1: Confirm the web server and its active config

cr0x@server:~$ ps -eo pid,comm,args | egrep 'apache2|httpd|nginx' | head
  1123 apache2 /usr/sbin/apache2 -k start
  1450 apache2 /usr/sbin/apache2 -k start

Meaning: Apache is running (apache2). If you only see nginx, stop editing .htaccess; it’s not in the request path.

Decision: If Apache isn’t the serving layer, locate the real ingress (load balancer, reverse proxy, Nginx) and fix routing there.

Task 2: Capture the exact HTTP symptom from the server itself

cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/ | sed -n '1,12p'
HTTP/1.1 500 Internal Server Error
Date: Sat, 27 Dec 2025 12:10:13 GMT
Server: Apache/2.4.57 (Ubuntu)
Content-Type: text/html; charset=iso-8859-1

Meaning: It’s a real 500 at the web server, not a CDN edge issue.

Decision: Go directly to Apache error logs and config validation before touching WordPress.

Task 3: Tail Apache error logs during a request

cr0x@server:~$ sudo tail -n 50 /var/log/apache2/error.log
[Sat Dec 27 12:10:13.492112 2025] [core:alert] [pid 1123] [client 127.0.0.1:39912] /var/www/example.com/public_html/.htaccess: Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration

Meaning: mod_rewrite isn’t loaded, or Apache doesn’t know rewrite directives in this context.

Decision: Enable mod_rewrite (on Debian/Ubuntu) and reload Apache, or fix the container image/package set.

Task 4: Validate Apache module availability (mod_rewrite, mod_headers)

cr0x@server:~$ apache2ctl -M | egrep 'rewrite|headers'
 headers_module (shared)

Meaning: headers is present; rewrite is not. That matches the log error.

Decision: Enable rewrite, then re-test. Without it, WordPress permalinks won’t function as expected.

Task 5: Enable mod_rewrite and reload Apache (Debian/Ubuntu)

cr0x@server:~$ sudo a2enmod rewrite
Enabling module rewrite.
To activate the new configuration, you need to run:
  systemctl restart apache2

Meaning: The module is now enabled in Apache’s config.

Decision: Restart/reload Apache in a controlled way. If you’re in HA, drain one node first.

cr0x@server:~$ sudo systemctl reload apache2

Meaning: Reload succeeded (no output is common). If it fails, systemctl status apache2 will tell you.

Decision: Immediately rerun curl and tail logs to confirm the error class changes.

Task 6: Check if Apache is allowed to honor .htaccess (AllowOverride)

cr0x@server:~$ sudo apache2ctl -S 2>&1 | sed -n '1,20p'
VirtualHost configuration:
*:80                   example.com (/etc/apache2/sites-enabled/example.com.conf:1)
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"

Meaning: You have the vhost file path. Next, inspect the directory stanza for AllowOverride.

Decision: If AllowOverride None, then .htaccess rules are ignored; fix at vhost-level or change AllowOverride for this directory only.

cr0x@server:~$ sudo sed -n '1,200p' /etc/apache2/sites-enabled/example.com.conf
<VirtualHost *:80>
  ServerName example.com
  DocumentRoot /var/www/example.com/public_html

  <Directory /var/www/example.com/public_html>
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

Meaning: Overrides are allowed. So .htaccess is in play, for better or worse.

Decision: If the site is broken after an edit, rolling back .htaccess will have immediate impact.

Task 7: Make an on-box backup with timestamps before changing anything

cr0x@server:~$ cd /var/www/example.com/public_html
cr0x@server:~$ sudo cp -a .htaccess .htaccess.bak.$(date +%F-%H%M%S)
cr0x@server:~$ sudo ls -la .htaccess*
-rw-r--r-- 1 www-data www-data  821 Dec 27 12:11 .htaccess
-rw-r--r-- 1 www-data www-data  821 Dec 27 12:11 .htaccess.bak.2025-12-27-121102

Meaning: You now have an escape hatch. If you’re wrong (it happens), you can restore quickly.

Decision: Never edit without a backup. In incident response, “I’ll remember what it used to be” is a lie you tell yourself.

Task 8: Validate filesystem ownership and permissions (403s often start here)

cr0x@server:~$ namei -l /var/www/example.com/public_html/.htaccess
f: /var/www/example.com/public_html/.htaccess
drwxr-xr-x root     root     /
drwxr-xr-x root     root     var
drwxr-xr-x root     root     www
drwxr-x--- www-data www-data example.com
drwxr-x--- www-data www-data public_html
-rw-r----- www-data www-data .htaccess

Meaning: Apache (often www-data) can read the file if it runs as www-data. If any directory lacks execute permission for the Apache user, Apache can’t traverse and you’ll get 403/500 weirdness.

Decision: If permissions are too tight, fix directory execute bits first before blaming rewrite rules.

Task 9: Look for the classic redirect loop with headers

cr0x@server:~$ curl -sS -I http://example.com/ | sed -n '1,12p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/

Meaning: HTTP redirects to HTTPS. Fine.

Decision: Now test HTTPS and check for a second redirect back to HTTP, or to a different host.

cr0x@server:~$ curl -sS -I https://example.com/ | egrep 'HTTP/|Location'
HTTP/2 301
location: http://example.com/

Meaning: That’s a loop: HTTPS redirects back to HTTP. Usually caused by mixed proxy headers and rewrite conditions that mis-detect scheme.

Decision: Fix canonicalization in one place (LB or Apache), and ensure X-Forwarded-Proto is honored correctly. Don’t stack competing redirects in .htaccess and plugins.

Task 10: Quickly disable .htaccess without deleting it (safe test)

cr0x@server:~$ cd /var/www/example.com/public_html
cr0x@server:~$ sudo mv .htaccess .htaccess.disabled
cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/ | sed -n '1,8p'
HTTP/1.1 200 OK
Date: Sat, 27 Dec 2025 12:12:44 GMT
Server: Apache/2.4.57 (Ubuntu)

Meaning: Removing .htaccess from the request path restored service. That’s confirmation, not a solution.

Decision: Put back a minimal known-good .htaccess (WordPress default block), then re-enable the file.

Task 11: Restore WordPress default rewrite block safely

cr0x@server:~$ sudo tee /var/www/example.com/public_html/.htaccess >/dev/null <<'EOF'
# BEGIN WordPress

RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

# END WordPress
EOF
cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/ | sed -n '1,8p'
HTTP/1.1 200 OK
Date: Sat, 27 Dec 2025 12:13:21 GMT
Server: Apache/2.4.57 (Ubuntu)

Meaning: Root path is responding again. Now confirm permalinks and admin.

Decision: If this fixes it, you know the prior .htaccess changes were the trigger. Start reintroducing needed rules carefully.

Task 12: Verify permalinks routing works (nonexistent file should hit index.php)

cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/this-should-not-be-a-file | sed -n '1,10p'
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

Meaning: The request is being routed through WordPress rather than returning 404 from Apache. WordPress may still produce a 404 page, but that’s at the app layer.

Decision: If you get an Apache 404 instead, rewrite isn’t working. Re-check mod_rewrite, AllowOverride, and RewriteBase.

Task 13: Confirm WordPress sees the correct site URL and home URL (redirect loops often live here)

cr0x@server:~$ sudo -u www-data wp option get home --path=/var/www/example.com/public_html
https://example.com
cr0x@server:~$ sudo -u www-data wp option get siteurl --path=/var/www/example.com/public_html
https://example.com

Meaning: WordPress agrees on canonical scheme and host.

Decision: If these disagree (http vs https, www vs apex), fix them before adding redirects in .htaccess. Let one layer own canonicalization.

Task 14: Check that the PHP handler is healthy (500s can be blamed on .htaccess unfairly)

cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/wp-login.php | sed -n '1,12p'
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

Meaning: PHP execution is working at least for this entrypoint.

Decision: If this is 500 while static assets are 200, the issue is likely PHP-FPM, permissions, or application errors rather than rewrite itself.

Task 15: Validate Apache config health after changes (catch the “won’t start” reload)

cr0x@server:~$ sudo apache2ctl configtest
Syntax OK

Meaning: Apache’s global configuration parses. This does not fully validate every runtime .htaccess edge case, but it catches many disasters.

Decision: If it’s not OK, fix before reloading. A broken reload in a single-node setup is a self-inflicted outage.

Task 16: Find exactly which .htaccess directive is breaking requests

cr0x@server:~$ sudo grep -RIn --color=never "Invalid command\|RewriteCond\|RewriteRule\|Require\|Deny" /var/log/apache2/error.log | tail -n 5
/var/log/apache2/error.log:[Sat Dec 27 12:10:13.492112 2025] [core:alert] [pid 1123] [client 127.0.0.1:39912] /var/www/example.com/public_html/.htaccess: Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration

Meaning: The log points to the file and the offending directive.

Decision: Fix the module/config mismatch rather than deleting lines randomly.

Three corporate mini-stories from the trenches

Mini-story 1: The outage caused by a wrong assumption

The company was mid-rebrand. New domain, new TLS certs, new campaign pages. A developer added what looked like a harmless redirect in .htaccess to force everything to the new hostname. They tested it on their laptop. It worked.

In production, traffic came through a load balancer terminating TLS and sending plain HTTP to Apache. The redirect logic was based on %{HTTPS} and assumed that if Apache saw HTTP, the client was on HTTP. That assumption was false. The client was already on HTTPS; Apache just didn’t know it.

The redirect rule forced HTTPS, the load balancer sent HTTP to Apache, Apache forced HTTPS again, and the client bounced between endpoints until the browser gave up. Monitoring showed “healthy” instances because Apache was responding quickly—with redirects. From the outside, the site was unusable.

Fix was boring: configure Apache to trust X-Forwarded-Proto (or do canonicalization exclusively at the load balancer), then simplify .htaccess to only WordPress rewrite. The redirect moved to the edge where scheme is known. They also added a synthetic check that fails on excessive redirects, because “200 is not the same as usable.”

Mini-story 2: The optimization that backfired

A performance-minded engineer wanted to reduce PHP load. They added aggressive caching headers in .htaccess for everything under /wp-content/, plus a set of rewrite rules to serve precompressed assets. On paper: fewer requests, faster pages, happier users.

Then came the quiet failures. A plugin updated and changed file names but reused paths. The caching headers were set to long-lived immutable values. Browsers kept stale JavaScript. Some users couldn’t submit forms because the client-side validation code was old. Support tickets started: “checkout button doesn’t work.” The incident was not a clean outage; it was worse—partial breakage that dodged monitoring.

When they tried to “fix it fast,” they flushed CDN caches, but the browser caches stayed poisoned. They had to roll back headers, bump asset versions, and send targeted cache-busting guidance. The true cost wasn’t CPU; it was user trust and time.

Lesson learned: caching is a product change. If you don’t control asset versioning end-to-end, don’t set heroic cache lifetimes at the origin via .htaccess. Use the platform’s cache layer (CDN) with sane defaults and explicit invalidation strategy, or keep TTLs short.

Mini-story 3: The boring but correct practice that saved the day

At a different shop, the WordPress site sat behind an internal change management process. It wasn’t fancy. It was just consistent: every config edit, including .htaccess, went through a tiny repo and a deployment job that archived previous versions on the server.

One Friday afternoon, a contractor applied a “security hardening pack” that included blocking access to certain PHP files and adding restrictive rules under /wp-admin/. It immediately locked out admins—on the same day content updates were scheduled. Predictably, everyone blamed WordPress.

The on-call didn’t debate. They pulled the last known-good .htaccess artifact from the deploy logs, restored it, and confirmed recovery in minutes. No archaeology. No guessing. No heroic SSH spelunking in multiple instances.

Then, calmly, they built a test plan: verify admin login, REST API calls used by the editor, uploads, and cron. The contractor’s changes were reintroduced behind a staging environment, adjusted, and deployed with a rollback plan. It wasn’t glamorous. It worked.

Common mistakes: symptom → root cause → fix

This section exists because most .htaccess breakages rhyme. If you recognize the symptom, you can skip a lot of drama.

500 Internal Server Error immediately after editing .htaccess

  • Symptom: Every request returns 500; Apache error log points to .htaccess.
  • Root cause: Invalid directive (typo), module not enabled (commonly mod_rewrite), or directive not allowed in .htaccess context.
  • Fix: Check error log for the exact directive, enable the module (a2enmod rewrite), or move the directive into vhost config if not permitted.

403 Forbidden on everything, including the homepage

  • Symptom: 403 for / and assets; sometimes only after adding “deny” rules.
  • Root cause: Over-broad Require all denied, old-style Deny from all misapplied, or filesystem traverse permissions broken.
  • Fix: Remove/limit deny blocks; verify directory execute permissions with namei -l; ensure vhost has Require all granted for the DocumentRoot.

Pretty permalinks return Apache 404, but /index.php works

  • Symptom: /about/ fails with 404 at web server; /index.php?p=123 works.
  • Root cause: Rewrite not running: mod_rewrite disabled, AllowOverride not permitting rewrite, or wrong RewriteBase because WordPress is in a subdirectory.
  • Fix: Enable rewrite; set AllowOverride All or at least AllowOverride FileInfo; set RewriteBase /subdir/ when WordPress isn’t at the vhost root.

Infinite redirect loop (browser says “too many redirects”)

  • Symptom: Loop between http/https or between www and non-www.
  • Root cause: Canonicalization configured in multiple layers (plugin + .htaccess + load balancer), or scheme detection wrong behind TLS termination.
  • Fix: Pick one layer to canonicalize. Ensure WordPress home/siteurl matches. If behind a proxy, configure trusted forwarded headers and base redirects on them.

Admin works, but uploads and images 403

  • Symptom: Site loads, but media library shows broken images; direct access to /wp-content/uploads/... returns 403.
  • Root cause: A deny rule intended for PHP files or dotfiles matches too broadly, or filesystem permissions on uploads are wrong after migration.
  • Fix: Narrow the FilesMatch to specific sensitive files; verify ownership and permissions on wp-content/uploads.

Site is “up” but slow after adding .htaccess security/caching rules

  • Symptom: Increased TTFB; CPU spikes; logs show many internal rewrites.
  • Root cause: Rewrite rules causing excessive filesystem checks, or redirect chains; also possible that .htaccess file now has expensive regex evaluated on every request.
  • Fix: Simplify rules; move heavy logic into vhost config; prefer explicit location blocks at reverse proxy; measure with access logs and response times.

Hardening without breaking: headers, access controls, and limits

Hardening is not copy-paste. Hardening is threat modeling plus compatibility testing. WordPress has an admin UI, REST API, cron endpoint, plugin update flows, file uploads, and sometimes a caching plugin that expects certain headers. If you lock it down like a static site, it will behave like a locked door: closed.

Rules that are usually safe

  • Disable directory listing with Options -Indexes. This prevents accidental browsing of directories without an index file.
  • Block access to explicit sensitive files like wp-config.php and .env. Use explicit matches, not broad wildcards.
  • Add non-invasive security headers (X-Content-Type-Options, Referrer-Policy). They’re low-risk and high-signal.

Rules that are risky and should be treated as changes, not defaults

  • Blocking xmlrpc.php blindly. Some sites still use it (mobile apps, integrations). If you block it, validate that nothing depends on it.
  • IP allowlisting /wp-admin/. Great for internal sites; a support nightmare for distributed teams and incident response. If you do it, include a break-glass path.
  • Complex user-agent or query-string blocks. They tend to block legitimate requests and create “works on my machine” debugging sessions.
  • Over-aggressive caching headers at origin. Useful when you control versioning; dangerous when you don’t.

Where to put logic: .htaccess vs vhost vs load balancer

If you control the server config, prefer vhost config over .htaccess. It’s faster (no per-request filesystem lookups) and more auditable. Use .htaccess as a compatibility layer, not your primary policy engine.

If you have a load balancer or CDN, canonical redirects (www/non-www, http/https) usually belong there. That layer sees the real client scheme and can enforce consistent behavior across origins.

One quote (paraphrased idea): Systems fail in surprising ways; resilience comes from expecting failure and designing for recovery. — paraphrased idea associated with John Allspaw’s operational reliability thinking.

Short joke #2: The fastest way to learn rewrite rules is to cause a redirect loop and watch your browser become a cardio enthusiast.

Checklists / step-by-step plan

Checklist A: Get the site serving again (15-minute incident mode)

  1. Confirm the symptom locally with curl to 127.0.0.1 to avoid CDN noise.
  2. Tail Apache error logs and reproduce the request once.
  3. Backup the current .htaccess with a timestamp.
  4. Temporarily disable .htaccess by renaming it. If service returns, you’ve isolated the cause.
  5. Restore the minimal WordPress default rewrite block and re-test homepage and one permalink.
  6. Confirm admin entrypoints like /wp-login.php and /wp-admin/.
  7. Stop there. Don’t keep “improving” during the incident. Stabilize first.

Checklist B: Diagnose the actual root cause (after service restoration)

  1. Diff the broken file against the default. Identify which block introduced the behavior.
  2. Check enabled modules (rewrite, headers) and server version compatibility.
  3. Review proxy/LB behavior: TLS termination, forwarded headers, canonical redirects.
  4. Test staging with the exact same vhost and proxy header setup. “Staging without the load balancer” is not staging; it’s arts and crafts.
  5. Add synthetic tests: one for redirect count, one for login page status, one for a permalink, one for a static asset.

Checklist C: Reintroduce necessary rules safely

  1. Only add one logical change at a time (one redirect block, one deny block, one header block).
  2. After each change, run three checks: homepage, a permalink, and wp-login.
  3. Watch logs while testing. Apache will often tell you what the browser won’t.
  4. Prefer vhost config for permanent rules if you control it; keep .htaccess minimal.
  5. Document why the rule exists in comments. Future you will forget, and future you will be on-call.

FAQ

1) Can I just delete .htaccess?

You can, and it’s a good isolation test. But if you rely on pretty permalinks, deleting it usually degrades routing to “plain” URLs or breaks page resolution. Use deletion/rename to confirm causality, then restore the minimal default.

2) Why did a single line in .htaccess take down the entire site?

Because Apache parses .htaccess during request handling. A syntax error or an unknown directive can cause Apache to reject the request (often with 500) before WordPress ever runs. It’s fast failure, not graceful degradation.

3) I’m on Nginx. Why doesn’t my .htaccess change do anything?

Nginx doesn’t read .htaccess. If you’re behind Nginx (or a managed host using Nginx), the equivalent rules need to be implemented in Nginx config, not in a file WordPress expects Apache to read.

4) What’s the safest default WordPress .htaccess content?

The WordPress rewrite block shown above. It’s the smallest set that makes permalinks work. Add only the extra rules you can justify and test.

5) Should I put security headers in .htaccess?

It’s acceptable if you can’t edit vhost config, but keep it minimal. Some headers (especially Content Security Policy) can break plugins, editors, and embedded content. Start with low-risk headers and expand only after testing.

6) Why am I getting a redirect loop after forcing HTTPS?

Most commonly: TLS terminates at a load balancer, Apache sees backend HTTP, and your redirect logic uses %{HTTPS} instead of trusted forwarded headers. Fix canonicalization at the edge, or teach Apache about the real scheme.

7) Can a plugin rewrite my .htaccess automatically?

Yes. Many caching and security plugins modify .htaccess. That’s convenient until it isn’t. If you allow it, treat it like code: track changes, keep backups, and understand what the plugin writes.

8) What if I can’t access wp-admin to “Save Permalinks” and regenerate .htaccess?

Restore the default block manually (as shown), then regain admin access. Alternatively, use WP-CLI to adjust permalink settings, but the web server rewrite must still be correct or WordPress won’t see the intended routes.

9) Is AllowOverride All a bad idea?

Operationally convenient, security-sensitive. If you can, scope it: enable only what you need (AllowOverride FileInfo for rewrite, maybe Options if required). The fewer directives allowed, the smaller the blast radius of a bad edit.

10) How do I prevent this from happening again?

Stop treating .htaccess as a scratchpad. Put it under version control, deploy it like configuration, and add a rollback mechanism. Also, add synthetic monitoring that detects redirect loops and 500 spikes quickly.

Conclusion: next steps that prevent a repeat

If your WordPress site went down after an .htaccess change, the fix is not mystical. It’s disciplined:

  1. Restore service fast by backing up and reverting to the WordPress default rewrite block.
  2. Use logs as the source of truth. Apache tells you when it can’t parse or apply directives.
  3. Pick one layer for redirects (edge or origin), and stop stacking canonicalization rules in three places.
  4. Harden conservatively. Block explicit sensitive files, disable indexes, add a couple of safe headers. Save the heroics for staging.
  5. Operationalize the file: version control, automated deploy, and a rollback artifact you can apply while half-asleep.

The goal isn’t to never break .htaccess. The goal is to make breaking it non-catastrophic. Production systems reward boring correctness. They punish cleverness with interest.

← Previous
Docker IPv6 in Containers: Enable It Properly (and Avoid Surprise Leaks)
Next →
“640 KB is enough”: The quote myth that won’t die

Leave a comment