Guides
fail2ban for Gitea, Forgejo and GitLab
Self-hosted Git servers are a favorite target for password spraying. Gitea and Forgejo log every failed login in one consistent line, and GitLab has a stock fail2ban filter. This guide covers both, including Docker setups where the usual ban action doesn't reach the container.
To protect Gitea or Forgejo with fail2ban, enable file logging in app.ini and create a filter that matches Failed authentication attempt for <user> from <IP>. Then add a jail that reads gitea.log and bans ports 80, 443 and your Git SSH port. Forgejo logs the same message. For GitLab, enable the stock [gitlab] jail, which reads /var/log/gitlab/gitlab-rails/application.log.
What Gitea and Forgejo log#
Gitea and Forgejo write a warning for every failed web login and every failed connection to their built-in SSH server. From the current source (routers/web/auth/auth.go and modules/ssh/ssh.go), with the default log format:
2026/10/25 18:36:10 routers/web/auth/auth.go:316:SignInPost() [W] Failed authentication attempt for admin from 203.0.113.42:51234: user does not exist [uid: 0, name: admin]
2026/10/25 18:36:14 modules/ssh/ssh.go:249:sshConnectionFailed() [W] Failed authentication attempt from 198.51.100.7:40122The address comes with a source port (IP:port, or [IPv6]:port). The filter has to allow for that. The SSH line from sshConnectionFailed() is written only after a connection has completely failed authentication, so a client cycling through its keys doesn't trip it.
Step 1: make Gitea log to a file#
Gitea's default [log] mode is console, which goes to the journal or docker logs. Add a file:
[log]
MODE = console, file
LEVEL = Info
ROOT_PATH = /var/lib/gitea/logRestart Gitea. Failed logins now also appear in /var/lib/gitea/log/gitea.log. LEVEL must be Info or Warn. At Error the [W] lines are dropped. For Forgejo the file is usually /etc/forgejo/app.ini with logs under /var/lib/forgejo/log/. Check ROOT_PATH in your config.
If Gitea sits behind a reverse proxy, it must trust the proxy's X-Forwarded-For header. The defaults trust only loopback (127.0.0.0/8,::1/128). A proxy in another container or on another host needs its network added, or every failure is logged with the proxy's address:
[security]
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.0/8,::1/128,172.16.0.0/12Banning the proxy's address would lock everyone out. If it's in ignoreip, nobody gets banned. See fail2ban behind a reverse proxy.
Step 2: the filter#
[Definition]
failregex = ^\s*\S+:\d+:\S+\(\) \[[WE]\] Failed authentication attempt (?:for <F-USER>.+?</F-USER> )?from <ADDR>(?::\d+)?(?::|\s*$)
ignoreregex =This matches the web line (with user) and the SSH line (without), for IPv4 and bracketed IPv6. It's anchored on Gitea's file:line:function() [W] prefix, so a username that contains the text "from 1.2.3.4" can't inject a fake address. Test it against your log:
sudo fail2ban-regex /var/lib/gitea/log/gitea.log /etc/fail2ban/filter.d/gitea.localPaste a line into the regex tester if it doesn't match, for example when you changed Gitea's log FLAGS. Writing filters from scratch is covered in custom filters.
Step 3: the jail#
[gitea]
enabled = true
filter = gitea
backend = auto
logpath = /var/lib/gitea/log/gitea.log
port = http,https,2222
maxretry = 5
findtime = 1h
bantime = 1hSet port to what clients connect to: 80/443 on the reverse proxy, or 3000 if Gitea is exposed directly, plus the built-in SSH port if you use it. backend = auto is there because Ubuntu 24.04's defaults-debian.conf sets backend = systemd for all jails, and this jail reads a file.
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status giteaGitea in Docker#
Two things change when Gitea runs in a container.
The log path. In the official image, Gitea's data lives in /data and the log root is /data/gitea/log. Turn on file logging with an environment variable and read the file through the volume on the host:
services:
gitea:
image: gitea/gitea:latest
environment:
- GITEA__log__MODE=console,file
- GITEA__log__ROOT_PATH=/data/gitea/log
volumes:
- ./gitea:/data
ports:
- "3000:3000"
- "2222:22"With ./gitea:/data the host path is ./gitea/gitea/log/gitea.log relative to the compose directory. Use its absolute path in logpath.
The ban action. Traffic to a published Docker port is DNATed and goes through the FORWARD path, not INPUT. The default nftables action hooks input, so its bans never touch the container. Ban in Docker's DOCKER-USER chain instead:
[gitea]
enabled = true
filter = gitea
backend = auto
logpath = /srv/gitea/gitea/log/gitea.log
banaction = iptables-multiport
chain = DOCKER-USER
port = 3000,22The ports here are the container ports (3000 and 22), because the destination port has already been rewritten when the packet reaches DOCKER-USER. If a reverse proxy on the host terminates HTTPS and forwards to the container, the proxy is the one receiving connections. The normal input ban on 80/443 then works, and you don't need DOCKER-USER. The Docker guide covers this in depth.
SSH: built-in server vs OpenSSH#
- Gitea's built-in SSH server (
START_SSH_SERVER = true, or SSH inside the Docker image) logs failures togitea.log, and the filter above catches them. Include its port in the jail. - OpenSSH passthrough (the
gituser on the host's sshd): failures are logged by sshd, not Gitea. The stock sshd jail already covers them.
Git clients with many keys loaded can produce warnings before the right key is accepted. The sshConnectionFailed() line is written only when all attempts failed, so it's safe to count.
GitLab: the stock gitlab filter#
fail2ban ships a [gitlab] jail and a gitlab filter that match GitLab's Failed Login message in application.log:
2026-10-25T18:36:10.667Z: Failed Login: username=admin ip=203.0.113.42[gitlab]
enabled = true
backend = auto
port = http,https
logpath = /var/log/gitlab/gitlab-rails/application.logGitLab marked application.log as deprecated in 15.10. The structured application_json.log carries the same events. fail2ban 1.1.1's filter reads only the plain-text file. JSON support (logtype = json) is in the development branch after 1.1.1. Until your version has it, use this local filter for the JSON log. It's the upstream rule:
[Definition]
_groupre = (?:\s*"(?!(?:time|meta\.remote_ip|message)\b)\w[^"]+"\s*:\s*(?:"[^"]+"|\w+)\s*[,\}])
failregex = ^%(_groupre)s*"meta\.remote_ip":"<ADDR>"\s*,\s*%(_groupre)s*"message":"Failed Login: username=<F-USER>(?:\S*|[^"]*?)(?= ip=)</F-USER>
datepattern = ^\{%(_groupre)s*"time":"%%Y-%%m-%%dT%%H:%%M:%%S\.%%f%%z"\s*,\s*
ignoreregex =[gitlab]
enabled = true
filter = gitlab-json
backend = auto
port = http,https
logpath = /var/log/gitlab/gitlab-rails/application_json.logIt matches lines like {"severity":"INFO","time":"2026-10-25T18:36:10.979Z",...,"meta.remote_ip":"203.0.113.42",...,"message":"Failed Login: username=admin ip=203.0.113.42"}. For GitLab in Docker, the logs are wherever you mounted /var/log/gitlab, for example /srv/gitlab/logs/gitlab-rails/, and the DOCKER-USER advice above applies.
GitLab also has a built-in failed-authentication ban for Git over HTTP and rate limits in the admin area. fail2ban adds a firewall-level ban for the web sign-in page on top.
The gitlab filter reference#
The built-in filters this guide uses, exactly as fail2ban 1.1.1 ships them — with the log lines they match and a link to test them. All other built-in filters are in the filter library.
gitlab GitLab
The gitlab filter bans failed sign-ins to a self-hosted GitLab (application or JSON log). File: /etc/fail2ban/filter.d/gitlab.conf; jail [gitlab]; log on Debian/Ubuntu: /var/log/gitlab/gitlab-rails/application.log.
Log lines it bans (from fail2ban's test suite):
2020-04-09T14:04:00.667Z: Failed Login: username=admin ip=80.10.11.122020-04-09T14:15:09.344Z: Failed Login: username=user name ip=80.10.11.12
Show the failregex fail2ban compiles (1 line)
^: Failed Login: username=<F-USER>.+</F-USER> ip=<HOST>$Test gitlab in the browser or on the server: sudo fail2ban-regex /var/log/gitlab/gitlab-rails/application.log gitlab · source
FAQ#
Does Gitea have a fail2ban filter?#
fail2ban doesn't ship one, but Gitea logs a consistent line: Failed authentication attempt for <user> from <IP>:<port>. Enable file logging ([log] MODE = console, file), create /etc/fail2ban/filter.d/gitea.local with a regex for that line, and add a jail that reads gitea.log.
How do I use fail2ban with Gitea in Docker?#
Set GITEA__log__MODE=console,file so the container writes /data/gitea/log/gitea.log, and read it through the volume on the host. Ban with banaction = iptables-multiport and chain = DOCKER-USER on the container ports (3000, 22), because published-port traffic bypasses the INPUT chain.
Does the Gitea filter work for Forgejo?#
Yes. Forgejo is a Gitea fork and logs the same Failed authentication attempt for <user> from <IP> message for web logins and Failed authentication attempt from <IP> for its SSH server. Use the same filter and point logpath at Forgejo's log directory, often /var/lib/forgejo/log/.
Which GitLab log does the fail2ban gitlab filter read?#
In fail2ban 1.1.1, /var/log/gitlab/gitlab-rails/application.log, matching Failed Login: username=... ip=.... GitLab deprecated that file in 15.10. For application_json.log, use a local JSON filter (shown above) until your fail2ban includes the logtype = json option.
Why does fail2ban ban 127.0.0.1 or my proxy for Gitea?#
Gitea is logging the reverse proxy's address instead of the client's. Add the proxy's network to REVERSE_PROXY_TRUSTED_PROXIES (default: loopback only) with REVERSE_PROXY_LIMIT = 1 in app.ini so Gitea reads X-Forwarded-For, then check that new failure lines show real client IPs.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.