How to deny access to sensitive files in Apache

Blocking direct web access to sensitive files in Apache prevents common publishing mistakes such as leaving .env files, backup copies, database dumps, or repository metadata under the web root. A single downloadable secret or backup can expose credentials, source history, or internal data that makes the next stage of an attack much easier.

Sensitive-file protection in Apache 2.4 works best when it is attached to filesystem-aware containers. FilesMatch applies rules to matching filenames anywhere under the served tree, while DirectoryMatch applies them to matching directories and everything below them. That matters because Location and LocationMatch operate on URL space rather than the underlying filesystem path, so they are the wrong tool for protecting on-disk content such as .git directories or misplaced backups.

The steps below use the Debian or Ubuntu layout with a server-wide include under /etc/apache2/conf-available/, then enable it with a2enconf. Keep the deny patterns tight on sites that intentionally serve archives or database exports, always test syntax before reloading, and if a custom policy later blocks all dot-directories make sure ACME validation paths under /.well-known/acme-challenge/ still remain reachable when Let's Encrypt HTTP-01 validation is in use.

Steps to deny access to sensitive files in Apache:

  1. Identify the active Apache layout and default document root.
    $ sudo apache2ctl -S | sed -n '1,8p'
    VirtualHost configuration:
    *:80                   host.example.net (/etc/apache2/sites-enabled/000-default.conf:1)
    ServerRoot: "/etc/apache2"
    Main DocumentRoot: "/var/www/html"
    Main ErrorLog: "/var/log/apache2/error.log"
    ##### snipped #####

    On RHEL-family systems, the equivalent tree is usually /etc/httpd/ and the service name is httpd.

  2. Create a dedicated include file for the deny rules.
    $ sudo vi /etc/apache2/conf-available/deny-sensitive-files.conf

    A separate include keeps the security policy easy to audit and reuse instead of burying it inside unrelated virtual host changes.

  3. Add rules for hidden files, backup-style files, and version-control metadata directories.
    # Block hidden files such as .env, .htaccess, and .user.ini.
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>
    
    # Block common backup and dump files that should not be downloadable.
    <FilesMatch "(?i)(\.(bak|old|orig|save|swp|tmp|sql|sqlite|sqlite3|db)$|~$)">
        Require all denied
    </FilesMatch>
    
    # Block VCS metadata directories anywhere below the document root.
    <DirectoryMatch "(^|/)\.(git|svn|hg|bzr)(/|$)">
        Require all denied
    </DirectoryMatch>

    This split keeps the policy specific: FilesMatch covers matching filenames anywhere under the served tree, while DirectoryMatch blocks repository directories and everything inside them without relying on URL-only LocationMatch rules.

    Remove extensions from the second rule if the site intentionally serves those file types as downloads. Moving such files outside the web root is safer than weakening the deny rule globally.

    On RHEL-style systems, place the same block in /etc/httpd/conf.d/deny-sensitive-files.conf/ and skip the a2enconf step.

  4. Enable the include on Debian or Ubuntu.
    $ sudo a2enconf deny-sensitive-files
    Enabling conf deny-sensitive-files.
    To activate the new configuration, you need to run:
      service apache2 reload

    Files under /etc/httpd/conf.d/ are loaded automatically on RHEL-family systems, so there is no a2enconf step there.

  5. Test the Apache configuration syntax before reloading the service.
    $ sudo apache2ctl -t
    Syntax OK

    Use sudo apachectl -t or sudo httpd -t on platforms that ship those control names instead of apache2ctl.

  6. Reload Apache so the deny rules take effect.
    $ sudo service apache2 reload
     * Reloading Apache httpd web server apache2
     *

    On systemd hosts, sudo systemctl reload apache2 is the normal equivalent. The RHEL-family unit name is usually httpd.

  7. Request known-sensitive paths and confirm that Apache now returns 403 Forbidden.
    $ curl -I -H 'Host: host.example.net' http://127.0.0.1/.env
    HTTP/1.1 403 Forbidden
    Date: Wed, 08 Apr 2026 04:37:52 GMT
    Server: Apache/2.4.58 (Ubuntu)
    Content-Length: 274
    Content-Type: text/html; charset=iso-8859-1
    
    $ curl -I -H 'Host: host.example.net' http://127.0.0.1/.git/config
    HTTP/1.1 403 Forbidden
    Date: Wed, 08 Apr 2026 04:37:52 GMT
    Server: Apache/2.4.58 (Ubuntu)
    Content-Length: 274
    Content-Type: text/html; charset=iso-8859-1

    For name-based virtual hosts, keep the Host header pointed at the real site name so the request hits the intended vhost instead of the server default.

  8. If the site uses Let's Encrypt HTTP-01 validation, confirm that an ACME challenge file under /.well-known/acme-challenge/ is still reachable.
    $ curl -I -H 'Host: host.example.net' http://127.0.0.1/.well-known/acme-challenge/test-token
    HTTP/1.1 200 OK
    Date: Wed, 08 Apr 2026 04:37:52 GMT
    Server: Apache/2.4.58 (Ubuntu)
    Last-Modified: Wed, 08 Apr 2026 04:37:50 GMT
    ETag: "d-64eeb7659425a"

    The example rules above do not block ACME token files. This check matters only when the host actually uses HTTP-01 validation or when a broader dot-directory deny rule was added separately.

  9. Review the access log for denied requests so future scans are easy to spot.
    $ sudo tail -n 4 /var/log/apache2/access.log
    127.0.0.1 - - [08/Apr/2026:04:37:52 +0000] "GET /.env HTTP/1.1" 403 435 "-" "curl/8.5.0"
    127.0.0.1 - - [08/Apr/2026:04:37:52 +0000] "GET /.git/config HTTP/1.1" 403 435 "-" "curl/8.5.0"
    127.0.0.1 - - [08/Apr/2026:04:37:52 +0000] "GET /app.sql HTTP/1.1" 403 435 "-" "curl/8.5.0"
    127.0.0.1 - - [08/Apr/2026:04:37:52 +0000] "GET /.well-known/acme-challenge/test-token HTTP/1.1" 200 214 "-" "curl/8.5.0"

    If the site already uses a vhost-specific access log, review that file instead of the global /var/log/apache2/access.log path.