Hardening Apache reduces the chance that an exposed site leaks version details, serves unintended files, or accepts request types the application never meant to handle. A stronger baseline turns routine internet scanning into blocked requests instead of easy reconnaissance.

Most Apache hardening lives in a few places: global includes that control headers and protocol behavior, <VirtualHost> or <Directory> blocks that narrow what a site serves, and targeted probes that confirm the final response matches the intended policy. Security improves fastest when those controls are explicit instead of inherited accidentally from old snippets or forgotten .htaccess files.

Examples below use the Debian or Ubuntu layout with /etc/apache2/, apache2ctl, and the apache2 service name. On RHEL-family systems, the equivalent tree is usually /etc/httpd/ with the httpd unit name. Re-test each hardening layer after it is added, because Apache merges configuration sections and a broad request-space rule can unintentionally weaken a filesystem deny if it is placed too broadly.

Steps to secure Apache web server:

  1. Inventory the active Apache surface before changing it.
    $ sudo apache2ctl -M | sed -n '1,20p'
    Loaded Modules:
     core_module (static)
     so_module (static)
     watchdog_module (static)
     http_module (static)
     log_config_module (static)
     logio_module (static)
     version_module (static)
     unixd_module (static)
     access_compat_module (shared)
     alias_module (shared)
     auth_basic_module (shared)
     ##### snipped #####
    
    $ curl -sSI https://host.example.net/ | grep -iE '^(server|x-powered-by|x-frame-options|x-content-type-options|referrer-policy|content-security-policy):'
    server: Apache/2.4.58 (Ubuntu)

    Start by finding what the server actually exposes to clients, not only what the config is supposed to do. Modules, response headers, and vhost mapping usually reveal the quickest hardening wins.

    Use a2dismod on Debian or Ubuntu, or the platform module configuration on other systems, to remove modules the site does not need.

  2. Move the baseline hardening rules into a dedicated Apache include instead of scattering them across unrelated vhost files or .htaccess.
    $ sudoedit /etc/apache2/conf-available/security-hardening.conf
    $ sudo a2enconf security-hardening
    Enabling conf security-hardening.
    To activate the new configuration, you need to run:
      service apache2 reload
    # Reduce banner leakage.
    ServerTokens Prod
    ServerSignature Off
    
    # Block TRACE explicitly.
    TraceEnable Off
    
    # Send core browser-facing policies on normal and error responses.
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Content-Security-Policy "frame-ancestors 'self'"
    
    # Fail oversized requests quickly when the application does not need them.
    LimitRequestBody 1048576

    On RHEL-family systems, place the same block in /etc/httpd/conf.d/security-hardening.conf/ and skip the a2enconf step.

    ServerTokens Prod and ServerSignature Off reduce easy fingerprinting, but they do not replace patching or module cleanup.

    If the distro already loads a default snippet such as /etc/apache2/conf-enabled/security.conf, avoid leaving conflicting ServerTokens, ServerSignature, TraceEnable, or header directives active in both places. Apache uses the last directive it reads.

    If the application already sends Content-Security-Policy, merge frame-ancestors into the existing policy instead of emitting a second conflicting header.

  3. Keep per-directory overrides off unless a specific application still requires them.
    <Directory /var/www/html>
        AllowOverride None
    </Directory>

    AllowOverride None keeps security policy in the main Apache config, avoids per-request .htaccess filesystem lookups, and makes the effective rules easier to audit. If one path still needs overrides, scope them to that directory instead of reopening the whole document root.

  4. Deny unused methods, directory indexes, and common sensitive-file leaks in the same pass.
    <VirtualHost *:80>
        ServerName host.example.net
        DocumentRoot /var/www/html
     
        <Location "/">
            <LimitExcept GET POST>
                Require all denied
            </LimitExcept>
        </Location>
     
        <Directory /var/www/html/downloads>
            Options -Indexes
        </Directory>
    </VirtualHost>
     
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>
     
    <DirectoryMatch "(^|/)\.(git|svn|hg|bzr)(/|$)">
        Require all denied
    </DirectoryMatch>

    TRACE is controlled separately by TraceEnable Off, not by <LimitExcept>. Add OPTIONS when browser clients need CORS preflight, and keep methods such as PUT, PATCH, or DELETE only where the application really uses them.

    After adding a broad <Location> or vhost-wide method allowlist, re-test hidden-file and admin-endpoint rules. Apache merges configuration sections, so a request-space rule can unintentionally weaken a filesystem deny if it is placed too broadly.

  5. Force HTTPS, set an explicit TLS policy, and add HSTS only after external HTTPS behavior is stable.
    $ curl -I http://host.example.net/
    HTTP/1.1 301 Moved Permanently
    Location: https://host.example.net/
    
    $ openssl s_client -connect host.example.net:443 -servername host.example.net -tls1
    ##### snipped #####
    no protocols available
    # Inside the TLS virtual host or SSL include.
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLUseStapling On
    SSLStaplingCache "shmcb:/var/run/apache2/stapling_cache(128000)"

    Current mod_ssl still benefits from an explicit SSLProtocol policy when the goal is a hardened TLS 1.2/1.3-only edge rather than the broad default protocol set.

    Enable Strict-Transport-Security only after HTTPS redirects, certificate renewal, and subdomain coverage are confirmed stable, because browsers cache the policy.

  6. Keep administrative URLs and server-generated status pages blocked or allowlisted.
    $ curl -I -H 'Host: host.example.net' http://127.0.0.1/server-status
    HTTP/1.1 403 Forbidden

    Apply IP allowlists, authentication, or both to endpoints such as /server-status, /server-info, internal dashboards, and framework debug paths. If the site sits behind a reverse proxy, validate the effective client-IP handling before trusting an allowlist.

  7. Add abuse controls that fit the real workload instead of letting the default server accept every expensive request.

    Keep LimitRequestBody as small as the application allows, and use connection or request-rate controls for hostile clients or hotlinked assets. A hardened configuration should fail oversized or abusive requests quickly, not after they have consumed worker time or disk.

    When the site faces untrusted traffic with application-specific attack patterns, deploy ModSecurity or an upstream WAF in detection mode first and tune before blocking live traffic.

  8. Review the security-relevant logs before the next reload window.

    Access logs, audit logs, and rotation policy determine whether blocked methods, denied files, and repeated probes are still visible when an incident needs reconstruction. Keep the logging scope high enough to explain denials without flooding disk or hiding useful events in noise.

  9. Validate the final configuration from both Apache and the client side.
    $ sudo apache2ctl configtest
    Syntax OK
    
    $ sudo systemctl reload apache2
    
    $ curl -sSI https://host.example.net/ | grep -iE '^(server|x-frame-options|x-content-type-options|referrer-policy|content-security-policy):'
    Server: Apache
    X-Frame-Options: SAMEORIGIN
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    Content-Security-Policy: frame-ancestors 'self'
    
    $ curl -sS -D - -o /dev/null -X TRACE https://host.example.net/ | sed -n '1,6p'
    HTTP/1.1 405 Method Not Allowed
    Server: Apache
    X-Frame-Options: SAMEORIGIN
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    Content-Security-Policy: frame-ancestors 'self'
    
    $ curl -I https://host.example.net/.env
    HTTP/1.1 403 Forbidden

    Probe the public hostname, not only localhost, so the checks pass through the real TLS vhost, reverse proxies, and CDN layers. Add a request to a directory without an index file and any protected admin path that matters to the site.