/.git/config Web access log Triage: medium

GET /.git/config Requests in Your Logs: Scanners Hunting Exposed Repos

Published

An automated check for a .git directory left inside the web root, which hands a scanner the repository's configuration, its history and ultimately the application's source code and committed secrets.

What it looks like

203.0.113.7 - - [18/Oct/2026:03:51:11 +0000] "GET /.git/config HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
198.51.100.42 - - [18/Oct/2026:03:51:12 +0000] "GET /.git/HEAD HTTP/1.1" 404 162 "-" "python-requests/2.31.0"
192.0.2.88 - - [18/Oct/2026:04:02:44 +0000] "GET /static../.git/config HTTP/1.1" 404 162 "-" "Go-http-client/2.0"

On our own server, the 30 days to 18 October 2026

1,343

requests · about 45 a day

124

source addresses

Most requested variantsRequests
/.git/config246
/.git/HEAD85
/.git/index43
/api/.git/config36
/static../.git/config35
/media../.git/config34
/assets../.git/config29
/public/.git/config29

LogTriage's production API server (Hetzner, Germany): port 22 open to the internet, key-only since 6 October 2026 (password and keyboard-interactive authentication off); nginx in front of the API on ports 80 and 443. Paths as requested, query strings dropped and identifiers redacted; counted weekly from the nginx access log. More in the monthly attack report.

What the request is

Git keeps everything it knows about a repository in a .git directory at the project root. A scanner asking for /.git/config is testing whether that directory was deployed along with the application and whether the web server will hand its files back.

The files in the probe list each answer a different question. config is the per-repository configuration: remote URLs, branch tracking, and any options set locally — and when a remote was added with credentials embedded in the URL, they sit in this file in plain text. HEAD is a one-line pointer to the current branch, so a readable HEAD proves the directory exists even if nothing else is listable. index is the binary staging area, which enumerates tracked file paths and their object hashes. logs/HEAD is the reflog: commit hashes, author names and email addresses, timestamps and messages.

The real prize is not any single file. Once index or the ref files are readable, the object store under .git/objects/ can be walked and the working tree rebuilt offline — full application source, plus whatever was ever committed to it: configuration files, test fixtures, private keys, and credentials that were removed in a later commit but remain in history.

The directory ends up exposed in a predictable way: the site is deployed by cloning the repository into the document root rather than exporting files into it, so .git becomes an ordinary subdirectory of the web root. The /api/ and /public/ variants cover applications mounted one level down, and the static.., assets.. and media.. variants probe for an nginx alias that can be escaped upward.

Who sends it

This is undirected, internet-wide scanning: mass scanners sweeping address space, botnets that feed any recovered source or keys into further abuse, and research crawlers cataloguing exposures. A request for /.git/config says nothing about your organisation having been selected.

The user agent is weak evidence. Some clients announce an HTTP library such as python-requests or Go-http-client; many send a current Chrome string. The shape is what identifies the traffic: a handful of requests from one address within seconds, no Referer, no accompanying CSS, JavaScript or image requests, and a move on to other well-known paths — /.env, backup archives, admin panels — as soon as the first 404 comes back.

Is your server exposed?

Check what your own server already answered, then check the live path from the host:

# Every .git path in the log, with the status it received
awk '$7 ~ /\.git/ {print $9, $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

# Anything that was NOT 404/403 — this is the list that matters
awk '$7 ~ /\.git/ && $9 !~ /^(404|403)$/ {print}' /var/log/nginx/access.log

# Ask your own server, from your own server
for p in /.git/config /.git/HEAD /.git/index /static../.git/config; do
  printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1$p")" "$p"
done

# Does a .git directory sit inside a document root at all?
find /var/www /srv -maxdepth 4 -name '.git' -type d 2>/dev/null

Run the curl loop against every vhost name you serve, not just the default one — a rule added to the main site and forgotten on a staging or admin vhost is the usual reason these files stay reachable. Give rotated and compressed logs the same awk pass, since an exposure may already have been closed.

How to block it

The durable fix is deployment: build or export the application into the document root so no .git directory exists there, and point root at the application’s public directory rather than the project directory. Add a server-side deny as a second layer, in every server block:

# ACME and other well-known URIs must stay reachable
location ^~ /.well-known/ {
    allow all;
}

# Everything else beginning with a dot, .git included: not found, not logged
location ~ /\. {
    access_log off;
    log_not_found off;
    return 404;
}

Regular-expression locations are evaluated before prefix locations unless the matching prefix location uses ^~, so this rule answers /.git/config and /static../.git/config alike while /.well-known/acme-challenge/... keeps working. Returning 404 instead of 403 also tells the scanner nothing about which paths exist.

While you are in the configuration, give every alias directive and its location matching trailing slashes, so a prefix such as /static cannot be walked upward into the project root.

If any .git path was ever answered with a 200, the block is the start of the work, not the end: assume the repository and its full history are public and rotate accordingly.

Probes that arrive with it

Detect it

MITRE ATT&CK: T1595.003T1552.001

Frequently Asked Questions

Every one of these gets a 404 from us — is there anything left to do?
Not as an incident, because a 404 discloses nothing and the scanning will not stop. The useful work is preventive: confirm no .git directory sits under any document root on any vhost, confirm the deny rule exists in staging and admin server blocks too, and alert on non-404 responses to .git paths rather than on the requests themselves.
Which response codes should actually wake someone up?
Any 200 or 206 for a path under .git is a source-code and secrets disclosure until proven otherwise. A 403 is acceptable if it comes from an explicit deny rather than from a filesystem permission error, and a 301 or 302 deserves one look to see where it sends the client and whether the redirect target serves the file.
Why do scanners ask for paths like /static../.git/config?
That shape targets an nginx alias mapping where the location has no trailing slash, so a request such as /static../ resolves on disk one level above the aliased directory and can reach the project root. The scanner is testing the misconfiguration blind, which is why it tries several prefix names like static, assets and media in one burst.
Our .git directory really was reachable. What is the clean-up?
Treat the entire repository as public, not just the files currently checked out. Rotate every credential that ever appeared in a commit, in .git/config remote URLs or in configuration committed by mistake, then grep the access log for which .git paths returned 200 and to which addresses, remove the directory from the document root, and change the deployment so it exports files instead of cloning into the web root.

See what else is probing your server

Drop an nginx or Apache access log into the free browser analyzer: secret-file sweeps, exploit probes, scanners and credential stuffing are grouped by source with the evidence lines, and every known probe links back here. Nothing is uploaded.