Apache httpd 2.4 hardening checklist with working config

Updated November 2026

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:

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>

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-origin is 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-Only version first, watch the browser console, then rename it. Its frame-ancestors directive 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>

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:

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

Sources