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.3KThis 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.
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 +FollowSymlinksThis 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.
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 +SymLinksIfOwnerMatchSymLinksIfOwnerMatch 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.
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 -MultiViewsExample 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:
Options None
Options AllOptions 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:
Options -Indexes -MultiViews +FollowSymlinksThis disables directory listing and content negotiation while keeping symlinks enabled for mod_rewrite.
Common Mistake: Mixing Relative and Absolute Options
Wrong approach:
Options FollowSymlinksWhen 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:
Options -Indexes -MultiViews +FollowSymlinksUsing + 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>
Require ip 203.0.113.50
Require ip 192.168.1.0/24
Require ip 10.0
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>
Require host example.com
Require host .example.com
</IfModule>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">
</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).
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>
<FilesMatch "\.(bak|backup|old|save|tmp|temp)$">
Require all denied
</FilesMatch>
<FilesMatch "^(\.gitignore|\.gitattributes|\.gitmodules|\.editorconfig|\.npmrc|\.yarnrc)$">
Require all denied
</FilesMatch>
<FilesMatch "^(composer\.json|composer\.lock|package\.json|package-lock\.json|yarn\.lock|Gemfile|Gemfile\.lock|Makefile|Gruntfile|Gulpfile|Rakefile)$">
Require all denied
</FilesMatch>
<FilesMatch "^(README|CHANGELOG|CONTRIBUTING|LICENSE|TODO|INSTALL)(\.md|\.txt|\.rst)?$">
Require all denied
</FilesMatch>
</IfModule>Common Mistake: Blocking Only by Extension
Wrong approach:
<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:
<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:
Options -Indexes -MultiViews +FollowSymlinks
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC]
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
<IfModule mod_authz_core.c>
<FilesMatch "(^#.*#|\.(bak|conf|dist|fla|in[ci]|log|orig|psd|sh|sql|sw[op])|~)$">
Require all denied
</FilesMatch>
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
<FilesMatch "^(composer\.(json|lock)|package(-lock)?\.json|yarn\.lock|Makefile)$">
Require all denied
</FilesMatch>
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
</IfModule>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.