Secure and Speed Up Apache and Nginx: Configuration Guide
A small web-server configuration change can protect private files, reduce transfer size, and make repeat visits faster. The same change can also create a redirect loop, cache private pages, hide a deployment behind a year-long browser cache, or return HTTP 500 for every request. The difference is not the number of directives. It is whether the change has a boundary, a test, and a rollback.
This guide provides working Apache HTTP Server 2.4 and Nginx configurations for the same goals: deny sensitive files, disable directory listing, set deliberate browser-cache rules, compress suitable responses, add security headers, redirect HTTP to HTTPS, and verify the result. Apache users with shared hosting can use the .htaccess path. Server administrators should prefer the virtual-host path. Nginx does not read .htaccess files, so its equivalent belongs in the Nginx configuration.
Choose the configuration boundary first
| Environment | Correct configuration location | Activation | Rollback unit |
|---|---|---|---|
| Apache with server administration access | <VirtualHost> and <Directory> blocks | apachectl configtest, then graceful reload | Versioned virtual-host file |
| Apache shared or managed hosting | Document-root .htaccess, limited by AllowOverride | Read on the next request | Known-good .htaccess |
| Nginx server | http, server, and location blocks | nginx -t, then graceful reload | Versioned Nginx include or server file |
| CDN or reverse proxy in front | Origin configuration plus an explicit edge policy | Origin test, edge test, then controlled cache purge | Origin rollback and edge-policy rollback |
If you administer Apache, put stable rules in the main configuration. Apache must search for permitted .htaccess files along the filesystem path on every request, and each file is interpreted at request time. Distributed files exist for environments where the content owner cannot edit the main server configuration. They are not automatically the best place for production tuning.
Define the result before editing
Write down the real acceptance criteria. “Make the site secure and fast” cannot be tested. A useful change statement is narrower: hidden configuration files return 403 or 404, directory indexes remain unavailable, HTML is revalidated, versioned static assets are cached for one year, text responses use gzip or Brotli when supported, security headers appear on errors as well as successful pages, and all business transactions still work.
| Control | Evidence before change | Evidence after change | Failure to watch for |
|---|---|---|---|
| File protection | Document root, deployment layout, required public files | Expected public asset is 200; protected sample is 403/404 | Blocking an application entry point or public download |
| Cache policy | Asset naming and release process | Expected Cache-Control, Expires, ETag or Last-Modified behavior | Users retaining changed content under an unchanged URL |
| Compression | Content types, upstream compression, CPU headroom | Content-Encoding, Vary, size reduction | Double compression or compressing secrets beside reflected input |
| Headers | Frames, scripts, styles, fonts, APIs, third parties | Headers on 200, redirect, and error responses | CSP or framing policy breaking a legitimate workflow |
| Redirects | TLS termination point and canonical hostname | One bounded redirect to the intended URL | Loop behind a proxy or cached wrong 301 |
Protect the change and capture a baseline
Use a root-only backup directory and a change identifier. Do not overwrite the only known-good file. The examples intentionally avoid copying an entire filesystem tree or using an unresolved wildcard.
change_id="web-policy-20260821"
sudo install -d -m 0700 "/root/web-change-backup/${change_id}"
# Apache on Debian/Ubuntu. Adjust only after confirming the active file.
sudo cp -a /etc/apache2/sites-available/example.conf \
"/root/web-change-backup/${change_id}/apache-example.conf"
# Nginx alternative.
sudo cp -a /etc/nginx/sites-available/example.conf \
"/root/web-change-backup/${change_id}/nginx-example.conf"
# Shared-hosting Apache alternative, run as the site owner.
cp -p /var/www/example/public/.htaccess \
/var/www/example/private-backup/htaccess.before-change
Keep a backup outside the public document root. A backup named .htaccess.old inside the website may be downloadable unless another rule blocks it. Record baseline headers and response bodies from a public page, a missing page, a CSS file, a JavaScript file, an image, an authenticated page, and one protected test name.
curl -sS -D /tmp/home.before.headers -o /tmp/home.before.body \
https://www.example.com/
curl -sS -D /tmp/css.before.headers -o /dev/null \
https://www.example.com/assets/app.4f2a9c.css
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' \
http://www.example.com/
sha256sum /tmp/home.before.body
wc -c /tmp/home.before.body
Discover the active server and configuration
Do not guess whether Apache, Nginx, a control panel, a container, or a load balancer owns the response. A server header is only a clue and may be removed or supplied by a proxy. Inspect the process, listeners, configuration dump, modules, and request path.
sudo ss -ltnp | grep -E ':(80|443)\b'
ps -ef | grep -E '[a]pache2|[h]ttpd|[n]ginx'
# Apache family.
sudo apachectl -V
sudo apachectl -S
sudo apachectl -M | sort
# Nginx family.
sudo nginx -V
sudo nginx -T
# Observe the external path as the client sees it.
curl -sSIk https://www.example.com/
dig +short www.example.com A
dig +short www.example.com AAAA
Unsafe: publishing apachectl -S, nginx -T, environment output, or complete configuration dumps can expose internal hostnames, filesystem paths, upstream addresses, tokens, and certificate locations. Store diagnostics with restricted permissions and redact them before sharing.
Permit only the Apache overrides you need
An Apache .htaccess file is ignored unless the enclosing directory permits overrides. Granting AllowOverride All is convenient but broad. Use the minimum override classes or AllowOverrideList. This part must be placed in server or virtual-host configuration, not inside .htaccess.
<Directory "/var/www/example/public">
Options -Indexes +SymLinksIfOwnerMatch
AllowOverride None
AllowOverrideList FilesMatch Require ExpiresActive ExpiresDefault \
ExpiresByType AddOutputFilterByType Header RewriteEngine \
RewriteCond RewriteRule
Require all granted
</Directory>
Some hosting platforms expose override categories rather than an exact list. File protection needs authorization permission, cache and header rules need the appropriate file-information or indexes permissions, and rewrite rules need FileInfo. Confirm each directive’s documented context. An unpermitted directive normally causes HTTP 500 rather than being safely ignored.
Protect sensitive files on Apache 2.4
Use Apache 2.4 authorization syntax consistently. Mixing Order, Allow, or Deny from the compatibility module with Require can create confusing merge behavior. Deny filenames that should never be served, while keeping the pattern narrow enough for the application.
# Apache 2.4: deny distributed configuration, credentials, logs,
# shell scripts, SQL dumps, package metadata, and common VCS internals.
<FilesMatch "(?i)(^\.ht|^\.env(?:\.|$)|^composer\.(json|lock)$|^package-lock\.json$|^yarn\.lock$|\.(?:ini|log|sh|sql|sqlite|bak|dist|pem|key)$)">
Require all denied
</FilesMatch>
<FilesMatch "(?i)(?:^|/)(?:\.git|\.svn)(?:/|$)">
Require all denied
</FilesMatch>
FilesMatch evaluates filenames in directory context, not an arbitrary full URL routing policy. Keep secrets, private keys, logs, database dumps, deployment metadata, templates that contain credentials, and backups outside the document root. A deny rule is a second barrier, not a reason to store secrets publicly.
The production rule included application-specific extensions such as engine, inc, install, make, module, profile, test, po, theme, tpl.php, xtmpl, and Subversion administrative names. Add any of those only after inventory proves the application does not intentionally publish them. A generic rule that blocks .test or templates can break legitimate downloads.
Disable directory listing and handle symbolic links deliberately
Options -Indexes
# Prefer this over unrestricted FollowSymLinks where the host supports it.
Options +SymLinksIfOwnerMatch
Options -Indexes prevents Apache from generating a file list when no directory index resource exists. It does not prevent access to a file whose exact URL is known. File permissions and authorization still matter.
Unsafe: enabling Options +FollowSymLinks without understanding the deployment lets Apache follow links regardless of target ownership. A link created by a compromised deployment account may expose material outside the intended tree if filesystem permissions allow it. Prefer SymLinksIfOwnerMatch when it fits the application, or define a reviewed link policy in the virtual host. Test releases that use symlinked current directories before changing this option.
Design cache lifetimes around asset identity
A long lifetime is safe only when a changed file receives a changed URL. A filename such as app.4f2a9c.css can be cached for a year because the build emits a new fingerprint when its content changes. A stable app.css needs a shorter lifetime or revalidation. HTML and personalized responses should not inherit static-file caching.
| Response | Starting policy | Reason |
|---|---|---|
| Versioned CSS, JavaScript, fonts, and images | public, max-age=31536000, immutable | The URL changes with the bytes. |
| Stable-name CSS, JavaScript, images, media, favicon | Seven days to one month, with validators | The same URL may receive updated content. |
| HTML and API JSON | no-cache or application-owned policy | Caches may store but must revalidate; dynamic behavior stays current. |
| Account, checkout, admin, or personalized page | private, no-store where required | Shared storage or reuse can expose user-specific data. |
| RSS, Atom, RDF, XML feeds | Minutes to one hour | They change independently of a software release. |
| Manifest or service-worker entry point | Revalidate or short lifetime | A stale control file can pin an entire application release. |
Do not set private on assets intended for shared CDN caching, and do not describe must-revalidate as “never cache.” It controls reuse after staleness. Test the actual response because an application, CDN, or upstream proxy may replace the origin policy.
Configure Apache expiration rules
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 0 seconds"
ExpiresByType text/html "access plus 0 seconds"
ExpiresByType application/json "access plus 0 seconds"
ExpiresByType application/ld+json "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
ExpiresByType application/atom+xml "access plus 1 hour"
ExpiresByType application/rdf+xml "access plus 1 hour"
ExpiresByType application/rss+xml "access plus 1 hour"
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
ExpiresByType text/javascript "access plus 7 days"
ExpiresByType image/bmp "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/avif "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 week"
ExpiresByType image/vnd.microsoft.icon "access plus 1 week"
ExpiresByType audio/ogg "access plus 1 month"
ExpiresByType video/mp4 "access plus 1 month"
ExpiresByType video/ogg "access plus 1 month"
ExpiresByType video/webm "access plus 1 month"
ExpiresByType font/otf "access plus 1 month"
ExpiresByType font/ttf "access plus 1 month"
ExpiresByType font/woff "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 month"
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
</IfModule>
This keeps the source MIME coverage while using current common media types. It avoids a one-month default for unknown content. If the application emits correct Cache-Control, let the application own dynamic responses and scope these rules to static directories.
Add immutable caching only to fingerprinted assets
<IfModule mod_headers.c>
# Example fingerprint: name.4f2a9c.css or app-a18c29ef.js
<FilesMatch "(?i)(?:[.-][0-9a-f]{8,})\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|avif|woff2?)$">
Header always set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "(?i)\.(?:html?|php)$">
Header always set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
Unsafe: applying a one-year immutable policy to app.css, main.js, a service worker, HTML, or a user-generated image that can be replaced under the same URL can strand a broken or vulnerable release in browsers and intermediate caches. Prove content-addressed naming and purge behavior before using immutable.
Compress suitable Apache responses
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css text/csv
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/ld+json application/manifest+json
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
AddOutputFilterByType DEFLATE application/atom+xml application/rss+xml
AddOutputFilterByType DEFLATE image/svg+xml font/ttf
</IfModule>
Apache mod_deflate adds the Vary: Accept-Encoding response field required for shared caches. The old browser-specific BrowserMatch workarounds for Mozilla 4 and early Internet Explorer are not part of a current baseline. Add a user-agent exception only when current evidence proves a real supported-client defect.
JPEG, PNG, WebP, AVIF, MP4, compressed archives, WOFF, and WOFF2 are already compressed formats. Recompressing them consumes CPU for little benefit. Brotli can be useful when the module is available, but quality controls belong in server or virtual-host context. Measure CPU, latency, and transferred bytes rather than enabling the highest compression level.
Unsafe: compressing a TLS response that contains both a secret and attacker-controlled reflected content can contribute to BREACH-style information disclosure. Do not place CSRF tokens, secrets, or personalized values beside reflected input without an application-level defense. Compression is a transport optimization, not a universal switch.
Add response security headers carefully on Apache
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Start CSP in report-only mode and build from observed application needs.
Header always set Content-Security-Policy-Report-Only \
"default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>
A copied Content Security Policy is rarely correct. Inventory scripts, styles, images, fonts, frames, API destinations, workers, and forms. Collect reports, remove avoidable inline execution, use nonces or hashes where appropriate, test authentication and payment paths, then enforce the reviewed policy. Do not keep report-only forever and call the site protected.
HSTS is intentionally separate:
# Add only on the HTTPS virtual host after every required hostname works on TLS.
Header always set Strict-Transport-Security "max-age=86400"
# Increase deliberately after observation. Add includeSubDomains only when
# every present and future subdomain is HTTPS-ready. Preload is a separate commitment.
Unsafe: a long HSTS lifetime, includeSubDomains, or preload request can make an HTTP-only subdomain unreachable for a long time. Start with a short lifetime on a verified HTTPS host, monitor, and extend through change control.
Redirect Apache traffic without creating a loop
The cleanest HTTP-to-HTTPS redirect is a dedicated port 80 virtual host. If shared hosting forces the rule into .htaccess, test with 302 before changing to 301.
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=302,L,NE]
Behind a trusted load balancer, Apache may see an unencrypted backend connection even when the client used HTTPS. In that design, use a header such as X-Forwarded-Proto only when the proxy overwrites it and direct origin access is blocked. Never trust a client-supplied forwarding header on a publicly reachable origin.
Unsafe: changing an untested redirect to 301 can persist the wrong destination in browsers and search systems. Verify scheme, host, path, query string, ACME challenge behavior, POST endpoints, proxy behavior, and redirect count with 302. Promote to 301 only after the owner accepts the result.
Use this bounded Apache shared-hosting baseline
Options -Indexes
<FilesMatch "(?i)(^\.ht|^\.env(?:\.|$)|\.(?:ini|log|sh|sql|sqlite|bak|pem|key)$)">
Require all denied
</FilesMatch>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 0 seconds"
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/avif "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 month"
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml image/svg+xml
</IfModule>
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Do not paste the full baseline blindly. Start with protection, validate, add cache policy, validate, add compression, validate, then add headers. Small reversible steps tell you which directive caused a failure.
Validate Apache before and after activation
# Server-managed configuration.
sudo apachectl configtest
sudo apachectl -S
sudo apachectl -M | grep -E 'authz_core|expires|filter|deflate|headers|rewrite'
sudo systemctl reload apache2
sudo systemctl is-active apache2
sudo journalctl -u apache2 --since '-5 minutes' --no-pager
# RHEL-family service names may be httpd instead of apache2.
sudo httpd -t
sudo systemctl reload httpd
A shared-hosting user may not have a configuration-test command. Request a noncritical URL immediately after each small edit and inspect the hosting error log. Keep a second authenticated file-management session available so an HTTP 500 does not trap you.
Translate the goals to Nginx, not the syntax
Nginx does not search for or execute .htaccess. A file with that name is ordinary content unless a location rule denies it. Put equivalent policy in a server file or included snippet. Nginx selects a server by listen address and name, then selects a location. Regex locations are evaluated in order after the longest remembered prefix unless ^~ changes that search. A generic copied regex can therefore override an application’s intended PHP, static, or ACME location.
| Apache goal | Nginx mechanism | Important difference |
|---|---|---|
FilesMatch plus Require all denied | Exact or regex location with deny all or return 404 | Nginx matches the normalized URI, not a per-directory filename container. |
Options -Indexes | autoindex off | It can be set at HTTP, server, or location scope. |
ExpiresByType | expires and add_header Cache-Control | Usually scoped through locations or a content-type map. |
DEFLATE | gzip, optional compiled Brotli module | gzip_vary on should be explicit. |
Header always set | add_header ... always | Nested add_header directives can replace inherited headers. |
RewriteRule redirect | return 301 in a dedicated server | A simple return is clearer than a rewrite for canonical redirects. |
Protect files and listings in Nginx
server {
root /var/www/example/public;
autoindex off;
# Permit ACME challenges before the general hidden-file denial.
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
try_files $uri =404;
}
location ~ (^|/)\. {
return 404;
}
location ~* \.(?:ini|log|sh|sql|sqlite|bak|dist|pem|key)$ {
return 404;
}
location = /composer.json { return 404; }
location = /composer.lock { return 404; }
}
Using 404 avoids confirming that a sensitive filename exists; deny all produces 403 and is also valid. Neither choice replaces moving secrets out of the root. Confirm that framework dotfiles, /.well-known/ resources, and public downloads have deliberate handling.
Configure Nginx browser caching
# Stable-name static files: useful but bounded.
location ~* \.(?:css|js|mjs|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|mp4|webm|ogg)$ {
expires 7d;
add_header Cache-Control "public";
try_files $uri =404;
}
# Fingerprinted assets: adjust regex to the build's real naming convention.
location ~* [.-][0-9a-f]{8,}\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|avif|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Dynamic application example. Let the application be stricter when needed.
location ~ \.php$ {
add_header Cache-Control "no-cache" always;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
Regex locations are tested in order, so place the fingerprinted pattern before the broader static pattern when both can match. Replace the PHP socket with the installed service path. Do not create a generic PHP location unless the application architecture already uses one and try_files, path-info behavior, and upload boundaries have been reviewed.
Configure Nginx gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_proxied expired no-cache no-store private auth;
gzip_types
text/plain
text/css
text/csv
application/javascript
application/json
application/ld+json
application/manifest+json
application/xml
application/xhtml+xml
application/atom+xml
application/rss+xml
image/svg+xml;
Nginx always considers text/html without listing it in gzip_types. Check nginx -V before relying on an optional static-gzip or third-party Brotli module. If a CDN compresses the edge response, measure whether origin compression helps or simply adds CPU work.
Add Nginx security headers without losing inheritance
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy-Report-Only \
"default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
By default, Nginx inherits add_header values from the parent only when the current configuration level defines no add_header of its own. A cache-control header inside a location can therefore remove server-level security headers from that location. Inspect the effective nginx -T output and test several location types. Newer Nginx releases provide explicit inheritance controls, but write a configuration compatible with the installed version rather than assuming the newest directive exists.
Redirect HTTP to HTTPS in Nginx
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
try_files $uri =404;
}
# Use 302 during validation, then promote the reviewed destination to 301.
return 302 https://www.example.com$request_uri;
}
If the CDN or load balancer performs the public redirect, do not add a second origin redirect unless its forwarded-protocol trust boundary is defined. Test the origin directly with an approved Host header and the public endpoint separately. One layer should own canonicalization.
Use this bounded Nginx server pattern
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
root /var/www/example/public;
index index.php index.html;
autoindex off;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
location ~ (^|/)\. { return 404; }
location ~* \.(?:ini|log|sh|sql|sqlite|bak|pem|key)$ { return 404; }
location ~* \.(?:css|js|png|jpe?g|gif|svg|webp|avif|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public";
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Keep the site's reviewed FastCGI block here.
}
This is a pattern, not a full deployable virtual host. It intentionally omits certificate paths, application-specific FastCGI parameters, upload rules, rate limits, logging, upstream definitions, CSP enforcement, and HSTS. Merge it into the verified site configuration rather than replacing a working server block.
Test Nginx before reloading
sudo nginx -t
sudo nginx -T > /root/web-change-backup/web-policy-20260821/nginx.effective.txt
sudo systemctl reload nginx
sudo systemctl is-active nginx
sudo journalctl -u nginx --since '-5 minutes' --no-pager
# Confirm that new workers loaded and the listener remains present.
ps -ef | grep '[n]ginx'
sudo ss -ltnp | grep -E ':(80|443)\b'
Nginx validates syntax and referenced files with -t. During a reload, the master starts new workers with the new configuration and gracefully retires old workers. If applying the new configuration fails, the running configuration remains active. That protection does not detect a syntactically valid redirect loop, overly broad deny location, incorrect cache lifetime, or broken application route. HTTP tests remain mandatory.
Do not copy these tempting directives blindly
| Tempting directive | Why it is risky or misleading | Current operating decision |
|---|---|---|
Header set Connection keep-alive | Connection-specific headers are hop-by-hop and do not belong in HTTP/2 or HTTP/3 application responses. | Configure server timeouts and protocol support at the correct layer; do not inject the header. |
FileETag None everywhere | Removing validators can force full transfers after expiry. | Keep ETag or Last-Modified unless a measured multi-node inconsistency requires another strategy. |
One-month ExpiresDefault | Unknown and dynamic content can inherit a long lifetime. | Use a zero default and grant lifetimes to known types or paths. |
public cache on PHP | Personalized or authenticated responses can reach a shared cache. | Let the application own dynamic caching; start with no-cache or stricter. |
| Overlapping CSS/JS rules | The winning header depends on merge and processing behavior, making releases unpredictable. | Give each asset class one clear policy. |
| Old Mozilla/Internet Explorer compression exceptions | They add user-agent cache variance for unsupported clients. | Remove unless current supported-client evidence requires an exception. |
mode=777 or world-writable web files | Any local process or compromised account may alter served code. | Use deployment ownership and minimum directory/file modes. |
Verify behavior from more than one layer
# File protection and public availability.
curl -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/.env
curl -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/assets/app.4f2a9c.css
# Redirect chain.
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://www.example.com/path?q=1
curl -sSIL --max-redirs 5 http://www.example.com/path?q=1
# Cache and security headers.
curl -sSI https://www.example.com/
curl -sSI https://www.example.com/assets/app.4f2a9c.css
curl -sSI https://www.example.com/missing-page
# Compression negotiation.
curl -sS -H 'Accept-Encoding: gzip' -D - -o /dev/null https://www.example.com/
curl -sS --compressed -o /dev/null -w '%{size_download}\n' https://www.example.com/
Repeat from an external network and, where possible, query the origin and CDN separately. Verify desktop and mobile browsers, anonymous and authenticated sessions, form POSTs, downloads, range requests, fonts with cross-origin use, API calls, error pages, redirects, and a real application transaction.
Account for CDNs, proxies, and applications
A response may cross the application, FastCGI or upstream server, Apache or Nginx, load balancer, CDN, service worker, browser cache, and extension. Each layer may add, remove, or reinterpret headers. Decide ownership instead of setting the same field everywhere.
| Policy | Preferred owner | Cross-layer check |
|---|---|---|
| Personalized response caching | Application, because it knows authentication and data sensitivity | Origin and CDN do not override private or no-store. |
| Fingerprint static caching | Web server or CDN rule tied to build paths | Purge is not required for a new fingerprint; HTML points to the new URL. |
| Compression | Origin or edge, selected from topology and CPU | No double encoding; Vary and content length are correct. |
| HTTPS canonicalization | First trusted public layer | Origin does not loop on the proxy connection scheme. |
| CSP and security headers | Application or closest reviewed response layer | Headers exist on error pages and are not lost in a nested Nginx location. |
When a CDN is present, test with a cache-busting query only if the CDN’s key policy includes it. Otherwise you may think you bypassed cache when you did not. Inspect provider cache-status headers privately, compare Age, and purge only the exact approved objects where the provider supports it.
Troubleshoot from the first changed layer
| Symptom | Likely cause | First evidence |
|---|---|---|
| Every Apache request returns 500 | Unpermitted override, unavailable module, smart quotes, malformed regex, or mixed syntax | Apache error log and one-directive-at-a-time rollback |
| Rules have no effect | AllowOverride None, wrong document root, proxy cache, or different virtual host | apachectl -S, directory block, response path, cache status |
| Nginx test passes but application is 404 | Regex location intercepted a request or try_files changed routing | nginx -T, location order, access/error logs |
| Redirect loop | Origin ignores trusted proxy scheme or multiple layers canonicalize differently | Complete redirect chain and origin versus edge headers |
| Changed CSS is not visible | Long lifetime on an unchanged URL, CDN cache, or service worker | Requested URL, Age, cache control, service-worker state |
| No gzip response | Small body, missing MIME type, client did not advertise gzip, upstream already encoded, or module absent | Content-Type, Accept-Encoding, size, module/build output |
| Security headers missing on images or errors | Nginx header inheritance or Apache header condition/context | Headers from multiple status codes and locations |
| Fonts fail after deployment | CORS, MIME, CSP, location matching, or cached old font URL | Browser network console, response headers, CSP report |
Do not restart repeatedly. Preserve the first error, exact timestamp, request ID where available, effective configuration, status, and response headers. Restarting can remove evidence and interrupt unrelated sites while leaving the configuration error untouched.
Roll back precisely
# Apache server-managed example.
sudo install -m 0644 \
/root/web-change-backup/web-policy-20260821/apache-example.conf \
/etc/apache2/sites-available/example.conf
sudo apachectl configtest
sudo systemctl reload apache2
# Nginx example.
sudo install -m 0644 \
/root/web-change-backup/web-policy-20260821/nginx-example.conf \
/etc/nginx/sites-available/example.conf
sudo nginx -t
sudo systemctl reload nginx
# Then rerun the same public, protected, static, dynamic, redirect,
# compression, security-header, and application transaction tests.
Unsafe: deleting every .htaccess, replacing an entire configuration directory, restarting both web servers, purging the whole CDN, or rebooting is not a controlled rollback. Restore the exact changed file, validate its syntax, reload only the owning service, verify the application, and retain the failed configuration for analysis.
Apply the guide to common scenarios
| Scenario | Starting approach | Acceptance focus |
|---|---|---|
| Shared Apache hosting | Small .htaccess blocks, ASCII quotes, provider-supported directives | No 500, protected test names blocked, application routes intact |
| Apache virtual private server | Main virtual host with minimal AllowOverride or none | Config test, graceful reload, explicit directory scope, module inventory |
| Nginx PHP application | Server/include snippets, reviewed location order, existing FastCGI block | No source disclosure, front-controller routing, static policy, header inheritance |
| Static versioned site | Fingerprint assets for one-year immutable caching; revalidate HTML | A new release changes asset URLs and old HTML cannot pin missing assets |
| CDN-backed mixed site | Application owns private responses; edge owns selected static cache | Origin/edge parity, cache key, purge boundary, forwarded scheme trust |
| Authenticated portal | Conservative dynamic caching, tested CSP, compression threat review | No user data in shared cache and no broken login, logout, download, or form flow |
Production completion checklist
- The active web server, document root, virtual host, TLS termination point, proxy path, and configuration owner are confirmed.
- The exact changed files have protected backups outside the public root and a tested restoration command.
- Apache uses one authorization syntax and only permitted override directives; Nginx uses reviewed server and location blocks.
- Secrets and backups live outside the document root; deny rules provide an additional barrier.
- Directory indexes are off and symbolic-link behavior matches the deployment model.
- Static cache lifetimes match asset naming; HTML, APIs, private pages, manifests, and service workers do not inherit immutable caching.
- Compression targets suitable MIME types, emits correct variation behavior, and has been reviewed for sensitive reflected responses.
- CSP starts from observed application requirements; HSTS scope and lifetime are approved separately.
- Redirects were proven with 302 before any permanent response, including the CDN or load-balancer path.
- Syntax, reload, logs, protected files, public assets, redirects, caching, compression, headers, errors, and business transactions all pass.
- The final effective configuration, headers, timings, size comparison, owner approval, and rollback evidence are attached to the change.
Primary references
- Apache HTTP Server
.htaccesstutorial - Apache 2.4 authorization and
Require - Apache
mod_expires - Apache
mod_deflate - Apache
mod_headers - Apache per-directory rewrite guidance
- Nginx headers and expiration module
- Nginx gzip module
- How Nginx selects servers and locations
- Nginx command-line configuration tests and reload controls


