feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access

Public repos are now browsable/clonable anonymously (HTTPS) and over SSH,
proxied through the Hetzner VPS. Unlike other public apps this is
deliberately not behind the oauth2-proxy/Zitadel gate, since Forgejo
already enforces its own per-repo visibility. Root redirects to the
.profile landing page. Documents the REQUIRE_SIGNIN_VIEW gotcha and the
Zitadel OIDC redirect_uri fix needed whenever FORGEJO_DOMAIN changes.
This commit is contained in:
Brian Pooe 2026-07-19 18:34:13 +02:00
parent 1c2f4a1c81
commit fef521909b
5 changed files with 126 additions and 6 deletions

View file

@ -173,10 +173,17 @@ koffan.{{CADDY_BASE_DOMAIN:-home.example.com}} {
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080 reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080
} }
# Forgejo (LXC 114, .18). Git-over-SSH (port 2222) is not proxyable through # Forgejo (LXC 114, .18) -- publicly exposed at git.brianpooe.com (proxied via
# Caddy — that port is exposed directly on the LXC for SSH clone/push. # the VPS over NetBird for outside visitors) as well as reachable directly on
git.{{CADDY_BASE_DOMAIN:-home.example.com}} { # the LAN via split-horizon DNS. One canonical hostname everywhere, so this
# is NOT under CADDY_BASE_DOMAIN like other internal-only apps.
# Git-over-SSH is not proxyable through Caddy -- forgejo.brianpooe.com
# resolves straight to the LXC (10.0.10.18) instead, bypassing Caddy entirely.
# Root redirects to the .profile landing page instead of the repo explore
# list -- see the Forgejo README's "Public Exposure" section.
git.brianpooe.com {
import common import common
redir / /brianpooe 302
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000 reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000
} }

View file

@ -16,6 +16,13 @@ FORGEJO_DOMAIN=git.home.example.com
# Hostname shown in the web UI's SSH clone URL. Must resolve directly to # Hostname shown in the web UI's SSH clone URL. Must resolve directly to
# this LXC (not through Caddy — SSH can't be proxied), so it needs its own # this LXC (not through Caddy — SSH can't be proxied), so it needs its own
# DNS A record rather than reusing FORGEJO_DOMAIN's wildcard CNAME to Caddy. # DNS A record rather than reusing FORGEJO_DOMAIN's wildcard CNAME to Caddy.
#
# If this instance is publicly exposed, FORGEJO_DOMAIN/FORGEJO_SSH_DOMAIN
# should be the same public-facing names used everywhere (LAN and internet),
# resolved via split-horizon DNS: an internal DNS server answers them
# directly for LAN clients (its own zone/record, not the wildcard CNAME
# above), public DNS points them at your public edge for everyone else. See
# the Forgejo README's "Public Exposure" section for the full pattern.
FORGEJO_SSH_DOMAIN=forgejo.home.example.com FORGEJO_SSH_DOMAIN=forgejo.home.example.com
# Generate with: openssl rand -base64 32 # Generate with: openssl rand -base64 32
@ -28,4 +35,13 @@ FORGEJO_DATA_ROOT=/mnt/unas/forgejo_data
# false = open self-signup; leave true and create accounts with # false = open self-signup; leave true and create accounts with
# `docker exec forgejo forgejo admin user create ...` instead. # `docker exec forgejo forgejo admin user create ...` instead.
FORGEJO_DISABLE_REGISTRATION=true FORGEJO_DISABLE_REGISTRATION=true
# If this instance is ever exposed publicly with the intent of letting
# outsiders browse public repos anonymously (GitHub-style), this MUST stay
# false. true forces every page -- including /explore and any .profile
# landing page -- through /user/login for anonymous visitors, regardless of
# FORGEJO_LANDING_PAGE. It silently defeats public repo browsing.
FORGEJO_REQUIRE_SIGNIN_VIEW=false FORGEJO_REQUIRE_SIGNIN_VIEW=false
# login = anonymous visitors land on the sign-in page (LAN-only/private use).
# explore = anonymous visitors land on the public repo listing, like GitHub's
# logged-out homepage. Use explore if this instance is publicly exposed.
FORGEJO_LANDING_PAGE=login

View file

