Apache Web Server Access Control with mod_authz_core Require Directive and Authorization Providers

The Apache HTTP Server is meant to allow access to a bunch of defined resources that clients request over the public or private networks. Now we may not want to expose certain resources over these networks. In fact at a granular level, we may want to authorize access to resources based on various whitelisted hosts, IP addresses, environment variables, HTTP methods, time of the day, server expressions, etc. The mod_authz_core module allows us to do just that. It provides certain directives (chiefly Require) that comes with authorization capabilities allowing us to control the access of authenticated/non-authenticated clients to different resources or portions of the website.

Require Directive

The Require directive that goes into your configuration (either in the main server configuration file or .htaccess) tests if a client is authorized to access a resource based on the restrictions imposed by the authorization provider specified, and its arguments. Here’s how the syntax looks like:

Require [not] authorization-provider arg [arg] …

An authorization provider is the actual thing that allows enforcing access restrictions. Think of it as a piece of code, a function or a module that accepts a bunch of arguments and lets you control access to your web resources under certain conditions. Don’t worry too much, the majority of this article will be covering various kinds of authorization providers and you’ll find out its nothing super complex.

The Require directive can be specified within the <Directory>, <Files> or <Location> sections of the main server configuration files or the .htaccess file. To make sure no mistakes are made, let’s look at a pseudo-snippet:

... lots of config ...
<VirtualHost *:80>
    ...
    <Directory /var/www>
        ...
        # Allow access to all clients/users
        Require all granted
        ...
    </Directory>
    ...
    <Location /special-section>
        ...
        # Allow access to only authenticated users
        Require valid-user
        ...
    </Location>
    ...
</VirtualHost>
... lots of config ...

It is important to remember that some authorization providers work on a normal client whereas some of them work on users authenticated via HTTP Basic auth. Any further mention of “authenticated users” in this article would hence refer to the latter.

Let’s go through the different authorization providers provided by various modules then.

mod_authz_core

The mod_authz_core module provides us with some generic authorization providers besides providing the Require directive itself. It also provides the functionality to register other authorization providers. This functionality is well used by a lot of other modules (as you’ll see in the sections to follow) to register their own set providers to be used with the Require directive. Let’s go through the generic providers first that this module ships with.

Require all

The all provider allows or denies access unconditionally.

# Allow access unconditionally
Require all granted
# Deny access unconditionally
Require all denied

When you set up a brand new virtual host for your site, you’ll notice by default httpd doesn’t really let any kind of client access your website. It throws a 403 Forbidden page.

Apache’s default 403 Forbidden page

To solve for this you must put a Require all granted in your vhost in the context of a directory or location.

Require env

The env provider allows access if one of the environment variables provided in the arguments list is set. You can pass more than one environment variables to the provider.

# Syntax: Require env env-var [env-var] ...
# Only allow access to internal bots
SetEnvIf User-Agent "Internalbot" internal_bot
<Directory "/docroot">
    Require env internal_bot
</Directory>

We set an environment variable called internal_bot (using SetEnvIf) only if the clients’ user agent string contains the term “Internalbot” (regexp). Based on that, we allow access.

Require method

The method provider allows access for specified HTTP request types.

# Allows GET, HEAD, POST and OPTIONS requests only
Require method GET POST OPTIONS

Note: Specifying GET automatically includes HEAD as well.

Require expr

The expr provider allows authorization based on arbitrary ap_expr expressions. Expressions allow evaluating conditions based on checking variables, functions against values using binary, unary operators. If you go through the entire ap_expr documentation link, you’ll realise you can perform a plethora of checks. Few examples from the documentation itself:

# Allow access between business hours 9-5 (both inclusive)
Require expr "%{TIME_HOUR} -ge 9 && %{TIME_HOUR} -le 17"

# Allow access if query string has a “secret” substring and requested URI is one of the values
Require expr "!(%{QUERY_STRING} =~ /secret/) && %{REQUEST_URI} in { '/example.cgi', '/other.cgi' }"

# Allow access if the user agent is not "BadBot"
Require expr "%{HTTP_USER_AGENT} != 'BadBot'"

mod_authz_user

The mod_authz_user module provides additional authorization providers for the authenticated users (HTTP Basic auth), extending capabilities of the core.

Require user

