S
Home Application Security ยท Updated 2026-07-08

Web Server Hardening

A web server is the outermost layer of an application stack โ€” every public request passes through it before reaching application code. A poorly configured web server leaks information about the stack, accepts dangerous request types, serves content over insecure channels, and fails to instruct browsers on how to protect users. Hardening the web server addresses these issues at the infrastructure layer, independent of the application code running behind it.

This article covers Nginx and Apache HTTP Server, with IIS notes where configuration differs significantly. Examples assume Linux-based deployments.


1. Remove Version and Server Information

The first rule of web server hardening is minimising what you tell attackers about your stack. Default configurations advertise the server software and version in every response header and error page.

Nginx

# nginx.conf
http {
    server_tokens off;          # Removes version from Server header and error pages
                                # Server: nginx  (instead of Server: nginx/1.25.3)
}

Apache

# httpd.conf or security.conf
ServerTokens Prod               # Server: Apache  (suppresses OS, module, version)
ServerSignature Off             # Removes server info from error pages

IIS

# Remove Server header entirely (IIS 10+)
Import-Module WebAdministration
Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' `
  -filter 'system.webServer/security/requestFiltering' `
  -name 'removeServerHeader' -value 'True'

Additionally, suppress technology-revealing headers added by application frameworks:

# Remove X-Powered-By header (PHP, ASP.NET reveal themselves here)
fastcgi_hide_header X-Powered-By;
proxy_hide_header X-Powered-By;
# Apache โ€” requires mod_headers
Header unset X-Powered-By
Header always unset X-Powered-By

2. TLS Configuration

Correct TLS configuration is foundational. Refer to the SSL/TLS Certificates and Encryption articles for certificate selection and key management. The configuration below covers the server-side protocol and cipher settings.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # Protocols โ€” TLS 1.2 minimum; TLS 1.3 preferred
    ssl_protocols TLSv1.2 TLSv1.3;

    # Cipher suites โ€” forward secrecy, authenticated encryption only
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;    # Let TLS 1.3 clients negotiate freely

    # Session resumption โ€” performance + security balance
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;          # Disable tickets; forward secrecy degrades with long-lived ticket keys

    # OCSP stapling
    ssl_stapling        on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt;
    resolver            1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout    5s;

    # Diffie-Hellman parameters (for DHE cipher suites)
    ssl_dhparam /etc/ssl/dhparam.pem;   # Generate: openssl dhparam -out /etc/ssl/dhparam.pem 3072
}
SSLEngine on
SSLCertificateFile      /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile   /etc/ssl/private/example.com.key
SSLCACertificateFile    /etc/ssl/certs/ca-bundle.crt

SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite          ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder     off
SSLSessionTickets       off

SSLUseStapling          on
SSLStaplingCache        shmcb:/run/apache2/ssl_stapling(32768)
SSLStaplingStandardCacheTimeout 3600

Enforce HTTPS โ€” Redirect All HTTP

# Nginx โ€” redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
# Apache
<VirtualHost *:80>
    RewriteEngine On
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

3. HTTP Security Headers

Security headers instruct the browser on how to handle your content. They are the primary defence against a broad category of client-side attacks. Set them as close to the edge as possible โ€” ideally at the web server layer so they apply to every response regardless of the application behind it.

Strict-Transport-Security (HSTS)

Tells browsers to only connect via HTTPS for the specified duration. Prevents SSL stripping and protocol downgrade attacks.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Parameter Value Meaning
max-age 31536000 1 year โ€” browsers cache this policy
includeSubDomains โ€” Applies to all subdomains โ€” only add if all subdomains serve HTTPS
preload โ€” Allows submission to browser HSTS preload lists โ€” irreversible; test thoroughly first

Warning: includeSubDomains will break any subdomain still serving HTTP. Audit all subdomains before enabling.

X-Content-Type-Options

Prevents browsers from MIME-sniffing a response away from the declared Content-Type. Stops attacks where a file upload of a JPEG containing HTML is executed as HTML.

add_header X-Content-Type-Options "nosniff" always;

Always set this. There is no valid reason to omit it.

X-Frame-Options

Controls whether the page can be embedded in an <iframe> โ€” prevents clickjacking attacks where an attacker overlays your page in a transparent iframe on their malicious site.

add_header X-Frame-Options "DENY" always;
# Or SAMEORIGIN if your application legitimately iframes itself

Superseded by CSP frame-ancestors but keep both for compatibility with older browsers.

Referrer-Policy

Controls how much referrer information is sent with outbound requests. Prevents leaking internal URLs, session tokens in query strings, or user paths to third parties.

add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Policy Behaviour
no-referrer No referrer sent at all
strict-origin-when-cross-origin Full URL for same-origin; only origin for cross-origin; nothing for HTTPโ†’HTTPS downgrades โ€” recommended default
same-origin Referrer sent only for same-origin requests
unsafe-url Always sends full URL โ€” never use this

Permissions-Policy

Restricts which browser features and APIs the page and embedded third-party content can use. Limits the impact of compromised third-party scripts.

add_header Permissions-Policy "
  accelerometer=(),
  camera=(),
  geolocation=(),
  gyroscope=(),
  magnetometer=(),
  microphone=(),
  payment=(),
  usb=()
" always;

Deny all features your application does not use. If your site uses geolocation, allow it explicitly: geolocation=(self).

Content-Security-Policy (CSP)

CSP is the most powerful and most complex security header. It defines a whitelist of trusted sources for scripts, styles, images, fonts, and connections โ€” the browser blocks everything else.

add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' https://www.google-analytics.com https://js.stripe.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com https://www.google-analytics.com;
  frame-src https://js.stripe.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  report-uri https://csp-report.example.com/collect;
" always;

Deployment approach โ€” always start in report-only mode:

# Phase 1: Report-only โ€” no blocking, collect violations
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;

# Phase 2: Once violations are clean, switch to enforcing
add_header Content-Security-Policy "default-src 'self'; ..." always;

CSP directives reference:

Directive Controls Critical Setting
default-src Fallback for all resource types 'self'
script-src JavaScript sources Avoid 'unsafe-inline'; use nonces
style-src CSS sources Avoid 'unsafe-inline' where possible
connect-src fetch, XHR, WebSocket targets Prevents data exfiltration to unknown domains
frame-ancestors Who can embed this page 'none' or 'self'
object-src Plugins (Flash etc.) Always 'none'
base-uri <base> tag targets 'self'
form-action Where forms can POST 'self' โ€” critical for credential protection
upgrade-insecure-requests Forces HTTPโ†’HTTPS upgrades Always include

For SPAs and applications using inline scripts, use nonce-based CSP rather than 'unsafe-inline':

# Nginx with nonce โ€” requires server-side generation
# In application code, generate a cryptographically random nonce per request:
# nonce = base64(os.urandom(16))
# Inject into both the CSP header and the <script> tag

# Header:
add_header Content-Security-Policy "script-src 'self' 'nonce-$request_id';" always;

# Template:
# <script nonce="{{nonce}}">...</script>

For additional client-side protection depth, see the Advanced Web Application Firewall article.

Cross-Origin Headers

# Prevent cross-origin reads of your resources
add_header Cross-Origin-Resource-Policy "same-origin" always;

# Isolate your browsing context โ€” required for SharedArrayBuffer, precise timers
add_header Cross-Origin-Opener-Policy "same-origin" always;

# Require CORP on all sub-resources (enables full cross-origin isolation)
add_header Cross-Origin-Embedder-Policy "require-corp" always;

Note: Cross-Origin-Embedder-Policy: require-corp will break loading of cross-origin resources (e.g. CDN assets, third-party iframes) that do not set Cross-Origin-Resource-Policy. Only enable it if your application is designed for cross-origin isolation.

Complete Header Block โ€” Nginx

# Place in http {} or server {} block
add_header Strict-Transport-Security  "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options     "nosniff"                                       always;
add_header X-Frame-Options            "DENY"                                          always;
add_header Referrer-Policy            "strict-origin-when-cross-origin"               always;
add_header Permissions-Policy         "accelerometer=(), camera=(), geolocation=(), microphone=(), payment=(), usb=()" always;
add_header Content-Security-Policy    "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
add_header Cross-Origin-Resource-Policy "same-origin"                                 always;

4. Restrict HTTP Methods

Only enable the HTTP methods your application requires. TRACE enables cross-site tracing (XST) attacks and must always be disabled. OPTIONS reveals supported methods and is rarely needed externally.

# Nginx โ€” allowlist GET, POST, HEAD only
location / {
    limit_except GET POST HEAD {
        deny all;
    }
}
# Apache โ€” block TRACE and OPTIONS globally
<LimitExcept GET POST HEAD>
    Order deny,allow
    Deny from all
</LimitExcept>

# Explicitly disable TRACE
TraceEnable Off
# Verify TRACE is disabled
curl -X TRACE https://example.com -I
# Should return: 405 Method Not Allowed

5. Directory Listing and File Exposure

Disable directory listing to prevent exposing file structures, backup files, and configuration to unauthenticated users.

# Nginx โ€” directory listing is off by default; confirm it
autoindex off;
# Apache โ€” disable directory listing
Options -Indexes -FollowSymLinks

# Also disable .htaccess override in production for performance + security
AllowOverride None

Block Sensitive File Access

Prevent direct access to configuration files, version control directories, logs, and backup files:

# Nginx
location ~* /\.(git|env|svn|htaccess|htpasswd|DS_Store) {
    deny all;
    return 404;
}

location ~* \.(bak|backup|sql|log|conf|config|ini|old|orig|save|swp|tmp)$ {
    deny all;
    return 404;
}
# Apache
<FilesMatch "(\.env|\.git|\.htaccess|\.htpasswd|\.log|\.bak|\.sql|\.conf|\.ini|\.swp|\.old)$">
    Require all denied
</FilesMatch>

6. Request Size and Timeout Limits

Unconstrained request sizes enable denial-of-service attacks via large body uploads, and slow-read attacks via deliberately slow clients (Slowloris).

Nginx

http {
    # Limit request body size (adjust for legitimate file upload needs)
    client_max_body_size        10m;

    # Limit header size
    large_client_header_buffers 4 16k;
    client_header_buffer_size   1k;

    # Timeouts โ€” protect against Slowloris and slow-read attacks
    client_body_timeout         12s;    # Time to receive the request body
    client_header_timeout       12s;    # Time to receive the request headers
    keepalive_timeout           15s;    # Keep-alive connection timeout
    send_timeout                10s;    # Time between two successive writes to client
}

Apache

# mod_reqtimeout โ€” protect against Slowloris
<IfModule mod_reqtimeout.c>
    RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500
</IfModule>

# Limit request size
LimitRequestBody    10485760    # 10 MB
LimitRequestFields  100
LimitRequestFieldSize 8190
LimitRequestLine    8190

# Timeouts
Timeout             60
KeepAliveTimeout    5
MaxKeepAliveRequests 100

7. Rate Limiting

Rate limiting at the web server layer provides a fast, low-cost defence against brute force, credential stuffing, and scraping before requests reach the application.

http {
    # Define rate limit zones
    limit_req_zone $binary_remote_addr zone=global:10m    rate=30r/s;
    limit_req_zone $binary_remote_addr zone=login:10m     rate=5r/m;
    limit_req_zone $binary_remote_addr zone=api:10m       rate=60r/m;

    server {
        # Apply global rate limit with burst allowance
        limit_req zone=global burst=50 nodelay;

        # Stricter limit on authentication endpoints
        location ~* ^/(login|signin|auth|token|password) {
            limit_req zone=login burst=3 nodelay;
            limit_req_status 429;
            # ...
        }

        # API rate limit
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
        }
    }
}
# Apache โ€” mod_ratelimit (less granular than Nginx)
<Location "/login">
    SetOutputFilter RATE_LIMIT
    SetEnv rate-limit 400   # KB/s โ€” limits response rate, not request rate
</Location>

# For request-level rate limiting in Apache, use mod_evasive:
<IfModule mod_evasive24.c>
    DOSHashTableSize    3097
    DOSPageCount        5
    DOSSiteCount        50
    DOSPageInterval     1
    DOSSiteInterval     1
    DOSBlockingPeriod   10
</IfModule>

8. Error Handling

Custom error pages prevent leaking stack traces, server paths, framework versions, and SQL errors to clients.

# Nginx โ€” custom error pages
error_page 400 401 403 404     /errors/4xx.html;
error_page 500 502 503 504     /errors/5xx.html;

location ^~ /errors/ {
    internal;   # Only accessible via internal redirects, not directly
    root /var/www;
}
# Apache
ErrorDocument 400 /errors/400.html
ErrorDocument 403 /errors/403.html
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html

Ensure error pages:
- Do not include server paths, software versions, or stack traces
- Return the correct HTTP status code (not 200 with an error body)
- Do not include user-supplied input (reflected XSS risk)


9. Logging

Comprehensive, structured logging is essential for incident detection and forensic investigation.

Nginx โ€” Structured JSON Logging

http {
    log_format json_combined escape=json
        '{'
            '"time":"$time_iso8601",'
            '"remote_addr":"$remote_addr",'
            '"method":"$request_method",'
            '"uri":"$uri",'
            '"status":$status,'
            '"body_bytes_sent":$body_bytes_sent,'
            '"request_time":$request_time,'
            '"http_referer":"$http_referer",'
            '"http_user_agent":"$http_user_agent",'
            '"http_x_forwarded_for":"$http_x_forwarded_for",'
            '"ssl_protocol":"$ssl_protocol",'
            '"ssl_cipher":"$ssl_cipher"'
        '}';

    access_log /var/log/nginx/access.log json_combined;
    error_log  /var/log/nginx/error.log  warn;
}

Apache โ€” Combined Log Format

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %T" combined_plus
CustomLog /var/log/apache2/access.log combined_plus
ErrorLog  /var/log/apache2/error.log
LogLevel  warn

Log Security

  • Do not log passwords or session tokens โ€” sanitise query strings before logging (?token=REDACTED)
  • Rotate logs โ€” logrotate daily with compression and 90-day retention minimum
  • Forward to SIEM โ€” logs on the server are accessible to an attacker who has compromised it; ship off-host in real time
  • Restrict log file permissions โ€” 640, owned by root, readable by log aggregation service user only

10. Keep-Alive and HTTP/2

# Enable HTTP/2 for performance and security (requires TLS)
listen 443 ssl http2;

# Keep-Alive โ€” balance resource use against performance
keepalive_timeout  65;
keepalive_requests 1000;

HTTP/2 provides several security benefits over HTTP/1.1:
- Header compression reduces attack surface for header-injection attacks
- Multiplexing reduces connection overhead, making Slowloris-style attacks less effective
- Requires TLS in practice (browsers only implement HTTP/2 over TLS)


11. Disable Unused Modules

Every enabled module is additional attack surface. Audit and disable modules not in use.

Apache โ€” Common Modules to Disable

# Disable modules (Debian/Ubuntu)
a2dismod status        # Server status page โ€” disable on production
a2dismod info          # Server info page โ€” disable on production
a2dismod userdir       # User home directory access
a2dismod autoindex     # Directory listing
a2dismod cgi           # CGI execution โ€” disable unless needed

# Confirm loaded modules
apache2 -M

Nginx

Nginx is compiled with modules statically โ€” avoid enabling ngx_http_stub_status_module on production, or restrict it to localhost:

location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

12. Operating System and Process Hardening

The web server process itself should run with minimum privilege.

# Nginx / Apache should run as an unprivileged user, not root
# Nginx default: www-data or nginx
ps aux | grep nginx

# Ensure web root is not writable by the web server process
chown -R root:www-data /var/www/html
chmod -R 750 /var/www/html

# Web server should not have write access to its own config
chmod 644 /etc/nginx/nginx.conf
chown root:root /etc/nginx/nginx.conf

# Consider running in a chroot or container for additional isolation

Bind only to required network interfaces:

# Nginx โ€” if only serving on one interface
server {
    listen 192.0.2.1:443 ssl http2;   # Specific IP, not 0.0.0.0
}

Hardening Checklist

Control Priority Verify With
server_tokens off / ServerTokens Prod High curl -I https://example.com \| grep Server
X-Powered-By header removed Medium curl -I https://example.com \| grep -i powered
TLS 1.0 and 1.1 disabled Critical testssl.sh or SSL Labs
TLS 1.3 enabled High testssl.sh
Weak cipher suites disabled Critical testssl.sh
OCSP stapling enabled Medium openssl s_client -status
HTTP โ†’ HTTPS redirect (301) Critical curl -I http://example.com
HSTS header with โ‰ฅ1 year max-age High curl -I https://example.com \| grep -i strict
X-Content-Type-Options: nosniff High curl -I https://example.com
X-Frame-Options: DENY High curl -I https://example.com
Referrer-Policy set Medium curl -I https://example.com
Permissions-Policy set Medium curl -I https://example.com
CSP enforcing (not report-only) High curl -I https://example.com \| grep -i csp
TRACE method disabled High curl -X TRACE https://example.com -I
Directory listing disabled High Browse to a directory path
.env, .git, backup files blocked Critical curl https://example.com/.env โ†’ 404
client_max_body_size / LimitRequestBody set Medium Attempt large upload
Rate limiting on auth endpoints High ab or wrk against /login
Custom error pages (no stack traces) Medium Trigger a 404, 500
Logs structured and forwarded to SIEM High Verify log aggregation
Unused modules disabled Medium nginx -V / apache2 -M
Web server runs as unprivileged user High ps aux \| grep nginx
Web root not writable by server process High ls -la /var/www/html
HTTP/2 enabled Medium curl --http2 -I https://example.com

References

The Security Architecture Site โ€” for internal reference use. Back to contents