Serve System User Home Directories Over the Web with Apache HTTPD mod_userdir

In a lot of cases (especially universities), there may be a setup where a single server machine has multiple users (teachers and students) who can upload some pages or scripts that they may also want to serve over the web. In such cases there has to be some way to access the content for each user separately. This could happen via:

  • Multiple domains (foo.com, bar.com)
  • Multiple subdomains (foo.example.com, bar.example.com)
  • Multiple relative paths to a domain (example.com/foo, example.com/bar)

The Apache HTTP Web Server (httpd) ships with mod_userdir module that allows us to do something similar to the last case above, i.e., have multiple relative paths for each user. Using mod_userdir we can allow access to example.com/~username (note the presence of ~ in the syntax) where the username will map to the home directory in the filesystem by default, but we can also configure this behaviour.

For those who may not know, a user home directory – /home/foo, /home/bar – is automatically created by the operating system when a user is added with commands like adduser.

mod_userdir allows us to give a website to everyone on a system.

If this is something you’d want to use, then lets go through the steps to enable this feature and see how the configuration works in detail.

Enabling The Module

Enabling the module is easy in most Unix-like or Unix-based systems, be it macOS or FreeBSD or different Linux distributions like Ubuntu, Debian, CentOS, Fedora, etc. Enabling a module that httpd ships by default generally involves the following steps:

  • Find the main server config path.
  • Uncomment the LoadModule directive that loads the module with any Include directive that loads the default configuration for that module. In some OS distributions, there may not be an existing line to uncomment. In such cases the main config file (httpd.conf or apache2.conf) will anyway have a comment on how to add a LoadModule line to the config. Just search for the term “LoadModule” and “Include” in the file and you’ll find the instructions.
  • For systems that ship with a2enmod you can simply do a2enmod modname instead of following the previous step. In our case that’s a2enmod userdir

Of course you’ll have to restart your web server after making any configuration changes with whichever command works in your case – service apache2 restart, systemctl restart apache2, apachectl restart, httpd restart.

With the userdir module in effect now, let’s look at the usage details.

UserDir Directive

If you look at the default mod_userdir configuration file that ships with Apache and included in the main server config file (httpd.conf or apache2.conf), it looks like this:

<IfModule mod_userdir.c>
	UserDir public_html
	UserDir disabled root

	<Directory /home/*/public_html>
		AllowOverride FileInfo AuthConfig Limit Indexes
		Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
		Require method GET POST OPTIONS
	</Directory>
</IfModule>

Note: If you really can’t find this file in the main server config folder, then just pick up a word from the code above and run a grep.

Due to this configuration, once the module is enabled, if you actually put some sample HTML files in the public_html folder of user home directories and try to visit http://IP_ADDR/~user, then you’ll be able to see the HTML content being served. This is how the file/folder structure should look:

$ tree /home/
/home/
├── bar
│   └── public_html
│       └── index.htm
└── foo
    └── public_html
        └── index.htm

Apache does this automatic serving for any system user because of the UserDir directive which is the main configuration directive that we have to care about with relation to the mod_userdir module.

UserDir can be placed inside:

  1. The main server config (like in the code above).
  2. A virtual host block.

And it can accept three kinds of values:

  1. Directory path
  2. Redirect URLs
  3. Certain keywords – enabled and disabled

Directory Path

Syntax:

UserDir directory-path

For a given request URL – http://example.com/~foo/one/two.html – let’s look at the different values we can pass to UserDir and how it’ll map the request URI (/~foo/one/two.html) to the filesystem to serve content:

# For http://example.com/~foo/one/two.html
UserDir public_html   	        # Will map to /home/foo/public_html/one/two.html
UserDir /usr/web                # Will map to /usr/web/foo/one/two.html
UserDir /home/*/www             # Will map to /home/foo/www/one/two.html

The examples above covers various possible usages:

  1. A relative directory path will be relative to the (system) home directory of ~user from the requested URL. In this case public_html translates to /home/foo/public_html. That’s where one/two.html will be served from.
  2. An absolute directory path can be used to point to a location other than the default home directory. The absolute path is concatenated with the username to form the directory path to serve pages from. In this case it’s /usr/web/foo and not /home/foo. Hence one/two.html will be served from /usr/web/foo.
  3. We can use a wildcard (asterisk or *) match as well which will be replaced with the ~user from the requested URL. In our case that translates to /home/foo/www/one/two.html.

