Directory Access Control in .htaccess
Controlling who can access what on your web server is one of the most critical responsibilities of a .htaccess configuration. A misconfigured server can expose source code, database credentials, version control history, and backup files to anyone with a browser. These are not hypothetical risks. Automated scanners constantly probe websites for files like .env, .git/config, and database.sql, and a single exposed file can lead to a complete compromise.
This guide covers the Options directive for controlling directory behavior, the Require directive for IP and host-based access control, FilesMatch blocks for targeted file protection, and comprehensive patterns for blocking access to sensitive files that should never be publicly accessible.
The Options Directive
The Options directive controls which server features are available in a specific directory. It determines whether Apache will list directory contents, follow symbolic links, or attempt content negotiation. Each feature can be enabled with a + prefix or disabled with a - prefix.
The general syntax is:
Options [+|-]option [+|-]option ...
Indexes (Directory Listing)
When a user requests a directory URL (like https://example.com/images/) and no index file (index.html, index.php, etc.) exists in that directory, Apache has two choices: show a listing of all files in the directory or return a 403 Forbidden error. The Indexes option controls this behavior.
With Indexes enabled (often the default), Apache generates an HTML page listing every file and subdirectory:
Index of /images/
Name Last modified Size
─────────────────────────────────────────────────
backup-database.sql 2024-01-15 09:30 4.2M
config.php.bak 2024-01-14 14:22 1.1K
logo.png 2024-01-10 08:00 45K
secret-notes.txt 2024-01-12 11:15 2.3K
This is a significant security risk. Attackers can browse your entire file structure, discover backup files, configuration files, and other sensitive resources that you never intended to be publicly accessible.
Disable directory listing:
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
With Indexes disabled, requests for directories without an index file return a 403 Forbidden response instead of a file listing.
Disabling Indexes should be considered mandatory for every production website. Even if you believe no directory lacks an index file, it takes only one overlooked subdirectory to expose your file structure. Always disable it as a baseline security measure.
FollowSymlinks
The FollowSymlinks option tells Apache whether to follow symbolic links (symlinks) in the directory. A symbolic link is a file that points to another file or directory elsewhere on the filesystem.
Options +FollowSymlinks
This option is required for mod_rewrite to function. If you use any RewriteRule directives in your .htaccess, you must have FollowSymlinks enabled, otherwise the rewrite engine will not work.
# Required combination for URL rewriting
Options +FollowSymlinks
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(.*)$ /index.php [L]
</IfModule>
On some shared hosting environments, FollowSymlinks is disabled for security reasons (to prevent users from creating symlinks to files outside their home directory). In those cases, you can use the alternative:
Options +SymLinksIfOwnerMatch
SymLinksIfOwnerMatch only follows a symlink if the owner of the symlink matches the owner of the target file. This is more secure but has a performance cost because Apache must perform additional filesystem checks on every request.
If neither FollowSymlinks nor SymLinksIfOwnerMatch is allowed by your hosting provider and you need mod_rewrite, contact your host. There is no workaround, as the rewrite engine fundamentally requires one of these options.
MultiViews
MultiViews enables Apache's content negotiation feature. When a user requests /about and that exact file does not exist, Apache scans the directory for files named about.* (like about.html, about.php, about.txt) and serves the best match based on the client's preferences.
While this sounds convenient, it often causes problems:
- It can interfere with clean URL rewrite rules by matching files before your
RewriteRulehas a chance to process the request. - It can serve unexpected files. A request for
/downloadmight servedownload.phpwhen you intended it to hit your front controller. - It creates unpredictable behavior that is difficult to debug.
Disable MultiViews:
Options -MultiViews
Example of the problem MultiViews causes:
Imagine you have these files:
/var/www/html/
├── about.html
├── about.php
└── index.php (front controller)
With MultiViews enabled, a request to /about bypasses your front controller and directly serves either about.html or about.php (whichever Apache considers the "best match"). Your application's routing logic never runs.
With MultiViews disabled, the request goes through your rewrite rules and hits the front controller as expected.
None and All
The Options directive also supports two special values that control all options at once:
# Disable ALL options
Options None
# Enable ALL options
Options All
Options None turns off every option, including Indexes, FollowSymlinks, MultiViews, ExecCGI, and Includes. This is the most restrictive setting.
Options All enables all options except MultiViews (which must be explicitly enabled due to its potential for unintended behavior).
Recommended production baseline:
# Disable everything, then enable only what you need
Options -Indexes -MultiViews +FollowSymlinks
This disables directory listing and content negotiation while keeping symlinks enabled for mod_rewrite.
Common Mistake: Mixing Relative and Absolute Options
Wrong approach:
# This REPLACES all options with only FollowSymlinks
# Indexes is now enabled because it wasn't explicitly disabled
Options FollowSymlinks
When you use Options without + or - prefixes, it replaces the entire options list rather than modifying it. Other options that were previously disabled by a parent configuration may become re-enabled.
Correct approach:
# This MODIFIES the existing options list
Options -Indexes -MultiViews +FollowSymlinks
Using + and - prefixes ensures you only change the specific options you intend to, leaving everything else as configured by the parent directory or server config.
The Require Directive (mod_authz_core)
The Require directive, provided by mod_authz_core (Apache 2.4+), controls who is allowed to access resources. It replaces the older Allow, Deny, and Order directives from Apache 2.2.
Require all granted
Allows access to everyone without restriction:
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
This is the most permissive setting. Use it for publicly accessible resources like your main website content, public assets, and static files.
Require all denied
Denies access to everyone without exception:
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
Any request returns a 403 Forbidden response. This is used to completely block access to specific files, directories, or locations. No IP address, no user, and no authentication can override this.
Require ip
Restricts access to specific IP addresses or ranges:
<IfModule mod_authz_core.c>
# Allow a single IP
Require ip 203.0.113.50
# Allow an IP range using CIDR notation
Require ip 192.168.1.0/24
# Allow a partial IP match (all IPs starting with 10.0.)
Require ip 10.0
# Allow multiple IPs and ranges
Require ip 203.0.113.50 198.51.100.0/24 10.0
</IfModule>
When multiple Require ip directives are listed without a wrapping <RequireAny> or <RequireAll> block, they are implicitly combined with OR logic. A client matching any one of the listed IPs or ranges is granted access.
Require host
Restricts access based on the client's hostname (resolved via reverse DNS):
<IfModule mod_authz_core.c>
# Allow access from a specific domain
Require host example.com
# Allow access from any subdomain of example.com
Require host .example.com
</IfModule>
Require host relies on reverse DNS lookups, which add latency to every request and can be spoofed. For security-critical access control, always prefer Require ip over Require host. IP-based restrictions are faster and more reliable.
Combining Requirements
Apache 2.4 provides container directives to combine multiple Require statements with explicit logic:
RequireAll (AND logic): All enclosed requirements must be satisfied.
<RequireAll>
Require ip 192.168.1.0/24
Require valid-user
</RequireAll>
The client must be from the 192.168.1.0/24 network and must provide valid authentication credentials.
RequireAny (OR logic): At least one enclosed requirement must be satisfied.
<RequireAny>
Require ip 192.168.1.0/24
Require ip 10.0.0.0/8
</RequireAny>
The client can be from either network.
RequireNone (NOT logic): None of the enclosed requirements may be satisfied.
<RequireAll>
Require all granted
<RequireNone>
Require ip 203.0.113.100
Require ip 198.51.100.200
</RequireNone>
</RequireAll>
This grants access to everyone except the two listed IP addresses.
Practical example: Admin area restricted to office IPs:
<IfModule mod_authz_core.c>
<Location "/admin">
<RequireAny>
Require ip 203.0.113.0/24
Require ip 198.51.100.50
</RequireAny>
</Location>
</IfModule>
FilesMatch Blocks for Access Control
While the Require directive controls access to directories and locations, <FilesMatch> lets you target specific files or file patterns using regular expressions. This is essential for protecting individual files without affecting the rest of your site.
The syntax is:
<FilesMatch "regex-pattern">
# Access control directives
</FilesMatch>
Blocking a Specific File
<FilesMatch "^config\.php$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
Any direct request to config.php returns 403 Forbidden. The file can still be included or required by other PHP scripts on the server, but it cannot be accessed directly via a browser.
Blocking Multiple File Types
<FilesMatch "\.(sql|log|ini|conf)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
This blocks direct access to all files ending in .sql, .log, .ini, or .conf.
Using Files Instead of FilesMatch
For a single, exact filename match without regex, use the simpler <Files> directive:
<Files ".htpasswd">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</Files>
The difference: <Files> matches exact filenames (with optional wildcards), while <FilesMatch> uses full regular expressions.
Blocking Access to Sensitive Files
This is where access control becomes most critical. Production servers frequently contain files that should never be publicly accessible. Here is a comprehensive approach to locking them down.
Dotfiles (.env, .git, .htpasswd)
On Linux and macOS systems, files and directories beginning with a dot (.) are hidden from normal directory listings but are fully accessible via HTTP if someone knows (or guesses) their name. These dotfiles often contain the most sensitive data on your server.
Common dangerous dotfiles:
| File / Directory | Contains |
|---|---|
.env | Database passwords, API keys, application secrets |
.git/ | Entire version control history, including source code |
.gitignore | Reveals project structure and file patterns |
.svn/ | Subversion metadata and source code |
.htpasswd | Hashed passwords for HTTP authentication |
.htaccess | Server configuration (should not be directly downloadable) |
.ssh/ | SSH keys (catastrophic if exposed) |
.DS_Store | macOS directory metadata revealing file structure |
Block all dotfiles and dot-directories:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC]
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
Let's break down each line:
- Line 1 excludes the
.well-known/directory from the block. This directory is a standardized location (RFC 5785) used by SSL certificate validation (Let's Encrypt), security policies, and other protocols. Blocking it would break certificate renewal and other legitimate services. - Lines 2-3 check that the requested path actually exists as a file (
-f) or directory (-d). This prevents the rule from interfering with clean URL routing where dots might appear in the URL path. - Line 4 matches any path component that starts with a dot and returns a 403 Forbidden response (
[F]flag).
The .well-known exclusion is essential. If you block it, automated SSL certificate renewal (via Let's Encrypt/Certbot or cPanel AutoSSL) will fail because the certificate authority cannot reach the validation files. Always keep this exclusion in place.
Alternative approach using FilesMatch (simpler but less comprehensive):
<IfModule mod_authz_core.c>
<FilesMatch "^\.">
Require all denied
</FilesMatch>
</IfModule>
This blocks access to any file starting with a dot. However, it does not block access to dot-directories or their contents as thoroughly as the rewrite-based approach. For maximum protection, use the mod_rewrite method.
Config Files
Configuration files containing database credentials, API keys, and application settings should never be accessible via HTTP. Even if your application framework stores them outside the document root, backup copies or deployment artifacts sometimes end up in publicly accessible directories.
<IfModule mod_authz_core.c>
<FilesMatch "(^#.*#|\.(bak|conf|dist|fla|in[ci]|log|orig|psd|sh|sql|sw[op])|~)$">
Require all denied
</FilesMatch>
</IfModule>
This single regex pattern blocks a wide range of dangerous file types:
| Pattern | Matches | Why It Is Dangerous |
|---|---|---|
^#.*# | #config.php# | Emacs auto-save files containing source code |
.bak | config.php.bak | Backup files with full source code |
.conf | app.conf, nginx.conf | Configuration files with server settings |
.dist | .env.dist, config.dist | Distribution config templates revealing structure |
.fla | animation.fla | Adobe Flash source files |
.in[ci] | .inc, .ini | Include files and INI configs with credentials |
.log | error.log, debug.log | Log files that may contain sensitive data or stack traces |
.orig | file.orig | Original files from merge conflicts |
.psd | design.psd | Photoshop source files (intellectual property) |
.sh | deploy.sh, backup.sh | Shell scripts revealing server operations |
.sql | backup.sql, dump.sql | Database dumps with all your data |
.sw[op] | .swp, .swo | Vim swap files containing file contents |
~ | config.php~ | Backup files created by various text editors |
Backup Files
Text editors and IDEs create temporary and backup files while you work. If you edit files directly on the server (which you should avoid, but it happens), these backup files can end up in your document root.
Beyond the general pattern above, here are additional patterns for commonly overlooked backup files:
<IfModule mod_authz_core.c>
# Block common backup file patterns
<FilesMatch "\.(bak|backup|old|save|tmp|temp)$">
Require all denied
</FilesMatch>
# Block version control metadata files
<FilesMatch "^(\.gitignore|\.gitattributes|\.gitmodules|\.editorconfig|\.npmrc|\.yarnrc)$">
Require all denied
</FilesMatch>
# Block package manager files
<FilesMatch "^(composer\.json|composer\.lock|package\.json|package-lock\.json|yarn\.lock|Gemfile|Gemfile\.lock|Makefile|Gruntfile|Gulpfile|Rakefile)$">
Require all denied
</FilesMatch>
# Block documentation that reveals project internals
<FilesMatch "^(README|CHANGELOG|CONTRIBUTING|LICENSE|TODO|INSTALL)(\.md|\.txt|\.rst)?$">
Require all denied
</FilesMatch>
</IfModule>
Common Mistake: Blocking Only by Extension
Wrong approach:
# Only blocks .env but not .env.local, .env.production, etc.
<Files ".env">
Require all denied
</Files>
Developers commonly have multiple environment files like .env.local, .env.production, .env.backup, etc. The <Files> directive with an exact name only blocks the base .env file.
Correct approach:
# Blocks .env and all variations (.env.local, .env.production, etc.)
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
The regex ^\.env matches any filename starting with .env, catching all variations.
Comprehensive Security Configuration
Here is a complete, production-ready access control configuration that combines all the patterns discussed in this guide:
# === Directory Options ===
Options -Indexes -MultiViews +FollowSymlinks
# === Block Hidden Files and Directories ===
<IfModule mod_rewrite.c>
RewriteEngine On
# Block dotfiles/dotdirs except .well-known
RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC]
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
# === Block Sensitive File Types ===
<IfModule mod_authz_core.c>
# Editor backup and temporary files
<FilesMatch "(^#.*#|\.(bak|conf|dist|fla|in[ci]|log|orig|psd|sh|sql|sw[op])|~)$">
Require all denied
</FilesMatch>
# Environment files (all variations)
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
# Package manager and build tool files
<FilesMatch "^(composer\.(json|lock)|package(-lock)?\.json|yarn\.lock|Makefile)$">
Require all denied
</FilesMatch>
# Prevent direct access to .htaccess and .htpasswd
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
</IfModule>
Apache already blocks .htaccess and .htpasswd files by default through its AccessFileName directive configuration. However, explicitly adding a <FilesMatch "^\.ht"> block is a defense in depth practice. It ensures protection even if someone changes the default Apache configuration.
Proper directory access control is not a one-time setup. Every time you add new tools, frameworks, or deployment processes to your project, review your access control rules to ensure that no new file types are left exposed. Treat every file in your document root as potentially accessible to the public, and explicitly block everything that should not be.