This guide provides comprehensive security hardening recommendations for Apache HTTP Server with actionable configuration examples.
Where possible, bind Apache to internal interfaces only:
# Listen on localhost only
Listen 127.0.0.1:80
Listen 127.0.0.1:443
# Or specific internal IP
Listen 192.168.1.10:80
Listen 192.168.1.10:443
Create a default virtual host to reject requests with unknown Host headers:
# Default catch-all for HTTP
<VirtualHost _default_:80>
ServerName unknown
ErrorLog /var/log/apache2/unknown-host-error.log
CustomLog /var/log/apache2/unknown-host-access.log combined
<Location />
Require all denied
</Location>
</VirtualHost>
# Default catch-all for HTTPS
<VirtualHost _default_:443>
ServerName unknown
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
ErrorLog /var/log/apache2/unknown-host-ssl-error.log
CustomLog /var/log/apache2/unknown-host-ssl-access.log combined
<Location />
Require all denied
</Location>
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile /etc/ssl/private/example.com.key
SSLCertificateChainFile /etc/ssl/certs/example.com-chain.crt
# Enable TLS 1.2 and TLS 1.3 only
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
# Modern cipher suite with forward secrecy
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# Server chooses cipher order
SSLHonorCipherOrder on
# Enable OCSP Stapling (improves TLS handshake performance)
SSLUseStapling on
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
</VirtualHost>
# Generate 4096-bit DH parameters (takes ~5-10 minutes)
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
Add to Apache config:
SSLDHParam /etc/ssl/certs/dhparam.pem
# Force HTTPS for 1 year with subdomains and preload
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
<VirtualHost *:80>
ServerName example.com
# Redirect all HTTP traffic to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
Prevent information disclosure by hiding Apache version and OS:
# Show only "Apache" without version or OS
ServerTokens Prod
# Disable server signature in error pages
ServerSignature Off
Verification:
curl -I https://example.com
# Should show: Server: Apache (no version)
Prevent directory enumeration attacks:
<Directory /var/www/html>
Options -Indexes
</Directory>
Alternative (disable mod_autoindex entirely):
# Debian/Ubuntu
sudo a2dismod autoindex
# RHEL/CentOS - comment out in httpd.conf
# LoadModule autoindex_module modules/mod_autoindex.so
Audit enabled modules:
# Debian/Ubuntu
apache2ctl -M
# RHEL/CentOS
httpd -M
Modules to consider disabling (if not needed):
# Debian/Ubuntu
sudo a2dismod status
sudo a2dismod userdir
sudo a2dismod cgi
sudo a2dismod include
sudo a2dismod dav
sudo a2dismod dav_fs
sudo a2dismod info
# RHEL/CentOS - comment out LoadModule lines
# LoadModule status_module modules/mod_status.so
# LoadModule userdir_module modules/mod_userdir.so
# LoadModule cgi_module modules/mod_cgid.so
# LoadModule include_module modules/mod_include.so
# LoadModule dav_module modules/mod_dav.so
# LoadModule info_module modules/mod_info.so
Remove default configurations:
# Debian/Ubuntu
sudo rm /etc/apache2/conf-enabled/welcome.conf
sudo rm /etc/apache2/conf-enabled/autoindex.conf
sudo rm /etc/apache2/conf-enabled/userdir.conf
# RHEL/CentOS
sudo rm /etc/httpd/conf.d/welcome.conf
sudo rm /etc/httpd/conf.d/autoindex.conf
sudo rm /etc/httpd/conf.d/userdir.conf
# Remove default Apache test pages
sudo rm -rf /var/www/html/index.html
sudo rm -rf /var/www/html/apache2*
# Remove default virtual host
sudo rm /etc/apache2/sites-enabled/000-default.conf
Deny access to the entire filesystem by default, then grant access to specific directories:
<Directory />
Require all denied
</Directory>
<Directory /var/www/html>
Require all granted
</Directory>
# Block .htaccess and .htpasswd files
<Files ".ht*">
Require all denied
</Files>
# Block common sensitive file patterns
<FilesMatch "^\.|~$|\.bak$|\.backup$|\.env$|\.config$|\.sql$|\.log$">
Require all denied
</FilesMatch>
# Block version control directories
<DirectoryMatch "/\.(git|svn|hg|bzr)">
Require all denied
</DirectoryMatch>
# Restrict admin directory to specific IPs
<Location "/admin">
Require ip 192.168.1.100 192.168.1.101
Require valid-user
</Location>
# Block access to development directories
<DirectoryMatch "^/var/www/(test|dev|staging)">
Require all denied
</DirectoryMatch>
# Restrict server-status to localhost only
<Location "/server-status">
Require ip 127.0.0.1 ::1
</Location>
# Restrict server-info (if mod_info is loaded)
<Location "/server-info">
Require ip 127.0.0.1 ::1
</Location>
# Disable TRACE method globally
TraceEnable Off
# Allow only necessary methods for main site
<Directory /var/www/html>
<LimitExcept GET POST HEAD OPTIONS>
Require all denied
</LimitExcept>
</Directory>
# API endpoint with specific methods
<Location "/api">
<LimitExcept GET POST PUT DELETE OPTIONS>
Require all denied
</LimitExcept>
</Location>
For better performance and security, disable .htaccess processing:
<Directory /var/www/html>
AllowOverride None
</Directory>
Add comprehensive security headers to all responses:
<IfModule mod_headers.c>
# Prevent clickjacking
Header always set X-Frame-Options "DENY"
# Prevent MIME-type sniffing
Header always set X-Content-Type-Options "nosniff"
# XSS protection (legacy, but still useful)
Header always set X-XSS-Protection "1; mode=block"
# Content Security Policy
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
# HTTP Strict Transport Security
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Referrer Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions Policy (formerly Feature-Policy)
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
# Remove ETag header (prevents information disclosure)
Header unset ETag
FileETag None
# Remove X-Powered-By header (if using PHP)
Header unset X-Powered-By
</IfModule>
Header Values Explained:
| Header | Recommended Value | Purpose |
|---|---|---|
X-Frame-Options |
DENY |
Prevents clickjacking attacks |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
X-XSS-Protection |
1; mode=block |
Enables browser XSS filter |
Content-Security-Policy |
See above | Controls resource loading |
Strict-Transport-Security |
max-age=31536000 |
Enforces HTTPS for 1 year |
Referrer-Policy |
strict-origin-when-cross-origin |
Controls referrer information |
Permissions-Policy |
See above | Disables browser features |
# Maximum simultaneous requests
MaxRequestWorkers 150
# Maximum requests per child process before respawn
MaxConnectionsPerChild 1000
# Timeout for requests (seconds)
Timeout 60
# Keep-alive settings
KeepAlive On
KeepAliveTimeout 5
MaxKeepAliveRequests 100
# Limit request body size (bytes) - 10MB example
LimitRequestBody 10485760
# Limit number of request headers
LimitRequestFields 100
# Limit size of each request header (bytes)
LimitRequestFieldSize 8190
# Limit request line length (bytes)
LimitRequestLine 8190
# Require client to send headers within 20-40 seconds
# Minimum rate of 500 bytes/second
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500
Install and configure mod_evasive:
# Debian/Ubuntu
sudo apt install libapache2-mod-evasive
# RHEL/CentOS
sudo yum install mod_evasive
Configuration:
<IfModule mod_evasive20.c>
DOSHashTableSize 3097
DOSPageCount 2
DOSSiteCount 50
DOSPageInterval 1
DOSSiteInterval 1
DOSBlockingPeriod 30
DOSLogDir "/var/log/apache2/mod_evasive"
DOSEmailNotify admin@example.com
</IfModule>
Install and enable mod_security:
# Debian/Ubuntu
sudo apt install libapache2-mod-security2
sudo a2enmod security2
# RHEL/CentOS
sudo yum install mod_security
Basic configuration:
<IfModule mod_security2.c>
# Enable mod_security
SecRuleEngine On
# Request body handling
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Response body handling
SecResponseBodyAccess Off
# Logging
SecDebugLog /var/log/apache2/modsec_debug.log
SecDebugLogLevel 0
SecAuditLog /var/log/apache2/modsec_audit.log
SecAuditLogType Serial
SecAuditLogParts ABIJDEFHZ
# Include OWASP Core Rule Set
IncludeOptional /etc/modsecurity/*.conf
</IfModule>
Apache should run as an unprivileged user:
# Debian/Ubuntu
User www-data
Group www-data
# RHEL/CentOS
User apache
Group apache
Create dedicated user if needed:
sudo useradd -r -s /usr/sbin/nologin apache-secure
sudo groupadd apache-secure
Ensure ServerRoot and configuration files are protected:
# Set ownership to root
sudo chown -R root:root /etc/apache2
sudo chown -R root:root /etc/httpd
# Set restrictive permissions
sudo chmod -R 755 /etc/apache2
sudo chmod -R 755 /etc/httpd
# Protect configuration files
sudo chmod 644 /etc/apache2/apache2.conf
sudo chmod 644 /etc/httpd/conf/httpd.conf
# Protect SSL certificates
sudo chmod 400 /etc/ssl/private/*.key
sudo chmod 400 /etc/ssl/certs/*.crt
sudo chown root:root /etc/ssl/private/*.key
sudo chown root:root /etc/ssl/certs/*.crt
# Set ownership
sudo chown -R www-data:www-data /var/www/html
# Or for RHEL
sudo chown -R apache:apache /var/www/html
# Set permissions (read-only for web server)
sudo chmod -R 755 /var/www/html
sudo find /var/www/html -type f -exec chmod 644 {} \;
sudo find /var/www/html -type d -exec chmod 755 {} \;
# Disable symlinks
sudo chattr +i /var/www/html # Optional: make immutable
<Directory /var/www/html>
Options -FollowSymLinks -SymLinksIfOwnerMatch
</Directory>
Enable comprehensive logging for security monitoring:
# Access log with custom format
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %T" combined
CustomLog /var/log/apache2/access.log combined
ErrorLog /var/log/apache2/error.log
# Set log level
LogLevel warn
# Separate logs for virtual hosts
<VirtualHost *:443>
ServerName example.com
ErrorLog /var/log/apache2/example.com-error.log
CustomLog /var/log/apache2/example.com-access.log combined
</VirtualHost>
Log rotation (/etc/logrotate.d/apache2):
/var/log/apache2/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 640 root adm
sharedscripts
postrotate
systemctl reload apache2 > /dev/null 2>&1 || true
endscript
}
Always test configuration before applying:
# Test configuration syntax
sudo apache2ctl configtest
# Or
sudo httpd -t
# Expected output: Syntax OK
Restart Apache to apply hardening configuration:
# Debian/Ubuntu
sudo systemctl restart apache2
sudo systemctl status apache2
# RHEL/CentOS
sudo systemctl restart httpd
sudo systemctl status httpd
# View logs for errors
sudo journalctl -u apache2 -f
# Or
sudo tail -f /var/log/apache2/error.log
After applying hardening, verify:
ServerTokens Prod, ServerSignature Off)Options -Indexes).ht*, .git, .env)TraceEnable Off)AllowOverride None)Verify your hardening with these tools:
| Tool | Purpose |
|---|---|
| SSL Labs | TLS/SSL configuration test |
| SecurityHeaders.com | Security headers analysis |
| Mozilla Observatory | Comprehensive security scan |
curl -I https://example.com |
Manual header inspection |
nmap --script http-enum -sV example.com |
Service enumeration test |
openssl s_client -connect example.com:443 |
TLS connection test |
Test security headers:
curl -I https://example.com
Expected output should include:
HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
content-security-policy: default-src 'self'
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
Test TLS configuration:
# Test TLS 1.2
openssl s_client -connect example.com:443 -tls1_2
# Test TLS 1.3
openssl s_client -connect example.com:443 -tls1_3
# Verify TLS 1.0/1.1 are disabled
openssl s_client -connect example.com:443 -tls1_1
# Should fail with "no protocols available"
Verify server tokens:
curl -I https://example.com | grep -i server
# Should show: Server: Apache (no version number)
Prefer automation? See Apache HTTP Server Ansible Setup for an example playbook.
Prefer containers? See Apache HTTP Server Docker Setup.
Any questions?
Feel free to contact us. Find all contact information on our contact page.