Apache httpd 2.4 hardening checklist with working config
Hardening Apache means switching off what you do not use, denying access by default, and putting limits on what clients can send. None of it replaces patching, but it reduces how many bugs can reach you and how much damage a mistake can do. Each step below is valid httpd 2.4 configuration and shows how to check that it worked.
Where to put the settings
Keep your hardening in one file of its own so package updates do not overwrite it:
- Debian, Ubuntu:
/etc/apache2/conf-available/hardening.conf, thensudo a2enconf hardening. Note that Debian already shipsconf-available/security.conf(enabled by default) withServerTokens OS,ServerSignature OnandTraceEnable Off. Edit that file instead, or make sure your file loads after it. Files inconf-enabledload in alphabetical order, sohardening.confloads beforesecurity.conf. You can name yourszz-hardening.conf. - RHEL, AlmaLinux, Rocky, Fedora:
/etc/httpd/conf.d/hardening.conf. Every*.conffile there is read automatically.
After every change, test the configuration and reload:
# Debian, Ubuntu
sudo apache2ctl -t && sudo systemctl reload apache2
# RHEL family, Fedora
sudo apachectl configtest && sudo systemctl reload httpd
Hide version details
ServerTokens Prod
ServerSignature Off
The default ServerTokens Full sends the exact version, the OS type and compiled-in modules. The documentation's example is Server: Apache/2.4.2 (Unix) PHP/4.2.2 MyMod/1.2. Prod reduces it to Server: Apache. ServerSignature Off (the upstream default) removes the version footer from error pages and directory listings. ServerTokens works only in the main server config, not per virtual host.
Be honest with yourself about what this buys. The httpd documentation says plainly that hiding the Server header "does nothing at all to make your server more secure". It only stops casual scanners from listing your exact version. It is still worth doing, because many audit tools flag it.
Turn off TRACE
TraceEnable Off
With this setting, TRACE requests get 405 Method Not Allowed from both the core and mod_proxy. The documentation notes that TRACE does not expose a vulnerability in httpd itself. Still, ordinary websites never need TRACE, and security scanners report it, so switching it off costs nothing. You cannot block TRACE with <Limit> or mod_allowmethods. TraceEnable is the only switch.
Load only the modules you use
Many recent CVEs are in optional modules such as mod_dav, mod_lua, mod_sed, mod_proxy_ftp, mod_proxy_ajp and mod_auth_digest (see Vulnerabilities (CVEs)). A module that is not loaded cannot be attacked. List what is loaded:
sudo apachectl -M | sort
Disable modules you do not need:
# Debian, Ubuntu (example: no status page, no user dirs, no listings)
sudo a2dismod status userdir
sudo a2dismod -f autoindex
sudo apache2ctl -t && sudo systemctl restart apache2
# RHEL family: comment out the LoadModule lines in
# /etc/httpd/conf.modules.d/*.conf, for example in 00-dav.conf:
#LoadModule dav_module modules/mod_dav.so
sudo apachectl configtest && sudo systemctl restart httpd
Debian's a2dismod marks autoindex as "essential" and asks you to type a confirmation phrase. The -f option skips that prompt. Use a restart, not a reload, after removing modules. Common candidates to remove on a plain website are autoindex, cgi/cgid, dav/dav_fs, include, info, lua, proxy_ftp, userdir and status. If you need status, keep it and restrict it as shown in Server status.
Deny by default
Apache serves any file it can map to a URL. Start by denying the whole filesystem, then allow only your web roots:
<Directory "/">
Options None
AllowOverride None
Require all denied
</Directory>
<Directory "/var/www/example">
Options -Indexes -Includes -ExecCGI +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Hidden files such as .env and .htpasswd, but keep /.well-known/ working
<FilesMatch "^\.(?!well-known)">
Require all denied
</FilesMatch>
# Version-control folders that were deployed by mistake
<DirectoryMatch "/\.(git|svn|hg)(/|$)">
Require all denied
</DirectoryMatch>
# Never run scripts from the upload folder
<Directory "/var/www/example/uploads">
<FilesMatch "\.(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
</Directory>
<IfModule mod_userdir.c>
UserDir disabled root
</IfModule>
-Indexesstops directory listings,-Includesturns off server-side includes, and-ExecCGIstops CGI outside ScriptAlias folders. Every option has a + or − sign. Mixing signed and unsigned options in one line is rejected at startup.AllowOverride Nonemeans .htaccess files are ignored, which is both faster and safer. The default has been None since 2.3.9. Turn it on only for the directories that need it (see .htaccess files).- The
FilesMatchrule matches file names only, so it does not cover files inside a.gitfolder (their names are things likeconfig). That is why the separateDirectoryMatchblock is needed. UserDir disabled rootis recommended by the httpd security tips. It is wrapped in<IfModule>because the directive is a syntax error when mod_userdir is not loaded.
Watch out for <Location>. <Location> sections are merged after <Directory> and <Files>. A Require all granted inside <Location "/"> overrides every deny above, including the upload rule. In a test on httpd 2.4.69, adding such a block made /uploads/x.php, /.env and /.git/config return 200 instead of 403. Grant access in <Directory> blocks only.
Check the result:
curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/.env # 403
curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/.git/config # 403
curl -s -o /dev/null -w '%{http_code}\n' -X TRACE https://www.example.com/ # 405
ETags: nothing to fix on 2.4
Old audit reports said to set FileETag MTime Size because ETags revealed file inode numbers. In 2.4 that is already the default. The inode was dropped after 2.3.14. Check only that no old configuration sets FileETag All or FileETag INode:
sudo grep -Rni 'FileETag' /etc/apache2 /etc/httpd 2>/dev/null
Security headers
These response headers tell browsers to refuse some attacks. They need mod_headers (sudo a2enmod headers on Debian and Ubuntu). On Fedora and RHEL it is loaded by default from conf.modules.d/00-base.conf.
<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=()"
Header always set X-Frame-Options "SAMEORIGIN"
# Start in report-only mode, switch to Content-Security-Policy when clean
Header always set Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>
- X-Content-Type-Options: nosniff
- The browser must trust your Content-Type and not guess, so an uploaded "image" cannot run as a script.
- Referrer-Policy
strict-origin-when-cross-originis the browser default today. Setting it explicitly protects older browsers and documents your choice.- Permissions-Policy
- Turns off browser features your site does not use. Browser support varies.
- Content-Security-Policy
- The strongest defense against cross-site scripting, and the easiest to break a site with. Run the
-Report-Onlyversion first, watch the browser console, then rename it. Itsframe-ancestorsdirective is the modern replacement for X-Frame-Options. Keeping both does no harm.
Send HSTS (Strict-Transport-Security) only from your HTTPS virtual host, once HTTPS works everywhere. See HTTPS and TLS.
If PHP-FPM or a proxied app already sends one of these headers, always set can produce a duplicate. The mod_headers documentation gives this pattern to avoid it:
Header onsuccess unset X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
Check with curl -sI https://www.example.com/.
Limits and timeouts
LimitRequestBody 10485760
RequestReadTimeout handshake=5 header=10-20,MinRate=500 body=20,MinRate=500
Timeout 30
KeepAliveTimeout 5
<Location "/upload">
LimitRequestBody 104857600
</Location>
- LimitRequestBody caps request bodies. Since 2.4.54 the default is 1,073,741,824 bytes (1 GiB). In 2.4.53 and earlier it was unlimited. 10,485,760 bytes is 10 × 1,024 × 1,024 = 10 MiB, plenty for forms. Raise it only where uploads happen (100 MiB above). PHP has its own limits as well.
- RequestReadTimeout (mod_reqtimeout) closes connections that send the TLS handshake, headers or body too slowly. The default is
handshake=0 header=20-40,MinRate=500 body=20,MinRate=500, so slow-handshake protection is off unless you set it. More in DDoS and bad bots. - Timeout defaults to 60 seconds. The security tips suggest lowering it on sites that get attacked, but not below what your slowest legitimate script or proxied backend needs.
- Leave
LimitRequestFields(100),LimitRequestFieldSize(8190) andLimitRequestLine(8190) at their defaults unless something breaks. Raising the field size was one precondition of CVE-2026-63292.
Users and file permissions
Apache starts as root to bind ports 80 and 443, then serves requests as an unprivileged user: www-data on Debian and Ubuntu (set through /etc/apache2/envvars), apache on RHEL and Fedora (User and Group in httpd.conf). Check:
ps -o user,pid,cmd -C apache2,httpd
One process runs as root and the rest as the service user. Then follow these rules:
- Config, binaries and log directories are owned by root and writable only by root. A log directory writable by the web user lets an attacker plant symlinks that root later writes through.
- Web content is owned by a deploy user, not by
www-data/apache. The server needs to read it, not change it. - Only directories the application really writes to (uploads, cache) are writable by the service user, and scripts are denied there.
- Secrets such as
.envandwp-config.phpare mode 640 or stricter, and ideally outside the document root.
sudo chown -R deploy:www-data /var/www/example
sudo find /var/www/example -type d -exec chmod 750 {} +
sudo find /var/www/example -type f -exec chmod 640 {} +
sudo chown -R www-data:www-data /var/www/example/uploads # apache:apache on RHEL
SELinux and AppArmor
On RHEL, AlmaLinux, Rocky and Fedora, SELinux confines httpd by default. Keep it enforcing (getenforce should print Enforcing), and give files the right labels instead of switching it off. Content is labeled httpd_sys_content_t. Folders the server may write to need httpd_sys_rw_content_t:
# Serve a non-standard web root with the same labels as /var/www
sudo semanage fcontext -a -e /var/www /srv/www
sudo restorecon -Rv /srv/www
# Let the app write only to its upload folder
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/srv/www/example/uploads(/.*)?"
sudo restorecon -Rv /srv/www/example/uploads
# Allow outgoing connections only if the app or a proxy really needs them
sudo setsebool -P httpd_can_network_connect 1
The httpd_selinux(8) man page lists every type and boolean. On Debian and Ubuntu the usual mandatory access control system is AppArmor. Run sudo aa-status to see whether any profile covers apache2. If none does, file permissions are your main barrier.
Quick checklist
- Security updates installed and service restarted (patching)
ServerTokens Prod,ServerSignature Off,TraceEnable Off- Unused modules disabled (
apachectl -Mreviewed) <Directory "/">denied; listings, SSI and CGI off;AllowOverride None- Hidden files, VCS folders and scripts in upload folders denied
- Security headers sent; CSP tested in report-only mode first
- LimitRequestBody and RequestReadTimeout set
- Content not writable by the service user; SELinux enforcing
- HTTPS with HSTS (HTTPS and TLS); optionally a WAF (ModSecurity WAF)
Sources
- Security Tips, Apache HTTP Server 2.4 documentation
- Apache core features (ServerTokens, ServerSignature, TraceEnable, Options, FileETag, LimitRequestBody, Timeout), httpd 2.4 documentation
- mod_reqtimeout, Apache HTTP Server 2.4 documentation
- mod_headers, Apache HTTP Server 2.4 documentation
- mod_allowmethods, Apache HTTP Server 2.4 documentation
- mod_unixd (User, Group), Apache HTTP Server 2.4 documentation
- Debian apache2 2.4.69-1 security.conf, sources.debian.org
- Content-Security-Policy header, MDN
- X-Content-Type-Options header, MDN
- Referrer-Policy header, MDN
- Permissions-Policy header, MDN
- X-Frame-Options header, MDN
- Configuring SELinux for applications and services with non-standard configurations, Red Hat
- httpd_selinux(8) man page
- httpd package 00-base.conf, Fedora package sources