@ -81,11 +81,73 @@ override), freeing 22 for Forgejo's git-SSH. See `.env.sample` for the
one-time steps if you want to do the same elsewhere. one-time steps if you want to do the same elsewhere.
Since SSH bypasses Caddy, this instance instead gets a plain Technitium `A` Since SSH bypasses Caddy, this instance instead gets a plain Technitium `A`
record — `forgejo.home.brianpooe.com` → `10.0.10.18` — so clone URLs read record — `forgejo.brianpooe.com` → `10.0.10.18` — so clone URLs read
`ssh://git@forgejo.home.brianpooe.com/<owner>/<repo>.git` instead of the raw `ssh://git@forgejo.brianpooe.com/<owner>/<repo>.git` instead of the raw
LXC IP. Add this in the Technitium admin UI (or via its API) as a normal LXC IP. Add this in the Technitium admin UI (or via its API) as a normal
zone record; it isn't behind the `*.home.brianpooe.com` → Caddy wildcard. zone record; it isn't behind the `*.home.brianpooe.com` → Caddy wildcard.
## Public Exposure
This instance is publicly reachable at `git.brianpooe.com` (HTTPS) and
`ssh://git@forgejo.brianpooe.com` (SSH), proxied through the Hetzner VPS —
see `~/netbird-vps/docs/netbird-acl-playbook.md` and `public-app-sso.md` on
the VPS for the edge side. Unlike other public apps (koffan, IT-Tools), this
one is deliberately **not** behind the oauth2-proxy/Zitadel SSO gate: public
repos need to be browsable/clonable with no login, like GitHub. Forgejo
enforces its own per-repo visibility regardless of network path, so private
repos stay private without an extra gate.
One canonical hostname everywhere — `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` are
`git.brianpooe.com`, not a `.home.` subdomain — resolved via split-horizon
DNS: Technitium answers it directly on the LAN (own zone, `A` record ->
`10.0.15.5`), public DNS (`*.brianpooe.com` wildcard) sends it to the VPS.
Same pattern for `forgejo.brianpooe.com` (`A` record -> `10.0.10.18`
directly, bypassing Caddy, since SSH can't be proxied).
Two settings matter for a public instance and are easy to get wrong:
- `FORGEJO_REQUIRE_SIGNIN_VIEW` **must be `false`**. When `true` it forces
every page (including `/explore` and the `.profile` landing page) through
`/user/login` for anonymous visitors, regardless of `LANDING_PAGE` — it
silently defeats "outsiders can browse public repos."
- `FORGEJO_LANDING_PAGE=explore` so anonymous visitors land on the public
repo listing instead of a login page. On top of that, the Caddy vhost for
`git.brianpooe.com` (`docker-compose-files/caddy/Caddyfile_template`) has
`redir / /brianpooe 302`, so the *root* domain goes straight to the
`.profile` landing page instead of `/explore``/explore` is still one
click away via the nav sidebar. Update/remove that redirect if the
account name or desired landing target ever changes.
**Changing `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` also requires updating the
Zitadel OIDC app's redirect URI**, or sign-in breaks with
`invalid_request: redirect_uri is missing in the client configuration`.
Forgejo's OIDC callback is always `<ROOT_URL>/user/oauth2/zitadel/callback`
— this is Forgejo's own direct Zitadel app (project **Homelab**, app
**Forgejo**), separate from the shared oauth2-proxy app used by
koffan/it-tools. Update it via the Zitadel console or, from the VPS:
```bash
PAT=$(cat ~/netbird-vps/zitadel/zitadel-data/admin.pat)
curl -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-X PUT "https://zitadel.brianpooe.com/management/v1/projects/345934587441971203/apps/382370882418376707/oidc_config" -d '{
"redirectUris": ["https://git.brianpooe.com/user/oauth2/zitadel/callback"],
"postLogoutRedirectUris": ["https://git.brianpooe.com"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER"
}'
```
Public git-over-SSH reaches the LXC via a raw TCP forward on the VPS
(`~/netbird-vps/ssh-forward/`, a `socat` container in `network_mode: host`
listening on the VPS's now-freed port 22) over a NetBird route added
specifically for this (`10.0.10.18/32` on the `homelab-lan` network, routed
through the `pi-caddy` peer — see the VPS's `netbird-acl-playbook.md`
"Model 2" pattern). The VPS's own admin sshd was relocated to port 2200 to
free port 22 for this forward.
## Notes ## Notes
- Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than - Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than

View file

@ -125,7 +125,7 @@ Every policy in this table uses:
## 3a. IoT → CADDY Policies (reverse direction) ## 3a. IoT → CADDY Policies (reverse direction)
The Home Assistant host itself also needs to reach Caddy directly — it The Home Assistant host itself also needs to reach Caddy directly — it
pulls `homelab-blueprint` from Forgejo (`git.home.brianpooe.com`, proxied pulls `homelab-blueprint` from Forgejo (`git.brianpooe.com`, proxied
through Caddy) to deploy its own docker-compose stack. through Caddy) to deploy its own docker-compose stack.
**Two independent layers enforce traffic into the Caddy host — both must **Two independent layers enforce traffic into the Caddy host — both must
@ -176,6 +176,31 @@ to free 22 up. Direct admin SSH to this LXC now needs `-p 2200`; `pct exec
`ufw status` on that host is the fastest way to check which subnets can `ufw status` on that host is the fastest way to check which subnets can
already reach it before assuming a UniFi rule is the problem. already reach it before assuming a UniFi rule is the problem.
## 3c. Public Exposure of Forgejo (Hetzner VPS)
Forgejo is also reachable from the public internet, at `git.brianpooe.com`
(HTTPS, public repos only — anonymous browsing/cloning, like GitHub) and
`ssh://git@forgejo.brianpooe.com` (SSH, requires a registered account key —
self-registration is disabled, so this is effectively you-only). Full detail
lives in `docker-compose-files/forgejo/README.md` ("Public Exposure") and,
on the VPS itself, `~/netbird-vps/docs/netbird-acl-playbook.md` and
`public-app-sso.md`. Summary of what changed on the LAN side of that path:
- **DNS is split-horizon**, not a new firewall rule: `git.brianpooe.com` and
`forgejo.brianpooe.com` are each their own single-name Technitium zone
(`A` record direct to `10.0.15.5` and `10.0.10.18` respectively) so LAN
clients resolve them locally while the public `*.brianpooe.com` wildcard
sends everyone else to the VPS. No UniFi policy changes were needed for
the HTTPS path — it already flows through the existing `ALLOW CADDY to
Forgejo` rule above.
- **The public SSH path is a new NetBird route**, not a UniFi rule: the VPS
NetBird peer previously only routed to the Pi Caddy (`10.0.15.5/32`).
A second `10.0.10.18/32` resource (routing peer `pi-caddy`, "Model 2" in
the playbook) was added so the VPS can reach Forgejo's git-SSH directly.
The VPS forwards its own (now-freed) public port 22 to `10.0.10.18:22`
via a `socat` container (`~/netbird-vps/ssh-forward/`) — the VPS's own
admin sshd was relocated to port 2200 to make room.
--- ---
## 4. Public DNS Block Policies (per zone) ## 4. Public DNS Block Policies (per zone)