Note: Whenever we use an asterisk (*) in the directive’s value as a directory path or redirect URL (next section), it’ll always get replaced with the username.

Redirect URLs

Syntax:

UserDir redirect-url

We can also pass URLs to UserDir that will be used to send HTTP 302 redirects to the client. For the same sample URL that we saw in the previous section – http://example.com/~foo/one/two.html – this is the translation logic stated by the documentation:

        Directive Value	              Translated path
        ---------------               ---------------
UserDir http://example.com/users	  http://example.com/users/bob/one/two.html
UserDir http://example.com/*/usr	  http://www.example.com/bob/usr/one/two.html
UserDir http://www.example.com/~*/	  http://www.example.com/~bob/one/two.html

Although when I tested it on Ubuntu and CentOS, the expected behaviour did not happen. The way it actually works is that, you must have a wildcard in the Directive value and the value till that point will be considered. So the URL http://example.com/~foo/one/two.html will be translated like this:

        Directive Value	              Translated path
        ---------------               ---------------
UserDir http://example.com/users	  403 Forbidden
UserDir http://example.com/*/usr	  http://example.com/foo/one/two.html (/usr is discarded)
UserDir http://www.example.com/~*/	  http://example.com/~foo/one/two.html (as expected)

In the second case you’ll notice that /usr is discarded in the translation. The URL value is picked till the * point only. This is what I meant by “value till that point will be considered” above.

Keywords

Syntax:

UserDir keyword

Let’s look at what keywords we can use. The first one is disabled. This will turn off all username to directory translations. In effect, the entire feature will be disabled.

# Disable all username to directory translation
UserDir disabled

Now if you want, you can enable this feature for specific users with enabled keyword. We can pass a list of users (space delimited) for the username to directory translation:

# But enable for user foo and bar
UserDir enabled foo bar

Although we first disabled the feature for all users and then enabled for only foo and bar, we still need to specify whether the feature will work on translating the request URI into a directory path or a redirect URL. Hence the combined configuration would look something like this:

UserDir disabled
UserDir enabled foo bar
UserDir public_html

We’ve disabled the userdir feature for all users except foo and bar and configured a public_html folder to be used from their home directories to serve content for incoming HTTP requests automatically.

Here’s a detailed version on how the rules affect the configuration:

  • The keyword disabled used alone will turn off request username to filesystem directory mapping or translations for all users except those that are used with UserDir enabled in some other line.
  • The keyword disabled can also specify a list of usernames delimited by spaces. In such cases a “hard” disable will be performed where trying to turn on the translation for that user with UserDir enabled will also not work.
  • The keyword enabled takes one or more usernames to turn on username to directory translation for. They ignore any global disable in effect, i.e., UserDir disabled (first point above) but fail to work in case of a local disable in effect, i.e., UserDir disabled foo (second point above).

Multiple Directory Paths and URLs

Apache mod_userdir also allows us to set multiple directory paths and URLs where the successive values act as fallback values.

UserDir public_html /usr/web http://example.com/*/

A request to http://example.com/~foo/one/two.html will try to find the page at:

  1. /home/foo/public_html/one/two.html – If not found then next step.
  2. /usr/web/foo/one/two.html – If not found then next step.
  3. If the redirect URL has an asterisk then that will be replaced with the username and client will be 302 redirected. In this case the client will be redirected to http://example.com/foo/. If the match here fails (no asterisk is found) then next step.
  4. HTTP 404 Not Found will be thrown.

Security Tip

It is highly recommended to always set the following when using mod_userdir:

UserDir disabled root

Let’s see why. You may want to have the following setting to automatically serve the contents of all the user home directories (/home/foo, /home/bar) at example.com/~user:

# This will translate to /home/user/./
UserDir ./

But this will also open /root (root user’s home) as a response to example.com/~root which is not desirable. Hence username to directory translation for root user must be disabled. Of course the /root directory will only be served if Apache has the appropriate permissions to access it (+x).

Leave a Reply

Your email address will not be published. Required fields are marked *