A simple bot protection for self-hosted Forgejo instances https://sitegui.dev/post/2026/05/the-bots-came-to-hunt-my-forge-data
Find a file
2026-05-29 09:00:04 +02:00
src Allow git integration 2026-05-28 21:58:27 +02:00
.gitignore Initial commit 2026-05-28 19:42:25 +02:00
Cargo.lock Rename _ to - 2026-05-28 21:32:29 +02:00
Cargo.toml Rename _ to - 2026-05-28 21:32:29 +02:00
default.env Rename _ to - 2026-05-28 21:32:29 +02:00
deploy.sh Rename _ to - 2026-05-28 21:32:29 +02:00
Dockerfile Rename _ to - 2026-05-28 21:32:29 +02:00
example.png Update README 2026-05-29 09:00:04 +02:00
LICENSE Initial commit 2026-05-28 19:42:25 +02:00
README.md Update README 2026-05-29 09:00:04 +02:00
rust-toolchain Initial commit 2026-05-28 19:42:25 +02:00

Forgejo Shield

A simple bot protection for self-hosted Forgejo instances. Readme more about it on this blog post.

In collaboration with the reverse proxy Caddy, every request to Forgejo will first be sent to this "forgejo-shield". It can then either let it through or shield it by presenting a button that a user has to click to set a cookie.

Note that forgejo-shield does not receive the body of the original request, it only acts as a bouncer: either allowing the request to go as normal by answering "200 OK" or providing a replacement answer.

Requests that pass through

Some requests are always allowed to continue their normal journey:

  • static assets at /assets/*
  • at most two levels deep, like /, /foo and /foo/bar
  • git endpoints, like /foo/bar.git/baz/boom

The flow is described below:

sequenceDiagram
    Client ->>+ Caddy: GET /foo
    Caddy ->>+ Shield: GET /<br>X-Forwarded-Method: GET<br>X-Forwarded-Uri: /foo
    Shield -->>- Caddy: 200 OK
    Caddy ->>+ Forgejo: GET /foo
    Forgejo -->>- Caddy: response
    Caddy -->>- Client: response

Requests that are shielded

Anything else will require the right cookie to be present. When not, the user will first receive a HTML form with a button to POST to another page, so that the cookie can be set.

sequenceDiagram
%% part 1
    Client ->>+ Caddy: GET /bar
    Caddy ->>+ Shield: GET /<br>X-Forwarded-Method: GET<br>X-Forwarded-Uri: /bar
    Shield -->>- Caddy: 403 Forbidden<br>HTML form to continue
    Caddy -->>- Client: response

the shield form

sequenceDiagram
%% part 2
    Client ->>+ Caddy: POST /__forgejo-shield/bar
    Caddy ->>+ Shield: GET /<br>X-Forwarded-Method: POST<br>X-Forwarded-Uri: /__forgejo-shield/bar
    Shield -->>- Caddy: 303 See Other<br>Set-Cookie: __FORGEJO-SHIELD=yada<br>Location: /bar
    Caddy -->>- Client: response
%% part 3
    Client ->>+ Caddy: GET /bar<br>Cookie: __FORGEJO-SHIELD=yada
    Caddy ->>+ Shield: GET /<br>X-Forwarded-Method: GET<br>X-Forwarded-Uri: /bar<br>Cookie: __FORGEJO-SHIELD=yada
    Shield -->>- Caddy: 200 OK
    Caddy ->>+ Forgejo: GET /bar
    Forgejo -->>- Caddy: response
    Caddy -->>- Client: response

The value of the cookie is randomly chosen at every startup.

Deploy

  1. build the container image with podman build . -t forgejo-shield
  2. run the container as "forgejo-shield" and make sure it's accessible from the Caddy container
  3. configure your Caddyfile with a forward_auth block, like this:
     git.sitegui.dev {
         forward_auth forgejo-shield:8080 {
             uri /
         }
    
         reverse_proxy forgejo:3000
     }
    

Deploy at sitegui.dev

Set git alias with:

git config alias.deploy '!git push && ssh -4 sitegui@ssh.sitegui.dev ./deploy sitegui/forgejo-shield'

then simply run git deploy to push and deploy.