This user provider lets you specify a list of authenticated users who’d be allowed access.

Require user foo john mike

Require valid-user

The valid-user provider just allows access to any successfully authenticated user.

Require valid-user

mod_authz_host

The mod_authz_host extends the authorization types with further host-related providers.

Require ip

The ip provider allows authorization based on the IP address of the remote connecting client. Of course more than one IP address can be passed as arguments. Let’s see a few examples right off the docs:

# A full IP (private or public) match
Require ip 10.1.2.3

# A partial IP check
Require ip 10 172.20 192.168.2

# Network/netmask pair
Require ip 10.1.0.0/255.255.0.0

# In the CIDR notation
Require ip 10.1.0.0/16

# IPv6 addresses and subnets
Require ip 2001:db8::a00:20ff:fea7:ccea
Require ip 2001:db8:1:1::a
Require ip 2001:db8:2:1::/64
Require ip 2001:db8:3::/48

Require host

The host provider authorizes clients based on their host names. It does a double reverse DNS lookup to ensure that the original IP matches the specified hostname(s). In a double reverse DNS, an IP to hostname lookup is done (reverse), followed by a hostname to IP (forward) for double checking.

Require host .net example.org

The hostnames resolved from the IP must end in one of the value strings specified above (.net or example.org). It is important to understand that complete components are matched, hence example.org will match foo.example.org but not fooexample.org.

Require forward-dns

The forward-dns provider allows access based on simple DNS checks. The IP of the connecting client must match with the DNS records for the specified hostname(s).

Require forward-dns foo.example.com

Require local

The local provider is the simplest of all host providers. It only allows localhost to access resources. So either of the following two conditions must apply:

  1. The client address must match 127.0.0.8 or ::1.
  2. The client and server address must be the same.
Require local

If your setup has a bunch of proxies between the client and Apache server, then the client IP address resolved at Apache’s end will be that of the reverse proxy. Hence, the Require directives may not work as expected. In order to solve this, you’ll need to make use of mod_remoteip that lets the server resolve to actual client IP out of the IP address list passed by the intermediate proxies (or load balancers).

mod_authz_groupfile

The mod_authz_groupfile allows group authorization using plain text files. For this we’ll need to first create a file and define groups mapped to a list of authenticated users.

Group Files

Let’s create a group file in the server root and throw in some contents in it:

# cat /etc/apache2/groupfile.txt
mygroup: bob amy mike
mygroup2: foo bar bob

We just defined two groups called mygroup and mygroup2 and assigned 3 users that must be authenticated before authorization, to each. With the group file in hand, we can now define our group-based authorization rules.

Require group

The group provider accepts a bunch of group names as arguments to which the authenticated user must belong according to the mapping stored in the group file. Along with the provider, we also must tell httpd where to look for the group file. This is done with the AuthGroupFile directive.

AuthGroupFile groupfile.txt
# Authenticated user must belong to mygroup (should be bob, amy or mike), but not mygroup2
Require group mygroup

The file path passed to AuthGroupFile must either be an absolute path or one relative to the server root.

mod_authz_owner

The mod_authz_owner module authorizes access to authenticated users whose username or group name matches with the operation system owner or group of the requested file respectively. This module only takes effect when a specific file is requested like:

http://yoursite.com/index.php
http://yoursite.com/media/foo.png
http://yoursite.com/docs/readme.txt

It won’t work for access to http://yoursite.com/ or http://yoursite.com/media even if they have a DirectoryIndex specified. DirectoryIndex <file-name> specifies the file that’ll be processed when the directory is requested instead of a particular file (inside that directory) directly. It is mostly set to index.html or index.php.

Require file-owner

The file-owner provider only allows access to files where the name of the authenticated user is the same as that of the operating system user that owns the file.

Require file-owner

Require file-group

The file-group provider only allows access to files where the authenticated user is a member of a group in the group database that has the same name as the operating system group of the file. This group database should be a text file as we saw in the mod_authz_groupfile section above.

AuthGroupFile groupfile.txt
Require file-group

If you group file is huge then consider using mod_authz_dbm (next section) as it is more efficient for looking up username/password pairs.

mod_authz_dbm

The mod_authz_dbm module is totally similar to mod_authz_groupfile above except instead of a plain text file, it looks up the group in a dbm key/value database file which is a lot more performant than searching through a plain text file. Once the group is found, the authorization for a particular request happens against that.