View file

@ -5,6 +5,16 @@ Hetzner VPS as the edge. Currently on this path: Koffan
(`koffan.brianpooe.com`) and IT-Tools (`it-tools.brianpooe.com`); the setup is (`koffan.brianpooe.com`) and IT-Tools (`it-tools.brianpooe.com`); the setup is
built to be reused for any future app. built to be reused for any future app.
> [!NOTE]
> Forgejo (`git.brianpooe.com`) also runs through this VPS edge but is
> deliberately **not** on this SSO path — public repos need to be
> browsable/clonable with no login, like GitHub. It gets its own plain
> `reverse_proxy` vhost (`crowdsec` + `rate_limit`, no `forward_auth`) in
> `caddy/Caddyfile.template`. Forgejo enforces its own per-repo visibility
> regardless of network path, so private repos stay private without an SSO
> gate. Its public git-over-SSH path is separate again — a raw TCP forward,
> not Caddy at all — see the VPS's `docs/netbird-acl-playbook.md`.
> [!NOTE] > [!NOTE]
> This intentionally does **not** use NetBird's built-in reverse-proxy > This intentionally does **not** use NetBird's built-in reverse-proxy
> ("Services") feature. Self-hosted NetBird requires running your own proxy > ("Services") feature. Self-hosted NetBird requires running your own proxy