Apache
07 / 07

Performance, Security & .htaccess

Apache: Performance, Security & .htaccess

.htaccess

.htaccess files provide per-directory configuration without restarting Apache. They're processed on every request, which has a performance cost. Require AllowOverride All in the parent Directory block. Avoid in high-traffic production if possible — move rules to the VirtualHost block instead.

# .htaccess — common recipes

# 1. Deny access to sensitive files
<FilesMatch ".(env|git|log|sql|bak|sh)$">
    Require all denied
</FilesMatch>

# 2. SPA routing — serve index.html for unknown paths
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [L]

# 3. Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# 4. Block hotlinking
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www.)?myapp.com/ [NC]
RewriteRule .(jpg|jpeg|png|gif|webp)$ - [F,NC,L]

# 5. Custom error pages
ErrorDocument 404 /404.html
ErrorDocument 500 /500.html

Compression (mod_deflate)

# Enable gzip compression
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
    AddOutputFilterByType DEFLATE application/javascript application/json
    AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
    AddOutputFilterByType DEFLATE image/svg+xml

    # Don't compress already-compressed formats
    SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png|webp|woff2?|gz|zip)$ no-gzip

    # Add Vary header
    Header append Vary Accept-Encoding
</IfModule>

Browser Caching (mod_expires)

<IfModule mod_expires.c>
    ExpiresActive On

    # Images — 1 year
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png  "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"

    # Fonts — 1 year
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType application/font-woff2 "access plus 1 year"

    # CSS and JS — 1 month (use content hash in filename for cache-busting)
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"

    # HTML — no cache
    ExpiresByType text/html "access plus 0 seconds"
</IfModule>

<IfModule mod_headers.c>
    # Add Cache-Control header for immutable hashed assets
    <FilesMatch ".[0-9a-f]{8}.(css|js)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

Security Hardening

# Hide Apache version and OS info
ServerTokens Prod
ServerSignature Off

# Disable TRACE method (XST attack prevention)
TraceEnable Off

# Clickjacking protection
Header always set X-Frame-Options "SAMEORIGIN"

# MIME type sniffing prevention
Header always set X-Content-Type-Options "nosniff"

# XSS Protection (legacy, but still useful)
Header always set X-XSS-Protection "1; mode=block"

# CSP (adjust for your app)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

# Restrict access by IP
<Location /admin>
    Require ip 10.0.0.0/8 203.0.113.0/24
</Location>

# Disable unused HTTP methods
<LimitExcept GET POST PUT PATCH DELETE OPTIONS HEAD>
    Require all denied
</LimitExcept>

Performance Tuning (event MPM)

# /etc/apache2/mods-enabled/mpm_event.conf

<IfModule mpm_event_module>
    StartServers             2
    MinSpareThreads         25
    MaxSpareThreads         75
    ThreadLimit             64
    ThreadsPerChild         25
    MaxRequestWorkers      150    # Total concurrent requests
    MaxConnectionsPerChild  0     # 0 = unlimited (recycle after N requests if > 0)
</IfModule>

# Keep-Alive for persistent connections
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5

Logs — Monitoring & Analysis

# Real-time error log
sudo tail -f /var/log/apache2/error.log

# Top requested URLs
awk '{print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head 20

# Top 404s
awk '$9 == 404 {print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head 20

# Top client IPs
awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head 10

# Slow requests (requires mod_logio or custom LogFormat with %D)
# Add to LogFormat: %D = request time in microseconds
awk '{if ($NF > 1000000) print $NF/1000000 "s", $7}' /var/log/apache2/access.log | sort -rn | head 20

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free