DBM Group Files

In the dbm database files which essentially stores key/value pairs, the keys must be the username whereas value must be a comma-separated list of groups to which the corresponding user belongs. Creating the file is easy. Apache ships with a tool called htdbm that can be used to create and update DBM file formats. Let’s create one in our server root.

# htdbm -c -p groupdb username_foo
New password:
Re-type new password:
Database groupdb created.

username_foo will be the authenticated user name and the password you type is a list of comma-separated groups. The -p option is important to store the password as plain text because otherwise it’ll get encrypted and the directives below won’t really work. The thing is, htdbm is used for maintaining a database of passwords for modules like mod_authn_dbm. But in this case, we’re using it to create a simple dbm database file with plaintext key and values (no encryption).

There’s also the httxt2dbm tool that ships with the server that allows conversion of text files into dbm files and aren’t specifically meant for maintaining password-based databases. If you want to use that instead then just create a file with key value pairs separated by a space, spanning over multiple lines if required, and pass it through this tool.

# cat groupdb.txt
username_foo mygroup,mygroup2
# httxt2dbm -v -i groupdb.txt -o groupdb

With a dbm file in place, we can get onto using the relevant directives provided by this module.

Require dbm-group

The dbm-group provider allows access if the authenticated user belongs to the group specified by the dbm database. In the configuration we need to provide the location of the dbm file with the AuthDBMGroupFile directive that accepts an absolute path or one relative to the server root.

AuthDBMGroupFile "groupdb"
Require dbm-group mygroup

Require dbm-file-group

The dbm-file-group directive is similar to file-group provided by mod_authz_owner (seen above) in the sense that it authorizes access to specific files only if the authenticated user belongs to a group from the dbm database that matches with the operation system group of the file.

AuthDBMGroupFile "groupdb"
Require dbm-file-group

Others

There are other modules that provide more authorization providers like:

I won’t cover the providers for these modules as in order to understand them, the authentication implementation must also be understood which is beyond the scope of this article.

Next up, we’ll cover some directives provided by mod_authz_core that can be used to combine multiple Require directives together to express complex authorization logic.

<RequireAll> Directive

The <RequireAll> directive can be used to contain a bunch of authorization directives that must all succeed (and none must fail) in order for the <RequireAll> block itself to succeed. It must be placed within <Directory>, <Files>, <Location> or .htaccess.

<RequireAll>
    Require method GET, POST, OPTIONS
    Require local
    Require file-owner
</RequireAll>

The block above would allow access if the request HTTP method is GET, HEAD, POST or OPTIONS and the client is localhost and the resource being requested is a file where the system owner of the file is same as the authenticated user.

<RequireAny> Directive

With the <RequireAny> directive, out of the group of authorization directives that it encloses, only one must succeed in order for the entire <RequireAny> directive to succeed. It must be placed within <Directory>, <Files>, <Location> or .htaccess. So if we re-write the example from the previous section: 

<RequireAny>
    Require method GET, POST, OPTIONS
    Require local
    Require file-owner
</RequireAny>

This block would authorize access if the request method is GET, HEAD, POST or OPTIONS or the client is localhost or the system owner of the file being requested is the same as the authenticated user.

<RequireNone> Directive

The <RequireNone> directive contains a group of authorization directives where none must succeed in order for the entire block to succeed (and not fail). It must be placed within <Directory>, <Files>, <Location> or .htaccess. So modifying the example from the previous section:

<RequireNone>
    Require method GET, POST, OPTIONS
    Require local
    Require file-owner
</RequireNone>

The request must not be GET, HEAD, POST or OPTIONS (can be PUT, DELETE) and must not be from localhost (can be from a public IP) and if a file is being requested then the system owner of that should not match with the authenticated user. Hence, if none of the Require directives succeeds, then only the entire block will return a successful result and Apache will serve the content for the virtual host.

Here’s a fairly complex example from the documentation to blow your mind:

<Directory "/www/mydocs">
    <RequireAll>
        <RequireAny>
            Require user superadmin
            <RequireAll>
                Require group admins
                Require ldap-group "cn=Administrators,o=Airius"
                <RequireAny>
                    Require group sales
                    Require ldap-attribute dept="sales"
                </RequireAny>
            </RequireAll>
        </RequireAny>
        <RequireNone>
            Require group temps
            Require ldap-group "cn=Temporary Employees,o=Airius"
        </RequireNone>
    </RequireAll>
