.htaccess — Configure Apache Per Directory

Practical guide to Apache .htaccess — rewrites, redirects, access control, caching, compression and security headers configured per directory.

The .htaccess file is Apache's tool for per-directory configuration: you drop one into a folder, and its directives apply to that directory and everything below it – without touching the central server config and without a restart. With it you control rewrites and redirects (mod_rewrite), access protection, Basic Auth, and caching, compression and security headers. The server re-evaluates the file on every request, trading a little performance for flexibility – for static setups where you have full access, the central config is faster. For overrides to take effect at all, AllowOverride must permit them in the VirtualHost. This guide walks you through the directives you reach for daily, from clean URL routing to a hardened header set.

Basics & Options

Lines starting with # are comments. Options -Indexes disables directory listing.

# Comment
Options -Indexes

Options +FollowSymLinks — Allow Apache to follow symbolic links. Required for mod_rewrite to work.

Options +FollowSymLinks

Options -Indexes +FollowSymLinks — Disable directory listing and enable symlink following (common combination).

Options -Indexes +FollowSymLinks

DirectoryIndex index.php index.html — Set the default files Apache looks for when a directory is requested.

DirectoryIndex index.php index.html

DefaultType text/html — Set the default MIME type for files with no recognized extension.

DefaultType text/html

AddDefaultCharset UTF-8 — Append ;charset=UTF-8 to all text/* Content-Type headers.

AddDefaultCharset UTF-8

Redirects

Redirect 301 /old-page /new-page — Permanent redirect from /old-page to /new-page. Browsers and search engines cache this.

Redirect 301 /old-page.html /new-page

Redirect 302 /temp /new-location — Temporary redirect. Not cached by browsers or search engines.

Redirect 302 /temp /new-location

Redirect permanent /old https://example.com/new — Redirect to a full URL on another domain. 'permanent' is equivalent to 301.

Redirect permanent /blog https://blog.example.com/

RedirectMatch 301 ^/old-dir/(.*)$ /new-dir/$1 — Redirect using a regex pattern. Captures and re-uses path segments.

RedirectMatch 301 ^/posts/(.*)$ /blog/$1

RedirectMatch 301 \.html$ / — Redirect all requests ending in .html to the homepage.

RedirectMatch 301 \.html$ /

mod_rewrite

RewriteEngine On — Enable the rewrite engine. Required at the top of any rewrite block.

RewriteEngine On

RewriteBase / — Set the base URL path for rewrite rules. Use / for the document root.

RewriteBase /

RewriteRule ^old-page$ /new-page [R=301,L] — Permanently redirect /old-page to /new-page. R=301 sends a redirect, L stops processing.

RewriteRule ^contact$ /contact-us [R=301,L]

RewriteRule ^(.*)$ /index.php [L,QSA] — Route all requests to index.php (front controller pattern for CMS frameworks). QSA appends the query string.

RewriteRule ^(.*)$ /index.php [L,QSA]

Route to index.php only if the request does NOT match a real file or directory. The standard Drupal/WordPress pattern.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?q=$1 [L,QSA]

Force HTTPS: redirect all HTTP requests to their HTTPS equivalent.

RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Force www: redirect all non-www requests to www.example.com.

RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Remove www: redirect www.example.com to example.com (naked domain).

RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]

RewriteRule ^(.+)/$ /$1 [R=301,L] — Remove trailing slashes from all URLs (except the root).

RewriteRule ^(.+)/$ /$1 [R=301,L]

Map a clean URL like /page/3 to /index.php?page=3 internally (no redirect).

RewriteCond %{QUERY_STRING} ^$
RewriteRule ^page/([0-9]+)/?$ /index.php?page=$1 [L,QSA]

RewriteRule Flags

[L] — Last rule. Stop processing further rewrite rules after this one matches.

RewriteRule ^ /index.php [L]

[R=301] — Redirect. Send an HTTP redirect response (301 permanent, 302 temporary).

RewriteRule ^old$ /new [R=301,L]

[QSA] — Query String Append. Append the original query string to the new URL.

RewriteRule ^(.*)$ /index.php [L,QSA]

[NC] — No Case. Make the pattern match case-insensitively.

RewriteRule ^contact$ /contact-us [NC,R=301,L]

[NE] — No Escape. Do not URL-encode special characters in the substitution.

RewriteRule ^search$ /search?q=%{QUERY_STRING} [NE,L]

[P] — Proxy. Pass the request to a backend server via mod_proxy (internal proxy).

RewriteRule ^api/(.*)$ http://localhost:3000/$1 [P,L]

[F] — Forbidden. Return a 403 Forbidden response for the matched request.

RewriteRule \.env$ - [F,L]

[G] — Gone. Return a 410 Gone response (the resource no longer exists).

RewriteRule ^deleted-page$ - [G,L]

Access Control

Require all granted — Allow access to all visitors (Apache 2.4+). Replaces Allow from all.

Require all granted

Require all denied — Deny access to all visitors (Apache 2.4+). Replaces Deny from all.

Require all denied

Require ip 192.168.1.0/24 — Allow access only from a specific IP range.

Require ip 192.168.1.0/24

Require ip 203.0.113.5 — Allow access only from a specific IP address.

Require ip 203.0.113.5

Allow access if any of the listed requirements are met (OR logic).

<RequireAny>
    Require ip 192.168.1.0/24
    Require ip 127.0.0.1
</RequireAny>

Block access to a specific file (e.g. .env, wp-config.php, composer.json).

<Files ".env">
    Require all denied
</Files>

Block access to files matching a pattern (e.g. backups, logs, scripts).

<FilesMatch "\.(bak|sql|log|sh|ini)$">
    Require all denied
</FilesMatch>

Block direct access to sensitive dot-files like .htaccess and .htpasswd.

<FilesMatch "^\.(htaccess|htpasswd|env)">
    Require all denied
</FilesMatch>

Basic Authentication

Enable HTTP Basic Authentication. Users must have an entry in the .htpasswd file.

AuthType Basic
AuthName "Restricted Area"
AuthUserFile /path/to/.htpasswd
Require valid-user

htpasswd -c /path/to/.htpasswd <username> — Create a new .htpasswd file and add the first user. Prompts for password.

htpasswd -c /etc/apache2/.htpasswd admin

htpasswd /path/to/.htpasswd <username> — Add a new user to an existing .htpasswd file (omit -c to not overwrite).

htpasswd /etc/apache2/.htpasswd editor

htpasswd -D /path/to/.htpasswd <username> — Remove a user from the .htpasswd file.

htpasswd -D /etc/apache2/.htpasswd olduser

htpasswd -n <username> — Generate a hashed password entry and print it to stdout (without writing to file).

htpasswd -n admin

Custom Error Pages

ErrorDocument 404 /404.html — Show a custom HTML file for 404 Not Found errors.

ErrorDocument 404 /errors/404.html

ErrorDocument 403 /403.html — Show a custom HTML file for 403 Forbidden errors.

ErrorDocument 403 /errors/403.html

ErrorDocument 500 /500.html — Show a custom HTML file for 500 Internal Server Error.

ErrorDocument 500 /errors/500.html

ErrorDocument 404 "Page not found" — Show a plain text message for 404 errors.

ErrorDocument 404 "Sorry, this page does not exist."

ErrorDocument 503 /maintenance.html — Show a maintenance page for 503 Service Unavailable.

ErrorDocument 503 /maintenance.html

Security Headers

Header always set X-Content-Type-Options "nosniff" — Prevent browsers from MIME-sniffing a response away from the declared content type.

Header always set X-Content-Type-Options "nosniff"

Header always set X-Frame-Options "SAMEORIGIN" — Prevent clickjacking by disallowing the page to be embedded in iframes from other origins.

Header always set X-Frame-Options "SAMEORIGIN"

Header always set X-XSS-Protection "1; mode=block" — Enable the browser's built-in XSS filter and block the page if an attack is detected.

Header always set X-XSS-Protection "1; mode=block"

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" — Enable HTTP Strict Transport Security (HSTS) to enforce HTTPS for 1 year.

Header always set Strict-Transport-Security "max-age=31536000"

Header always set Referrer-Policy "strict-origin-when-cross-origin" — Control how much referrer information is sent with requests.

Header always set Referrer-Policy "strict-origin-when-cross-origin"

Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" — Disable browser features like geolocation, microphone, and camera access.

Header always set Permissions-Policy "geolocation=()"

Header always set Content-Security-Policy "default-src 'self'" — Enable Content Security Policy to restrict which sources can load content.

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'"

Remove X-Powered-By and Server headers to hide technology stack information.

Header unset X-Powered-By
Header unset Server

CORS Headers

Header always set Access-Control-Allow-Origin "*" — Allow all origins to make cross-origin requests (public API).

Header always set Access-Control-Allow-Origin "*"

Header always set Access-Control-Allow-Origin "https://example.com" — Allow cross-origin requests only from a specific domain.

Header always set Access-Control-Allow-Origin "https://app.example.com"

Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" — Specify which HTTP methods are allowed for cross-origin requests.

Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"

Header always set Access-Control-Allow-Headers "Content-Type, Authorization" — Specify which request headers are allowed in cross-origin requests.

Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"

Header always set Access-Control-Max-Age "86400" — Cache the preflight OPTIONS response for 24 hours to reduce preflight requests.

Header always set Access-Control-Max-Age "86400"

Compression (mod_deflate)

Enable gzip compression for common text-based content types.

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>

Disable compression for legacy browsers that don't handle it correctly.

BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|zip|gz|bz2|rar)$ no-gzip dont-vary — Skip compression for already-compressed file types (images, archives).

SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip

Caching (mod_expires)

Set browser cache expiry times per file type using mod_expires.

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/html "access plus 1 hour"
</IfModule>

Header set Cache-Control "max-age=31536000, public" — Set Cache-Control header for long-lived static assets (1 year).

Header set Cache-Control "max-age=31536000, public, immutable"

Header set Cache-Control "no-cache, no-store, must-revalidate" — Disable caching for dynamic or sensitive content.

Header set Cache-Control "no-cache, no-store, must-revalidate"

FileETag MTime Size — Configure which file attributes are used to generate ETags for cache validation.

FileETag MTime Size

PHP Settings

php_value upload_max_filesize 64M — Override the maximum file upload size for this directory (mod_php only).

php_value upload_max_filesize 64M

php_value post_max_size 64M — Override the maximum POST request size (should be >= upload_max_filesize).

php_value post_max_size 64M

php_value max_execution_time 300 — Override the maximum script execution time in seconds.

php_value max_execution_time 300

php_value memory_limit 256M — Override the maximum memory a script may use.

php_value memory_limit 256M

php_flag display_errors Off — Disable PHP error output in the browser for production (use On for development).

php_flag display_errors Off

php_value session.cookie_httponly 1 — Mark session cookies as HttpOnly to prevent JavaScript access.

php_value session.cookie_httponly 1

Conclusion

The .htaccess file is powerful because it puts configuration right where the code lives – ideal for shared hosting or per-project overrides. But because Apache re-reads it on every request, the rule of thumb is: anything meant to apply permanently and globally belongs in the central server config; reach for .htaccess only for what genuinely varies per directory. With redirects, choose deliberately between 301 (permanent, cached) and 302 (temporary), and explicitly protect sensitive files like .env or .htpasswd with Require all denied.

Further Reading

  • apache – the web server whose per-directory behavior .htaccess overrides
  • caddy – modern web server with automatic HTTPS as an Apache alternative
  • certbot – issue Let's Encrypt certificates for HTTPS redirects