# .htaccess — Configure Apache Per Directory

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

Source: https://www.jpkc.com/db/en/cheatsheets/web-servers/htaccess/

<!-- PROSE:intro -->
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.
<!-- PROSE:intro:end -->

## Basics & Options

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

```bash
# Comment
Options -Indexes
```

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

```bash
Options +FollowSymLinks
```

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

```bash
Options -Indexes +FollowSymLinks
```

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

```bash
DirectoryIndex index.php index.html
```

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

```bash
DefaultType text/html
```

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

```bash
AddDefaultCharset UTF-8
```

## Redirects

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

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

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

```bash
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.

```bash
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.

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

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

```bash
RedirectMatch 301 \.html$ /
```

## mod_rewrite

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

```bash
RewriteEngine On
```

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

```bash
RewriteBase /
```

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

```bash
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.

```bash
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.

```bash
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.

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

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

```bash
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).

```bash
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).

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

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

```bash
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.

```bash
RewriteRule ^ /index.php [L]
```

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

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

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

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

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

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

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

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

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

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

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

```bash
RewriteRule \.env$ - [F,L]
```

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

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

## Access Control

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

```bash
Require all granted
```

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

```bash
Require all denied
```

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

```bash
Require ip 192.168.1.0/24
```

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

```bash
Require ip 203.0.113.5
```

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

```bash
<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).

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

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

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

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

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

## Basic Authentication

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

```bash
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.

```bash
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).

```bash
htpasswd /etc/apache2/.htpasswd editor
```

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

```bash
htpasswd -D /etc/apache2/.htpasswd olduser
```

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

```bash
htpasswd -n admin
```

## Custom Error Pages

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

```bash
ErrorDocument 404 /errors/404.html
```

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

```bash
ErrorDocument 403 /errors/403.html
```

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

```bash
ErrorDocument 500 /errors/500.html
```

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

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

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

```bash
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.

```bash
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.

```bash
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.

```bash
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.

```bash
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.

```bash
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.

```bash
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.

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

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

```bash
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).

```bash
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.

```bash
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.

```bash
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.

```bash
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.

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

## Compression (mod_deflate)

Enable gzip compression for common text-based content types.

```bash
<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.

```bash
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).

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

## Caching (mod_expires)

Set browser cache expiry times per file type using mod_expires.

```bash
<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).

```bash
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.

```bash
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.

```bash
FileETag MTime Size
```

## PHP Settings

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

```bash
php_value upload_max_filesize 64M
```

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

```bash
php_value post_max_size 64M
```

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

```bash
php_value max_execution_time 300
```

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

```bash
php_value memory_limit 256M
```

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

```bash
php_flag display_errors Off
```

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

```bash
php_value session.cookie_httponly 1
```

<!-- PROSE:outro -->
## 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

- [mod_rewrite – official Apache documentation](https://httpd.apache.org/docs/current/mod/mod_rewrite.html) – reference for RewriteRule and RewriteCond
- [.htaccess tutorial – Apache HOWTO](https://httpd.apache.org/docs/current/howto/htaccess.html) – introduction and fundamentals
- [.htaccess – Wikipedia](https://en.wikipedia.org/wiki/.htaccess) – background and how it works
<!-- PROSE:outro:end -->

## Related Commands

- [apache](https://www.jpkc.com/db/en/cheatsheets/web-servers/apache/) – the web server whose per-directory behavior .htaccess overrides
- [caddy](https://www.jpkc.com/db/en/cheatsheets/web-servers/caddy/) – modern web server with automatic HTTPS as an Apache alternative
- [certbot](https://www.jpkc.com/db/en/cheatsheets/web-servers/certbot/) – issue Let's Encrypt certificates for HTTPS redirects

