Apache access control: allow or deny by IP, host and more
In Apache httpd 2.4 you decide who may reach a URL with the Require directive: Require ip 192.0.2.0/24 lets one network in, Require not ip inside <RequireAll> keeps an address out, and <RequireAny> mixes address rules with passwords. The old Order, Allow and Deny directives only survive through a compatibility module.
From mod_access to Require. The November 2000 Apache Today article "mod_access: Restricting Access by Host" (see the 2000 archive) described Apache 1.3's module. Its job is now done by mod_authz_host and mod_authz_core. All examples below were tested on Apache httpd 2.4 in November 2026.
The Require directive
Require goes inside a <Directory>, <Location> or <Files> section (in a virtual host, put it in one of those sections: directly inside <VirtualHost> it fails with “Require not allowed in <VirtualHost> context”), or in a .htaccess file when AllowOverride AuthConfig is set. Each line names a test ("provider"):
| Line | Lets the request in when… |
|---|---|
Require all granted | always |
Require all denied | never |
Require ip 10.1.0.0/16 | the client address is in the list |
Require host example.org | the client's DNS name ends in that domain (double DNS check) |
Require forward-dns home.example.net | the client's address is one the name resolves to (2.4.19 and later) |
Require local | the client is 127.0.0.0/8, ::1, or the server's own address |
Require env name | an environment variable is set (for example by SetEnvIf) |
Require method GET POST | the HTTP method is listed |
Require expr "…" | an expression is true |
Require valid-user, user, group | the visitor logged in (authentication) |
When a rule refuses a request, the visitor gets 403 Forbidden and the error log gets a line such as AH01630: client denied by server configuration: /var/www/example.com/admin/.
Allow only certain addresses
A typical case: an admin area reachable only from the office and the VPN.
<Location "/admin">
Require ip 192.0.2.0/24 2001:db8:1::/48
Require ip 198.51.100.7
</Location>
Several Require lines in one section count as "any of these", so this allows the office network, the IPv6 range and one VPN address. Apache accepts these forms (Apache does not allow a comment at the end of a directive line, so comments go on their own lines):
# one address
Require ip 203.0.113.9
# the same network written three ways: first bytes only, netmask, CIDR
Require ip 10.1
Require ip 10.1.0.0/255.255.0.0
Require ip 10.1.0.0/16
# IPv6 with a prefix length
Require ip 2001:db8:2:1::/64
Remember IPv6. If your server has an AAAA record, office laptops may arrive over IPv6 and miss an IPv4-only rule.
Block certain addresses
To keep the whole site open but shut out a few addresses, wrap the rules in <RequireAll>: everyone is granted, and then nobody may match the blocklist.
<Directory "/var/www/example.com">
<RequireAll>
Require all granted
Require not ip 198.51.100.7 203.0.113.0/24
</RequireAll>
</Directory>
A Require not line can only refuse; it can never let anyone in on its own. That is why it needs a positive partner inside <RequireAll>. Apache stops with an error if you forget:
$ sudo apache2ctl configtest
AH00526: Syntax error on line 82 of /etc/apache2/sites-enabled/example.com.conf:
negative Require directive has no effect in <RequireAny> directive
A <RequireAll> holding only not lines fails too, with AH01624: <RequireAll> directive contains only negative authorization directives. For long or changing blocklists, a firewall or fail2ban is a better tool than the web server configuration.
Host names and local access
Require host example.org matches www.example.org but not badexample.org, because only whole name parts are compared. To check it, Apache looks up the client's address in reverse DNS, then looks the name up forward to make sure it points back to the same address. This happens whatever HostnameLookups says, and it slows every matching request, so prefer IP ranges.
Require forward-dns works the other way: it resolves a name you give and compares addresses. That suits a home connection with a dynamic DNS name. Require local is the usual guard for status pages:
<Location "/server-status">
SetHandler server-status
Require local
</Location>
More on that in the server-status guide.
Rules based on the request
SetEnvIf (or BrowserMatch) marks requests, and Require env acts on the mark. This refuses two crawler names everywhere under the DocumentRoot:
BrowserMatchNoCase "(BadBot|EvilScraper)" bad_bot
<Directory "/var/www/example.com">
<RequireAll>
Require all granted
Require not env bad_bot
</RequireAll>
</Directory>
The same test written as an expression: Require expr "%{HTTP_USER_AGENT} !~ /BadBot|EvilScraper/i". The Apache documentation warns that the User-Agent header can be set to anything, so treat these rules as a filter for polite bots, not as security. Expressions can also test the time or the query string, for example Require expr "%{TIME_HOUR} -ge 9 && %{TIME_HOUR} -le 17" for office hours. Before httpd 2.4.16 the expression had to be written without the surrounding double quotes.
Combining rules
| Container | Grants when | Use it for |
|---|---|---|
<RequireAny> | at least one rule grants | "office network or password" (also the default for loose lines) |
<RequireAll> | no rule denies and at least one grants | "VPN and password", blocklists |
<RequireNone> | never grants by itself; fails if any rule grants | excluding a group inside <RequireAll> |
Example: staff must come from the VPN and log in.
<Location "/payroll">
AuthType Basic
AuthName "Payroll"
AuthUserFile "/etc/apache2/.htpasswd"
<RequireAll>
Require ip 10.8.0.0/16
Require valid-user
</RequireAll>
</Location>
Swap <RequireAll> for <RequireAny> and the VPN gets in without a password while everyone else must log in. Containers can be nested.
How sections inherit
A subfolder inherits its parent's rules until it sets its own; then its own rules replace the parent's. AuthMerging And or AuthMerging Or in the child combines them instead. Also, <Location> sections are applied after <Directory> sections, so a Require all granted in a <Location "/"> silently overrides every directory rule. Keep file-system rules in <Directory> and URL rules in <Location>.
Behind a proxy, load balancer or CDN
If requests reach Apache through another server, every client seems to come from the proxy's address, and Require ip checks the wrong thing. mod_remoteip replaces the address with the one in a header such as X-Forwarded-For, and Require ip then uses the replaced address:
sudo a2enmod remoteip # Debian, Ubuntu (RHEL loads it by default)
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.0.5
Always list your proxies with RemoteIPTrustedProxy or RemoteIPInternalProxy. Without that list, mod_remoteip trusts the header from any host, and anyone can type a fake X-Forwarded-For to slip past your IP rules. The reverse proxy guide covers the other side.
Converting old Order/Allow/Deny rules
You will still see 2.2-style rules in old tutorials and inherited configs. They need mod_access_compat, which Debian, Ubuntu and RHEL load by default but which the Apache documentation calls deprecated.
| Apache 2.2 | Apache 2.4 |
|---|---|
Order deny,allowDeny from all | Require all denied |
Order allow,denyAllow from all | Require all granted |
Order Deny,AllowDeny from allAllow from example.org | Require host example.org |
Order allow,denyAllow from allDeny from 198.51.100.7 | <RequireAll> with Require all granted and Require not ip 198.51.100.7 |
Allow from 127.0.0.1 + Require valid-user + Satisfy Any | Require ip 127.0.0.1 and Require valid-user on separate lines (implicit any) |
Do not mix the two styles. The upgrade guide shows a server-wide Order deny,allow / Deny from all beating a Require local on /server-status: the request gets 403 and the error log shows AH01797: client denied by server configuration, because the old directives take precedence when the sections merge. Convert everything in one go, then find leftovers and switch the module off:
sudo grep -rniE '^\s*(Order|Allow from|Deny from|Satisfy)\b' /etc/apache2/ /var/www/
sudo a2dismod access_compat
sudo apache2ctl configtest && sudo systemctl restart apache2
On RHEL and Fedora, search /etc/httpd/ and comment out the access_compat_module line in /etc/httpd/conf.modules.d/00-base.conf. If configtest then reports "Invalid command 'Order'", you missed one. More upgrade steps are in upgrading from 2.2 to 2.4.
Testing your rules
Test from an allowed and a refused address. curl prints the status code:
$ curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/admin/
403
$ curl -s -o /dev/null -w '%{http_code}\n' -A 'BadBot/1.0' https://www.example.com/
403
Then check the error log for the AH01630 line to confirm which rule fired. If an allowed address still gets 403, check for IPv6, a proxy in front, or a <Location> overriding your <Directory>.
Sources
- Access Control, Apache HTTP Server 2.4 documentation
- mod_authz_core, Apache HTTP Server 2.4 documentation
- mod_authz_host, Apache HTTP Server 2.4 documentation
- mod_access_compat, Apache HTTP Server 2.4 documentation
- Upgrading to 2.4 from 2.2, Apache HTTP Server 2.4 documentation
- mod_remoteip, Apache HTTP Server 2.4 documentation
- Core features (HostnameLookups), Apache HTTP Server 2.4 documentation
- 00-base.conf (modules loaded by default), CentOS Stream httpd package