11. Access control with mod_rewrite¶
When the dragons grow too mightyTo slay with pen or swordI grow weary of the battleAnd the storm I walk towardWhen all around is madnessAnd there’s no safe port in viewI long to turn my path homewardTo stop awhile with you—Rush, Madrigal
This chapter covers using mod_rewrite to control access to
resources — blocking requests based on IP address, hostname, user
agent, referrer, time of day, or other request characteristics.
As with other chapters, we’ll show both the mod_rewrite approach
and the simpler alternatives. In most access-control scenarios,
Require, SetEnvIf, and <If> blocks are clearer and more
maintainable than rewrite rules.
Note
The official Apache httpd documentation formerly had a dedicated
rewrite/access guide for access-control recipes using
mod_rewrite. That page has been deprecated and its content
folded into the rewrite/avoid guide — a clear signal from the
httpd project that these tasks are better handled by other means.
11.1. Forbidding image hotlinking¶
One of the oldest mod_rewrite recipes on the internet:
blocking requests for your images when the Referer header indicates
they’re being embedded on someone else’s site. The idea is that if
another site links directly to your images, their visitors consume your
bandwidth while you get none of the traffic.
Here’s the basic approach — return a 403 Forbidden for any image
request whose Referer doesn’t match your own domain:
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
RewriteRule \.(png|jpg|gif|svg)$ - [F]
The first RewriteCond allows requests with an empty referer
through — this covers direct visits, bookmarks, and privacy-conscious
browsers that strip the header. The second condition blocks requests
where the referer is present but doesn’t match your domain. The
[NC] flag makes the match case-insensitive.
Variant: serve an alternate image instead of a 403 — the classic “stop stealing my bandwidth” placeholder:
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
RewriteRule \.(png|jpg|gif|svg)$ /images/hotlink-denied.png [L]
Variant: redirect to the referring page’s homepage:
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
RewriteRule \.(png|jpg|gif|svg)$ %{HTTP_REFERER} [R=302,L]
11.1.1. The non-rewrite alternative¶
You can do this without mod_rewrite using SetEnvIf and
Require:
SetEnvIf Referer "^https?://(www\.)?example\.com" local_referer
SetEnvIf Referer "^$" local_referer
<FilesMatch "\.(png|jpg|gif|svg)$">
Require env local_referer
</FilesMatch>
This is arguably clearer: the access decision is expressed as an authorization rule, not as a rewrite trick.
Limitations: The Referer header is optional — many clients
don’t send it, and some privacy tools strip it. It’s also trivially
spoofed by anyone determined to hotlink your images. This technique
stops casual embedding but won’t deter a motivated actor.
11.2. Blocking specific robots/user agents¶
To block a specific bot by user agent string, test
%{HTTP_USER_AGENT} and return a 403 with the [F] flag:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} BadBot [NC]
RewriteRule ^ - [F]
To block multiple bots, chain them with [OR]:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} BadBot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} EvilScraper [NC]
RewriteRule ^ - [F]
11.2.1. The non-rewrite alternative¶
SetEnvIfNoCase User-Agent "BadBot" bad_bot
SetEnvIfNoCase User-Agent "EvilScraper" bad_bot
<RequireAll>
Require all granted
Require not env bad_bot
</RequireAll>
Why this is a weak defense: User agent strings are entirely client-controlled. Any bot that notices it’s being blocked can simply change its user agent string to impersonate a legitimate browser. This technique is useful for blocking well-behaved bots that respect their own identity (and for blocking the occasional lazy scraper), but it’s not a security measure.
For well-behaved bots, /robots.txt is the polite approach —
it tells crawlers which paths to avoid without any server-side
enforcement. For genuinely abusive traffic, escalate to firewall-level
blocking (iptables, AWS WAF, fail2ban, or similar) where the decision
is based on IP address and request patterns rather than a self-reported
identity.
11.3. Denying by IP address or hostname¶
For a small number of addresses, inline RewriteCond tests work:
RewriteEngine On
RewriteCond %{REMOTE_ADDR} ^192\.168\.1\.100$
RewriteRule ^ - [F]
But for a larger deny list, a RewriteMap is more maintainable.
Create a text file listing the blocked addresses:
# /etc/httpd/conf/denylist.txt
192.168.1.100 denied
10.0.0.50 denied
203.0.113.42 denied
Then use it in a RewriteCond:
RewriteMap denylist txt:/etc/httpd/conf/denylist.txt
RewriteCond ${denylist:%{REMOTE_ADDR}|OK} =denied
RewriteRule ^ - [F]
If the client’s IP is found in the map, the lookup returns denied
and the condition matches. If it’s not found, the default value OK
is returned and the request proceeds normally.
You can also check %{REMOTE_HOST} (the resolved hostname) in the
same way, though this requires HostnameLookups On, which adds a DNS
lookup to every request — a significant performance cost.
11.3.1. The modern alternative¶
Since httpd 2.4, the Require directive handles this natively and
integrates with the authorization framework:
<RequireAll>
Require all granted
Require not ip 192.168.1.100 10.0.0.50 203.0.113.42
</RequireAll>
Or by hostname:
<RequireAll>
Require all granted
Require not host evil.example.com
</RequireAll>
This is shorter, clearer, and shows up where security reviewers expect to find access control rules. Use it.
11.4. Referer-based deflection¶
Sometimes you want to redirect visitors who arrive from specific referring sites to a different URL — perhaps you’ve moved content and the old site still links to the wrong location, or you want to provide a landing page tailored to visitors from a particular source.
A RewriteMap keeps this clean when you have multiple referrers to
handle:
# /etc/httpd/conf/referer-redirects.txt
old-partner.example.com /welcome/partner
defunct-blog.example.net /welcome/blog-readers
RewriteMap refmap txt:/etc/httpd/conf/referer-redirects.txt
RewriteCond %{HTTP_REFERER} ^https?://([^/]+)
RewriteCond ${refmap:%1|NONE} !=NONE
RewriteRule ^ ${refmap:%1} [R=302,L]
The first RewriteCond extracts the hostname from the referer into
%1. The second looks it up in the map — if there’s no match, the
default NONE is returned and the rule is skipped.
This is a legitimate technique for managing inbound traffic from specific sources. Less legitimate uses — redirecting competitors’ referral traffic, serving different content to visitors from review sites — tend to create a maintenance headache and erode trust. Use good judgment.
11.5. Time-based access control¶
mod_rewrite exposes time variables — %{TIME_HOUR},
%{TIME_MIN}, %{TIME_WDAY}, and others — that you can use in
conditions. The most common use case is a maintenance window redirect:
# Redirect all traffic to a maintenance page between 2:00 and 4:00 AM
RewriteEngine On
RewriteCond %{TIME_HOUR} ^0[2-3]$
RewriteCond %{REQUEST_URI} !^/maintenance\.html$
RewriteRule ^ /maintenance.html [R=302,L]
The second condition prevents a redirect loop — without it, the
request for /maintenance.html itself would also be redirected.
You can combine time conditions for more specific windows:
# Saturday (6) and Sunday (0) only
RewriteCond %{TIME_WDAY} ^[06]$
RewriteRule ^/office-hours - [F]
11.5.1. The <If> alternative¶
Since httpd 2.4, the <If> directive with ap_expr is a much
cleaner way to express time-based logic:
<If "%{TIME_HOUR} -ge 2 && %{TIME_HOUR} -lt 4">
RedirectMatch 302 !^/maintenance\.html$ /maintenance.html
</If>
This reads like what it is: a conditional block that applies during certain hours. No rewrite engine, no pattern matching — just a condition and an action.
11.6. Requiring HTTPS¶
This is the single most common mod_rewrite question on the
mailing list — and the one where the alternatives are most compelling.
Approach 1: Redirect in a port-80 VirtualHost — the simplest and best option if you have access to the server config:
<VirtualHost *:80>
ServerName www.example.com
Redirect permanent "/" "https://www.example.com/"
</VirtualHost>
No rewrite engine, no conditions, no regex. Just a permanent redirect from HTTP to HTTPS. This is what you should use.
Approach 2: mod_rewrite — when you’re in .htaccess and
can’t define virtual hosts:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Approach 3: <If> with ap_expr:
<If "! %{HTTPS} == 'on'">
Redirect permanent "/" "https://www.example.com/"
</If>
All three accomplish the same thing. Approach 1 is preferred because
it’s the most explicit and doesn’t involve pattern matching. Approach 2
is what you’ll use in .htaccess. Approach 3 is a good
middle ground when you have server config access but want it expressed
as a condition.
See also the HTTP→HTTPS recipe in Recipes.
11.7. Environment variable gating¶
A powerful pattern in httpd configuration is to separate the decision from the action: one directive sets an environment variable based on some condition, and a later directive acts on that variable.
SetEnvIf and BrowserMatch are the typical variable-setters:
SetEnvIf Remote_Addr "^10\." internal_network
BrowserMatch "MSIE" legacy_browser
You can then test these variables in a RewriteCond:
RewriteEngine On
RewriteCond %{ENV:internal_network} ^$
RewriteRule ^/admin - [F]
This blocks access to /admin for anyone not on the internal
network (where the variable would be empty). The logic lives in two
places — the variable assignment and the rewrite condition — but it’s
compositional: you can reuse the same variable in multiple rules.
The [E=varname:value] flag on a RewriteRule works in the other
direction — the rewrite rule sets a variable for downstream use:
RewriteRule ^/api/ - [E=api_request:1]
Other directives — Header, CustomLog, <If> — can then
check %{ENV:api_request} to alter their behavior for API requests.
This is a useful technique for making mod_rewrite cooperate
with the rest of your configuration rather than trying to do
everything itself.
11.8. When not to use mod_rewrite for access control¶
Throughout this chapter, every recipe has come with a non-rewrite alternative — and in most cases, the alternative is better. Here’s why.
mod_rewrite access control works, but it has real drawbacks:
It’s invisible to the authorization framework. httpd has a well-defined authorization phase (
Require,<RequireAll>,<RequireAny>). Rewrite rules run before that phase. Access restrictions expressed as rewrite rules don’t show up in the places where security reviewers expect to find them, and they don’t compose with other authorization rules.It’s harder to audit. A
Require not ip 10.0.0.1is self-documenting. ARewriteCond %{REMOTE_ADDR}followed by aRewriteRule ^ - [F]achieves the same thing in more lines with more room for subtle bugs.It doesn’t integrate with logging. When
Requiredenies a request, the reason is logged through the authorization framework. When aRewriteRulereturns[F], the log just shows a 403 with no explanation of which rule denied it — you’ll need to enableRewriteLogto trace it.
The httpd 2.4 alternatives cover nearly everything:
Require ip,Require not ip,Require hostIP and hostname-based access control, with full CIDR support.
<RequireAll>,<RequireAny>,<RequireNone>Boolean composition of multiple access rules.
SetEnvIf+Require envHeader-based decisions (user agent, referer, custom headers).
<If>withap_exprArbitrary conditions: time of day, SSL variables, request characteristics, even complex boolean expressions.
Use mod_rewrite for access control only when you genuinely
need something the authorization framework can’t express — which, in
httpd 2.4 and later, is rare.