</Directory>

The authorization logic expressed by the block is that (let A = the authenticated user):

(
    (
        A’s username must be `superadmin`
    )
    or (
            (A must belong to the `admins` group and the `Administrators` LDAP group)
            and (A must belong to the `sales` group or have the LDAP `dept` attribute `sales`)
    )
    and (
        A must not belong to the `temps` group and the `Temporary Employees` LDAP group
    )
)

Require not

All the Require directive usage we saw above can also be negated using Require not. So for instance if Require local means authorize access to resources only if the request is from localhost, Require not local can negate that meaning allowing access from only non-localhost. The not negation can be applied in all the cases we’ve seen till now.

It is important to understand though that Require not cannot be used by itself to allow or deny a request. It must be used with another element that evaluates to true or false within a <RequireAll> directive.

<RequireAll>
    Require all granted
    Require not ip a.b.c.d
</RequireAll>

You aren’t allowed to put Require not inside <RequireAny> or <RequireNone>.

.htaccess

If you want to put any of the directives that we learnt till now in .htaccess to make changes at the directory level or because you don’t have root access to the system, then all you have to do is create the .htaccess file and put the contents as it is. No changes required. For instance using the dbm-group authorization provider below:

# cat /home/usr/myapp/.htaccess
AuthType basic
AuthName "private area"
AuthBasicProvider file
AuthUserFile htpasswd.txt
AuthDBMGroupFile groupdb
Require dbm-group mygroup

The code above enables basic auth and restricts access to mygroup group that should be looked up in the groupdb dbm file.

For these directives to take effect, the server must allow overriding with the AllowOverride directive:

# Inside your main conf's <Directory>
AllowOverride AuthConfig

You can get it done yourself if you have root access or ask the sysadmin.

Common UseCases

Let’s go through a few common use cases or just questions people struggle with in the beginning.

My brand new Virtual Host doesn’t work. Returns 403 Forbidden permission denied, why ?

By default, the Apache HTTP Server denies access to all resources in a virtual host. You’ll need to add the following:

Require all granted

I want to lock down access to just localhost.

That’s pretty straightforward, we’ve seen this before.

Require local

Do not specify different IPs like Require ip 127.0.0.1 because an IPv6 (::1) access would fail then. Require local takes care of all things local. Go back to the mod_authz_host section above if you’ve forgotten.

How to block an IP ?

Simple, with a negation.

<RequireAll>
    Require all granted
    Require not ip <IP_TO_BLOCK>
</RequireAll>

How to allow access only if a header is present in the request with a non-empty value ?

Let’s say you always want the X-Auth-Token header to be passed with a valid value, this will do the job:

Require expr "-n %{HTTP:Authorization}"

More on Apache expressions.

How to setup authentication and authorization for different users to different directories ?

This should do the trick (in your httpd.conf or apache2.conf):

<Directory /home/foo/myapp/dir1>
    AuthName "Restricted"
    AuthType Basic
    AuthBasicProvider file
    AuthUserFile htpasswd
    Require user1 user2 user3
</Directory>

<Directory /home/foo/myapp/dir2>
    AuthName "Restricted"
    AuthType Basic
    AuthBasicProvider file
    AuthUserFile htpasswd
    Require user3 user4 user5
</Directory>

Authorize authenticated user access to all pages except a specific one (or vice-versa).

<Directory /home/foo/myapp>
    AuthName "Restricted"
    AuthType Basic
    AuthBasicProvider file
    AuthUserFile htpasswd
    Require valid-user
</Directory>

# Page with no authentication, no authorization
<Location /foo.php>
    Require all granted
</Location>

Conclusion

We just learnt a plethora of authorization providers provided by various httpd modules to be used with the Require directive to impose access restrictions on resources. They can all be mixed up in a group as well to specify a more complex logic within <RequireAll>, <RequireAny> and <RequireNone> sections. Hopefully in the future, referring to this page will let you whip up your complex set of authorization configurations.

If you want to read up on Apache Web Server Authentication modules, then read my other article.

Leave a Reply

Your email address will not be published.