From fef521909b92b22ce696ba8fd399cc2c9af24e8e Mon Sep 17 00:00:00 2001 From: Brian Pooe Date: Sun, 19 Jul 2026 18:34:13 +0200 Subject: [PATCH] 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. --- docker-compose-files/caddy/Caddyfile_template | 13 +++- docker-compose-files/forgejo/.env.sample | 16 +++++ docker-compose-files/forgejo/README.md | 66 ++++++++++++++++++- docs/network/allow-caddy-to-apps.md | 27 +++++++- docs/network/public-app-sso.md | 10 +++ 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/docker-compose-files/caddy/Caddyfile_template b/docker-compose-files/caddy/Caddyfile_template index c06344a..0dc6ae2 100644 --- a/docker-compose-files/caddy/Caddyfile_template +++ b/docker-compose-files/caddy/Caddyfile_template @@ -173,10 +173,17 @@ koffan.{{CADDY_BASE_DOMAIN:-home.example.com}} { reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080 } -# Forgejo (LXC 114, .18). Git-over-SSH (port 2222) is not proxyable through -# Caddy — that port is exposed directly on the LXC for SSH clone/push. -git.{{CADDY_BASE_DOMAIN:-home.example.com}} { +# Forgejo (LXC 114, .18) -- publicly exposed at git.brianpooe.com (proxied via +# the VPS over NetBird for outside visitors) as well as reachable directly on +# 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 + redir / /brianpooe 302 reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000 } diff --git a/docker-compose-files/forgejo/.env.sample b/docker-compose-files/forgejo/.env.sample index d663ba2..d479197 100644 --- a/docker-compose-files/forgejo/.env.sample +++ b/docker-compose-files/forgejo/.env.sample @@ -16,6 +16,13 @@ FORGEJO_DOMAIN=git.home.example.com # 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 # 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 # 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 # `docker exec forgejo forgejo admin user create ...` instead. 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 +# 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 diff --git a/docker-compose-files/forgejo/README.md b/docker-compose-files/forgejo/README.md index f9e2554..357e745 100644 --- a/docker-compose-files/forgejo/README.md +++ b/docker-compose-files/forgejo/README.md @@ -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. 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 -`ssh://git@forgejo.home.brianpooe.com//.git` instead of the raw +record — `forgejo.brianpooe.com` → `10.0.10.18` — so clone URLs read +`ssh://git@forgejo.brianpooe.com//.git` instead of the raw 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. +## 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 `/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 - Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than diff --git a/docs/network/allow-caddy-to-apps.md b/docs/network/allow-caddy-to-apps.md index ef7a381..50259d6 100644 --- a/docs/network/allow-caddy-to-apps.md +++ b/docs/network/allow-caddy-to-apps.md @@ -125,7 +125,7 @@ Every policy in this table uses: ## 3a. IoT → CADDY Policies (reverse direction) 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. **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 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) diff --git a/docs/network/public-app-sso.md b/docs/network/public-app-sso.md index 379f72e..6691788 100644 --- a/docs/network/public-app-sso.md +++ b/docs/network/public-app-sso.md @@ -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 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] > This intentionally does **not** use NetBird's built-in reverse-proxy > ("Services") feature. Self-hosted NetBird requires running your own proxy