How to block it
One rule, in the right file, then one command to prove it worked. Pick your server below. If you are not sure which you run, the Apache and nginx sections cover the large majority of small business hosting.
Before you start
Blocking is a guard. It is not the repair.
These rules stop the folder being served. They do not remove it from the server, and they do not un-publish anything that has already been read.
If the check found your repository exposed, do the blocking first because it is quick, then change every secret the history has ever held, then change the deploy so the folder never reaches the server again. In that order.
Apache
The right place is the virtual host or the main configuration, because a rule there cannot be overridden by anything in the site folder and costs nothing to evaluate. Put this inside the VirtualHost block and reload.
On Apache 2.4:
<DirectoryMatch "/\.git">
Require all denied
</DirectoryMatch>
<FilesMatch "^\.git">
Require all denied
</FilesMatch>
The second block matters. The first stops the directory being walked into, and the second covers the loose files that sometimes sit beside it, such as the ignore file and the attributes file.
If you only have .htaccess
On shared hosting you often cannot edit the virtual host. This goes in .htaccess at the top of the web root instead, and it works, with the caveat that it is only in force while AllowOverride permits it. A hosting change that turns overrides off silently unblocks the folder.
RedirectMatch 404 /\.git
<FilesMatch "^\.">
Require all denied
</FilesMatch>
The redirect line answers with a not found rather than a forbidden, which tells a scanner less than a refusal does.
On Apache 2.2, which is long out of support and still in use, the syntax is Order allow,deny and Deny from all instead of Require all denied. If that is what your server takes, the version of Apache is a larger finding than the repository.
nginx
Inside the server block. The ^~ prefix stops nginx looking at any later regular expression location, so a PHP handler further down cannot pick the request up again.
location ^~ /.git {
deny all;
return 404;
}
location ~ /\.(?!well-known).* {
deny all;
return 404;
}
The second block denies every dotfile except the well known directory, which has to stay reachable because certificate renewal and several other standards use it. Blocking that by accident is the classic way to break automatic certificate renewal three months later, when nobody connects the two events.
Test the configuration before you reload it:
sudo nginx -t && sudo systemctl reload nginx
Caddy
In the site block of your Caddyfile:
@forbidden {
path /.git /.git/*
}
respond @forbidden 404
Then caddy validate and reload.
LiteSpeed and OpenLiteSpeed
LiteSpeed reads Apache .htaccess files, so the Apache section above applies as written. OpenLiteSpeed does not read them by default: add a context for the path in the virtual host configuration and set access to denied, or turn on .htaccess support and use the Apache rules.
IIS
Request filtering handles this. In web.config at the root of the site:
<configuration>
<system.webServer>
<security>
<requestFiltering>
<hiddenSegments>
<add segment=".git" />
</hiddenSegments>
</requestFiltering>
</security>
</system.webServer>
</configuration>
A hidden segment is refused anywhere in the path rather than only at the root, which is what you want.
Cloudflare, and anything else in front
If your traffic goes through Cloudflare you can block this at the edge, which means the request never reaches your server at all. It is a good belt to go with the braces, and it is the fastest thing to put in place if you have found a problem and cannot get at the server configuration this minute.
Create a WAF custom rule with this expression, and set the action to block:
starts_with(http.request.uri.path, "/.git")
Do not treat this as the fix. An edge rule protects the site while traffic goes through the edge. Anyone who learns the origin address and asks it directly gets the folder as before, and origin addresses leak in mail headers, in old records and in certificate transparency logs. Fix it on the server as well.
The same reasoning applies to any content delivery network or reverse proxy in front of your site.
If you run a framework
Laravel, Symfony, Drupal and most modern frameworks ship a public or web directory, and the document root is meant to point at it rather than at the project. Get that right and the repository is not underneath the web root at all, which is a stronger position than any block rule, because there is nothing to block.
WordPress has no such directory and the whole installation is the web root, so a repository at the top of a WordPress site is inside the served area by construction. Use the server rules above and treat changing the deploy as the real answer.
If your site is a static build from a generator, publish the output directory and nothing else. The repository lives on your machine and in your hosting provider's build environment, and it never needs to exist on the web server.
Prove it
Do not assume the rule took effect.
Reloaded configuration, cached responses at an edge and a second virtual host answering the same name all produce a fix that looks applied and is not. Check it:
curl -sS -o /dev/null -w "%{http_code}\n" https://example.co.uk/.git/HEAD
You want 404 or 403. Anything else means the rule is not in force where you think it is.
Check these three as well, because a rule that catches one path and not its neighbours is common:
curl -sS -o /dev/null -w "%{http_code} config\n" https://example.co.uk/.git/config
curl -sS -o /dev/null -w "%{http_code} listing\n" https://example.co.uk/.git/
curl -sS -o /dev/null -w "%{http_code} refs\n" https://example.co.uk/.git/refs/heads/main
Then run the check on this site again, from outside your own network, so that anything cached on your machine or answered by an internal name does not flatter the result.
While you are in there
The same rules and the same reasoning cover the other things that end up in a web root by accident. Worth checking for at the same time, using the commands above with the path changed: an environment file, a database dump left after a migration, an archive of the site, an editor backup file, and a configuration file saved with an extension the server does not execute.
If you would rather have somebody go through the whole surface rather than one door at a time, Dalaric runs an authorised test of a single web application and writes the findings up in plain English. £595 plus VAT.
Short answers
Questions people actually ask
I added the rule and the file still comes back. Why?
Usually one of three things. The configuration was edited but not reloaded. Something in front of your server, a content delivery network or a cache, is still answering. Or a second virtual host is answering for that name and does not have the rule. Check from outside your own network, because a cached answer on your machine will flatter the result.
Is blocking it enough, or do I have to delete the folder?
Blocking stops the leak and it is the right first move because it takes a minute. It is a guard against a mistake rather than the repair. The repair is that the repository never reaches the web server, which is a change to how the site is deployed.
Will blocking dotfiles break my certificate renewal?
It will if you block them all. Automatic certificate renewal uses a well known directory that begins with a dot, so a blanket rule stops renewal working and you find out about three months later, when nobody connects the two events. Every example on this page excludes it deliberately.
My host will not let me edit the server configuration. What then?
Use the htaccess form on the fix page, which works on most shared hosting. It is in force only while the host permits overrides, so it is weaker than a rule in the virtual host, and a hosting change can silently switch it off. Check it again after any migration.
Should it answer 403 or 404?
Either is safe. A not found tells a scanner slightly less than a refusal does, because a refusal confirms something is there to refuse. The difference is small and it is not worth a second deploy.
I put a rule in Cloudflare. Am I done?
Not quite. An edge rule protects the site while traffic goes through the edge. Anybody who learns your origin address and asks it directly gets the folder as before, and origin addresses leak in mail headers, in old records and in certificate transparency logs. Do both.
More guides
Deploying without publishing the repository
Three ways to get code onto a server that cannot leak like this.
A password is in your history
Why changing it is the fix and rewriting history is not.
Undoing almost anything
One table, from unstaging a file to recovering a commit you deleted.