# .htaccess Support

> CorePanel reads the .htaccess files a site brings with it from cPanel: deny rules, password-protected directories and mod_rewrite, with a per-file report of anything it does not apply.

Source: https://www.corepanel.net/docs/web/htaccess/
Last updated: 2026-08-27
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

A site arriving from cPanel brings its rules with it. WordPress writes a permalink
block, Wordfence writes nine `Deny from all` files, PrestaShop writes a front
controller, and a directory somebody password-protected years ago is protected by an
`.htaccess` nobody has looked at since.

CorePanel's web server reads those files. You do not have to rewrite them, and nothing
has to be turned on: it is the default for every site.

## What is applied

| Directive | Status |
|---|---|
| `RewriteEngine`, `RewriteRule`, `RewriteCond`, `RewriteBase` | Applied |
| `Order` / `Allow` / `Deny` | Applied |
| `Require all granted`, `Require all denied`, `Require ip`, `Require valid-user`, `Require user` | Applied |
| `<Files>`, `<FilesMatch>`, `<IfModule>` | Applied |
| `AuthType Basic`, `AuthName`, `AuthUserFile` | Applied |
| `Redirect`, `RedirectMatch`, `RedirectPermanent`, `RedirectTemp` | Applied |
| `AddEncoding`, and the `AddType` that goes with it | Applied — see [Caching plugins](#caching-plugins-keep-working) |
| `Options -Indexes`, `FileETag`, `IndexOptions`, `AddType`, `AddOutputFilterByType` | No effect needed — see below |
| `AddHandler`, `SetHandler`, `Action` | Not applied — the site decides, see below |
| `Header`, `ErrorDocument`, `DirectoryIndex`, `SetEnv`, `Expires*`, `php_value`, `php_flag` | Not applied — see below |

Anything not applied is **reported, never ignored quietly**. Every line gets one of four
answers — applied, no effect here, not supported, or an error — with its file and line
number, so a difference in behaviour after a migration has an explanation instead of
being a mystery.

There is deliberately no "coming soon". CorePanel is not trying to be a second Apache: it
serves old sites its own way, and a directive it does not implement is a decision rather
than a queue position. A report that promises work nobody intends to do is a report
nobody reads — including on the two or three lines that genuinely need somebody to look.

## The report, per site

Every account has an **Apache rules** section that reads the `.htaccess` files under a
site's document root and shows what happened to each line.

![Mockup of CorePanel's Apache rules section for example.com. A summary row reads 3 files, 9 applied, 2 with no effect, 2 not applied and 1 failed, above the document root. Three file cards follow: the root .htaccess with RewriteEngine On and a RewriteRule marked applied, an AddHandler line marked as having no effect because those extensions already run as PHP here, and an ExpiresByType line marked not applied because the server sets its own browser cache policy; wp-content/wflogs/.htaccess with Deny from all applied; and shop/.htaccess with a RewriteRule marked failed because the pattern uses a backreference.](https://www.corepanel.net/_astro/htaccess-report-panel.D9mKC93m.svg)

Four answers, and no fifth:

| | Meaning |
|---|---|
| **Applied** | The rule is in force. |
| **No effect** | It asks for what already happens here — every cPanel file carries a handler line naming the extensions that already run as PHP. |
| **Not applied** | Out of scope by design, with the reason on the line. |
| **Failed** | It could not be compiled, and that line is skipped. Worth looking at: a deny rule that failed is a deny rule not being enforced. |

Read it top-down and stop at the ones that are not green. In practice a migrated
WordPress site produces a page of applied rewrite rules, a couple of lines that do
nothing, and — occasionally — one that needs a decision. The filter at the top hides the
applied lines when there are hundreds of them.

The report reads the files **as they are on disk**, which is what *would* apply. The
running server picks a changed file up on its own within a second, so the two differ only
briefly; **Apply now** on a file, or **Apply to the whole site**, closes that gap when
the answer has to be immediate. The same report is available from the command line:

```bash
corepanel htaccess report example.com
corepanel htaccess report example.com --problems
```

A site whose `.htaccess` support has been [switched off](#turning-it-off-for-one-site)
still gets a full report, with a note saying so — "these rules are fine and this site is
not reading them" is exactly the answer somebody needs at that point, and an empty page
would send them hunting for a mistake in the file.

## Password-protected directories

cPanel's Directory Privacy keeps working. The `.htpasswd` file comes across untouched
and its passwords still work — the `$apr1$` hashes `htpasswd` writes are verified
directly, so nobody has to reset anything.

Two limits worth knowing:

- **Only `AuthType Basic`.** Digest authentication is refused, and the directory stays
  closed rather than opening.
- **DES-crypt and plaintext passwords are refused.** Both are from the 1990s and both are
  broken; an account using one is named in the report and cannot sign in until its
  password is reset. Bcrypt, `$apr1$` and `{SHA}` all work.

A password file that cannot be read leaves the directory protected and nobody able to
sign in. That is deliberate: the alternative — opening a directory because we could not
read the list of who may enter it — is the one failure that cannot be undone.

**Changing the password file is enough.** Add a user, change a password or take somebody
out of the file and the next request sees it, at most a second later — the same as for
the `.htaccess` itself, and without touching it. Restoring a password file that was
missing works the same way: the directory starts letting people in again on its own.

## Which files run as PHP is a panel setting

Every domain has a list of extensions that run as PHP. It lives in the site's settings,
where you can see it and change it — and it is the **only** thing that decides. An
`.htaccess` cannot add to it.

That is a deliberate difference from Apache, and it closes a well-worn attack. On Apache,
this in an uploads directory:

```apacheconf
AddHandler application/x-httpd-php .gif
```

makes every `.gif` there run as PHP. Pair it with an upload form that only checks the
file's magic bytes — a `GIF89a` header followed by code passes that check — and "somebody
can upload an image" becomes "somebody runs code as your account". It is one of the most
common ways a plugin vulnerability turns into a compromised site. Here the line simply
does nothing, wherever it came from.

In practice:

- **The cPanel handler line changes nothing.** Every cPanel-generated `.htaccess` carries
  `AddHandler application/x-httpd-ea-phpNN .php .php8 .phtml`, and those already run as
  PHP. The report marks it as having no effect and there is nothing to do.
- **A line naming any other extension is reported**, with the extension named — not as an
  error, but as a difference you can see. If the site genuinely ran PHP on that extension,
  add it to the domain's PHP settings; **until you do, files with that extension are
  served as they are**, which for a PHP file means publishing its source. And if you did
  not put the line there, it is worth finding out who did.

## What "not applied" means for caching and headers

Two groups of directives are dropped on purpose rather than being on a list to implement.

**`Expires*` and `Cache-Control` directives.** CorePanel sets its own browser cache
policy per file type, and honouring an `.htaccess` here would mean fighting over the
same header to reproduce a value that was usually set once, years ago, and forgotten.
That policy is [published in full](https://www.corepanel.net/docs/web/optimizations#browser-cache-how-long-a-visitor-keeps-a-file)
and is longer than what most of these files ask for — a year for fonts and for anything
carrying `?ver=`, a month for images — so a migrated site does not lose browser caching
here, it usually gains it.

**`php_value` and `php_flag`.** These only ever worked under `mod_php`, which no modern
server runs — under PHP-FPM they were already inert on the source server, so nothing
changes. PHP settings belong in the account's PHP configuration or a [`.user.ini`](https://www.corepanel.net/docs/php/user-ini).

**`Header`, `DirectoryIndex` and `ErrorDocument`.** Each of these is a per-domain setting
in the panel — visible there, and changeable without editing a file the application
rewrites on its own. A Content-Security-Policy compiled out of an `.htaccess` written
years ago breaks a site silently the day its assets move, which is the failure that
decided it.

## Directives that need nothing

`Options -Indexes` asks the server not to list the contents of a directory that has no
index file. CorePanel never lists one, so the directive is already satisfied. The same
goes for the `IndexOptions` / `AddIcon` family, for `FileETag`, and for the compression
filters: they configure Apache machinery whose job CorePanel does itself.

`Options +Indexes` — asking *for* a listing — is the one that is reported rather than
applied. Directory listings are not generated at all, so a directory that relied on one
will need an index file.

## Changes apply within a second

However the file was changed — saved in the File Manager, uploaded over FTP, edited over
SSH, or rewritten by a plugin — the new rules are in force on the next request, at most a
second later. There is nothing to reload and no cache to clear. The same holds for a
directory's password file, wherever in the account it lives.

Rules are compiled the first time a request touches the directory they live in, so a
site with no `.htaccess` costs nothing, and one with fifty pays only for the directories
people actually visit.

When you need the answer *now* rather than within a second — a deny rule you have just
fixed, a customer on the phone — **Apply to the whole site** in the Apache rules section
says so, and so does the command behind it:

```bash
corepanel htaccess reload example.com
```

It drops the site's compiled rules and the pages stored for them in one step, and reports
which document root it addressed. Naming directories after the hostname narrows it to
those (`corepanel htaccess reload example.com wp-content`).

## Rules and the page cache

If the site has the [page cache](https://www.corepanel.net/docs/web/optimizations) enabled, one detail is worth
knowing, because it is the only place where an `.htaccess` rule and a CorePanel feature
can disagree.

A cached page is served straight from the cache. Nothing else runs — not the rewrite
rules, not the deny rules. So when you protect a directory, the pages stored from before
you protected it have to go, and CorePanel drops them: the first request that reaches the
site rather than the cache picks up the new rules and empties that directory's cached
pages along with them.

That first request is usually immediate on a live site — any URL under the directory that
is not itself cached will do, and static files count. If you want to be certain a
protection took effect on a busy cached site, run `corepanel htaccess reload <hostname>`
after saving the file: it drops the compiled rules and the stored pages together, which
is the ordering that matters here.

**One case CorePanel cannot cover.** A rule that refuses *a particular address* —
`Deny from 203.0.113.10` — is not applied to a page served from the cache, because the
stored page is filed under its URL and carries no visitor address. A visitor from an
allowed address stores the page; the blocked address is then handed the stored copy.

This does **not** affect the two common cases:

- `Deny from all` — nobody is ever served the page, so nothing is ever stored to replay.
- Password-protected directories — a request carrying credentials is never cached, by
  design.

If a rule has to hold against a specific address and the site uses the page cache, turn
the page cache off for that site, or block the address at the firewall instead, where it
is enforced before anything is served. The compatibility report flags the rule so you are
not left to discover it.

## Caching plugins keep working

A WordPress arriving from another server usually brings a caching plugin, and the way
those plugins are fast is written in the `.htaccess`: rewrite rules that send a visitor
straight to a page the plugin rendered earlier and stored on disk, so PHP never runs.
CorePanel applies those rules, so the plugin keeps doing its job from the first request
after the migration — nothing to reconfigure, nothing to turn back on.

That includes the compressed copies. W3 Total Cache stores its pages as `.html_gzip`,
WP Super Cache as `.html.gz`, and both declare what they are in the same file:

```apache
<IfModule mod_mime.c>
    AddType text/html .html_gzip
    AddEncoding gzip .html_gzip
</IfModule>
```

Those two lines are what let CorePanel hand the stored bytes to the browser untouched —
the encoding says the file is already compressed, the type is the only thing that says it
is a page. Both have to be there. If the `AddType` is missing, the file is served as a
download rather than a page, so CorePanel does not serve it at all: the rewrite is
dropped and the page is generated normally. Slower on that request, and never a broken
one. The same happens for a visitor whose browser does not accept the encoding, and for
encodings other than gzip and Brotli.

**You will probably want to turn the plugin's page cache off anyway.** CorePanel's own
[page cache](https://www.corepanel.net/docs/web/optimizations) serves the same pages from memory rather than from
disk, and it is the faster of the two by a wide margin. Turn off *the page cache feature*
inside the plugin rather than deleting the plugin: W3 Total Cache and its peers also do
object caching, minification and CDN integration, and those are worth keeping — the
object cache in particular is what WooCommerce leans on in the cart and the admin, which
is exactly where a page cache cannot help.

The safe order is: enable CorePanel's page cache, confirm the site is right, then turn off
the plugin's. Running both is not dangerous, just wasteful — two copies of the same page
on the same server, and the one on disk is the slower.

## What this costs, and why other servers refuse it

`.htaccess` has a reputation for being slow, and the reputation is earned. It is worth
knowing exactly what the cost is, because it is the reason nginx and Caddy will not
implement it at all and the reason a panel that promises "full compatibility" is usually
not telling you the whole story.

**What it costs in principle.** Per-directory configuration means the server cannot know
what the rules are until it knows which directory the request lands in. Apache therefore
walks the directory chain on *every single request* — a `/shop/product/42` request looks
for an `.htaccess` in the document root, in `shop/`, and in `shop/product/` — reads every
file it finds, and re-evaluates it. Nothing is remembered between requests. That is the
price of the feature, and it is why the Apache documentation itself recommends turning
`.htaccess` off and moving the rules into the server configuration.

**What CorePanel actually pays.** The rules of a directory are parsed and compiled once,
the first time a request touches it, and then kept. What a request pays after that:

| | |
|---|---|
| A site with no `.htaccess` | Nothing at all — no lookups, no memory |
| A request to a directory whose rules are compiled | **0,61 µs** |
| The same, when the rules are revalidated | **7,6 µs**, at most once per second per directory |
| Memory, 200 sites | around **10 MB** total |

For scale: the PrestaShop page those rules run in front of takes 50 to 200 milliseconds
to generate. The rules are four to five orders of magnitude below the page they gate.

**How we get there.** Three things, and each one is what makes the next affordable:

- **Compiled once per directory, not parsed per request.** A real PrestaShop has 8.037
  directories and about ten of them are ever touched. Only those ten are ever compiled.
- **Checked for changes at most once a second.** Between checks the compiled rules are
  used as they are. This is the one deliberate difference from Apache, and it is what the
  ["changes apply within a second"](#changes-apply-within-a-second) section describes from
  the other side: an edit is live on the next request, at most a second later, instead of
  costing every request a trip to the disk.
- **Patterns are shared across every site on the server.** WordPress, Wordfence and W3
  Total Cache write the same rules on every site they are installed on — a W3 Total Cache
  file alone is 252 patterns — so those are compiled once for the whole machine and
  shared, not once per site. On our test corpus, ten sites running the same applications
  cost 480 KB of compiled rules instead of 5,3 MB: **11 times less**.

The `.htaccess` rules also never touch a page served from the [page
cache](https://www.corepanel.net/docs/web/optimizations), which answers before any of this runs — with the one
consequence covered in [Rules and the page cache](#rules-and-the-page-cache) above.

## Two rules with surprising outcomes

**A syntax error does not take the site down.** Apache answers 500 for the whole
directory when it cannot parse the file. CorePanel serves the site without those rules
and records the error — with one consequence worth stating plainly: a file that will not
parse is a file whose *deny rules are not in force*. Fix it as soon as the report shows
it.

**A rule that cannot be evaluated denies.** `Deny from badbot.example` needs a reverse
DNS lookup for every request, which is a per-request name resolution we will not do.
Rather than skip the line, the directory it appears in is closed and the line is
reported. Skipping it would publish exactly what somebody wrote it to protect.

**A `RewriteRule` whose `RewriteCond` did not compile is dropped with it**, and both
lines are reported. The usual cause is a lookahead or a backreference — the
`RewriteCond %{HTTP_HOST} ^(?!www\.)(.+)$` that half the "force www" recipes on the
internet are written with. The condition is what makes such a rule safe: keeping the
rule without it would redirect `www.example.com` to itself, which is an endless
redirect loop for the whole site. Rewrite the condition without the lookahead
(`RewriteCond %{HTTP_HOST} !^www\.` does the same job) and both lines go green.

## Proxied paths are outside its reach

An `.htaccess` governs what is served **out of the document root**. A path handled by a
[reverse proxy route](https://www.corepanel.net/docs/reverse-proxy) — including one published by an
[application](https://www.corepanel.net/docs/applications) — is not served from there at all: it is another
service mounted on a sub-path, with its own files and its own rules. So the site's
`.htaccess` is not consulted for it. Apache draws the same line: a URL claimed by
`ProxyPass` never becomes a path on disk, so no `.htaccess` is read for it either.

This is what lets an application live under a WordPress site. WordPress ends its
permalink block with `RewriteRule . /index.php [L]`, and the only thing holding it back
is "the request names no existing file" — which a proxied path never does. Without this
rule, every request to an application mounted at `/app` would be rewritten into
WordPress and come back as its 404.

Two consequences worth stating:

- **A deny rule or a password on that path does nothing.** Protect a proxied service
  inside the service itself, not with an `.htaccess` in the docroot beside it.
- **Everything else on the site is unaffected.** The front controller still catches
  permalinks, and every deny rule still refuses; only the paths the route table proxies
  away skip the file.

## Turning it off for one site

There is no server-wide switch, and that is a decision rather than an omission: the only
thing switching it off would change is that deny rules stop being enforced, publishing
`vendor/`, `config/` and `wp-content/wflogs` on every imported site.

A single site can opt out when its rules have deliberately been replaced by
CorePanel's own routing — for instance a [reverse proxy route](https://www.corepanel.net/docs/reverse-proxy)
that has taken over the paths the old front controller used to handle. Leaving the file
in place alongside the new routing applies two front controllers to the same request.
Contact support if you need this; it is not something a site needs by accident.
