Skip to content

.htaccess Support

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.

DirectiveStatus
RewriteEngine, RewriteRule, RewriteCond, RewriteBaseApplied
Order / Allow / DenyApplied
Require all granted, Require all denied, Require ip, Require valid-user, Require userApplied
<Files>, <FilesMatch>, <IfModule>Applied
AuthType Basic, AuthName, AuthUserFileApplied
Redirect, RedirectMatch, RedirectPermanent, RedirectTempApplied
AddEncoding, and the AddType that goes with itApplied — see Caching plugins
Options -Indexes, FileETag, IndexOptions, AddType, AddOutputFilterByTypeNo effect needed — see below
AddHandler, SetHandler, ActionNot applied — the site decides, see below
Header, ErrorDocument, DirectoryIndex, SetEnv, Expires*, php_value, php_flagNot 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.

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&#x27;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.

Four answers, and no fifth:

Meaning
AppliedThe rule is in force.
No effectIt asks for what already happens here — every cPanel file carries a handler line naming the extensions that already run as PHP.
Not appliedOut of scope by design, with the reason on the line.
FailedIt 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:

Terminal window
corepanel htaccess report example.com
corepanel htaccess report example.com --problems

A site whose .htaccess support has been switched off 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.

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.

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:

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

Section titled “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 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.

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.

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.

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:

Terminal window
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).

If the site has the page cache 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 addressDeny 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.

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:

<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 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

Section titled “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 .htaccessNothing at all — no lookups, no memory
A request to a directory whose rules are compiled0,61 µs
The same, when the rules are revalidated7,6 µs, at most once per second per directory
Memory, 200 sitesaround 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” 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, which answers before any of this runs — with the one consequence covered in Rules and the page cache above.

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.

An .htaccess governs what is served out of the document root. A path handled by a reverse proxy route — including one published by an application — 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.

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 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.