Secure and Speed Up Apache and Nginx: Configuration Guide

· Published · 22 min read

Two parallel web server configuration paths apply file protection, routing, caching, compression, response security, verification, and protected rollback

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

EnvironmentCorrect configuration locationActivationRollback unit
Apache with server administration access<VirtualHost> and <Directory> blocksapachectl configtest, then graceful reloadVersioned virtual-host file
Apache shared or managed hostingDocument-root .htaccess, limited by AllowOverrideRead on the next requestKnown-good .htaccess
Nginx serverhttp, server, and location blocksnginx -t, then graceful reloadVersioned Nginx include or server file
CDN or reverse proxy in frontOrigin configuration plus an explicit edge policyOrigin test, edge test, then controlled cache purgeOrigin 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.

ControlEvidence before changeEvidence after changeFailure to watch for
File protectionDocument root, deployment layout, required public filesExpected public asset is 200; protected sample is 403/404Blocking an application entry point or public download
Cache policyAsset naming and release processExpected Cache-Control, Expires, ETag or Last-Modified behaviorUsers retaining changed content under an unchanged URL
CompressionContent types, upstream compression, CPU headroomContent-Encoding, Vary, size reductionDouble compression or compressing secrets beside reflected input
HeadersFrames, scripts, styles, fonts, APIs, third partiesHeaders on 200, redirect, and error responsesCSP or framing policy breaking a legitimate workflow
RedirectsTLS termination point and canonical hostnameOne bounded redirect to the intended URLLoop 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.

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.

ResponseStarting policyReason
Versioned CSS, JavaScript, fonts, and imagespublic, max-age=31536000, immutableThe URL changes with the bytes.
Stable-name CSS, JavaScript, images, media, faviconSeven days to one month, with validatorsThe same URL may receive updated content.
HTML and API JSONno-cache or application-owned policyCaches may store but must revalidate; dynamic behavior stays current.
Account, checkout, admin, or personalized pageprivate, no-store where requiredShared storage or reuse can expose user-specific data.
RSS, Atom, RDF, XML feedsMinutes to one hourThey change independently of a software release.
Manifest or service-worker entry pointRevalidate or short lifetimeA 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 goalNginx mechanismImportant difference
FilesMatch plus Require all deniedExact or regex location with deny all or return 404Nginx matches the normalized URI, not a per-directory filename container.
Options -Indexesautoindex offIt can be set at HTTP, server, or location scope.
ExpiresByTypeexpires and add_header Cache-ControlUsually scoped through locations or a content-type map.
DEFLATEgzip, optional compiled Brotli modulegzip_vary on should be explicit.
Header always setadd_header ... alwaysNested add_header directives can replace inherited headers.
RewriteRule redirectreturn 301 in a dedicated serverA 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 directiveWhy it is risky or misleadingCurrent operating decision
Header set Connection keep-aliveConnection-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 everywhereRemoving validators can force full transfers after expiry.Keep ETag or Last-Modified unless a measured multi-node inconsistency requires another strategy.
One-month ExpiresDefaultUnknown and dynamic content can inherit a long lifetime.Use a zero default and grant lifetimes to known types or paths.
public cache on PHPPersonalized or authenticated responses can reach a shared cache.Let the application own dynamic caching; start with no-cache or stricter.
Overlapping CSS/JS rulesThe winning header depends on merge and processing behavior, making releases unpredictable.Give each asset class one clear policy.
Old Mozilla/Internet Explorer compression exceptionsThey add user-agent cache variance for unsupported clients.Remove unless current supported-client evidence requires an exception.
mode=777 or world-writable web filesAny 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.

PolicyPreferred ownerCross-layer check
Personalized response cachingApplication, because it knows authentication and data sensitivityOrigin and CDN do not override private or no-store.
Fingerprint static cachingWeb server or CDN rule tied to build pathsPurge is not required for a new fingerprint; HTML points to the new URL.
CompressionOrigin or edge, selected from topology and CPUNo double encoding; Vary and content length are correct.
HTTPS canonicalizationFirst trusted public layerOrigin does not loop on the proxy connection scheme.
CSP and security headersApplication or closest reviewed response layerHeaders 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

SymptomLikely causeFirst evidence
Every Apache request returns 500Unpermitted override, unavailable module, smart quotes, malformed regex, or mixed syntaxApache error log and one-directive-at-a-time rollback
Rules have no effectAllowOverride None, wrong document root, proxy cache, or different virtual hostapachectl -S, directory block, response path, cache status
Nginx test passes but application is 404Regex location intercepted a request or try_files changed routingnginx -T, location order, access/error logs
Redirect loopOrigin ignores trusted proxy scheme or multiple layers canonicalize differentlyComplete redirect chain and origin versus edge headers
Changed CSS is not visibleLong lifetime on an unchanged URL, CDN cache, or service workerRequested URL, Age, cache control, service-worker state
No gzip responseSmall body, missing MIME type, client did not advertise gzip, upstream already encoded, or module absentContent-Type, Accept-Encoding, size, module/build output
Security headers missing on images or errorsNginx header inheritance or Apache header condition/contextHeaders from multiple status codes and locations
Fonts fail after deploymentCORS, MIME, CSP, location matching, or cached old font URLBrowser 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

ScenarioStarting approachAcceptance focus
Shared Apache hostingSmall .htaccess blocks, ASCII quotes, provider-supported directivesNo 500, protected test names blocked, application routes intact
Apache virtual private serverMain virtual host with minimal AllowOverride or noneConfig test, graceful reload, explicit directory scope, module inventory
Nginx PHP applicationServer/include snippets, reviewed location order, existing FastCGI blockNo source disclosure, front-controller routing, static policy, header inheritance
Static versioned siteFingerprint assets for one-year immutable caching; revalidate HTMLA new release changes asset URLs and old HTML cannot pin missing assets
CDN-backed mixed siteApplication owns private responses; edge owns selected static cacheOrigin/edge parity, cache key, purge boundary, forwarded scheme trust
Authenticated portalConservative dynamic caching, tested CSP, compression threat reviewNo 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

Related technical notes

Mail queue investigation separating deferred traffic, SMTP responses, route health, and safe diagnostic evidenceLinux, MTA & Security · Jul 20, 2026 · 3 min read

Postfix Queue Backlog: A Safe Diagnostic Runbook

Diagnose a Postfix backlog from queue age, destination groups, SMTP responses, host health, DNS, and route evidence before changing retries or concurrency.

Technical review

Need this checked against your own sending system?

Share the domain, headers, bounces, provider warning, logs, or infrastructure symptom and NitWings will identify the practical next step.

Schedule a Technical Review
Advertisement