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.
132 lines
5.6 KiB
Markdown
132 lines
5.6 KiB
Markdown
# Public App Exposure with Zitadel SSO (oauth2-proxy on the VPS)
|
|
|
|
How LAN apps are exposed to the public internet behind Zitadel SSO, using the
|
|
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
|
|
> cluster, and that proxy manages its own TLS — the edge in front of it must do
|
|
> raw TLS passthrough, which only Traefik supports. Our edge is Caddy +
|
|
> CrowdSec, and replacing it (or losing CrowdSec in front of public apps) was
|
|
> not worth it. oauth2-proxy gives the same Zitadel SSO gate while keeping the
|
|
> existing edge.
|
|
|
|
## Request path
|
|
|
|
```
|
|
Browser
|
|
└─ https://koffan.brianpooe.com (wildcard *.brianpooe.com → VPS)
|
|
└─ VPS Caddy (+ CrowdSec)
|
|
├─ forward_auth → oauth2-proxy:4180 → Zitadel OIDC (SSO gate)
|
|
└─ reverse_proxy https://10.0.15.5 over NetBird (Pi Caddy)
|
|
· Host/SNI rewritten to koffan.home.brianpooe.com
|
|
└─ Pi Caddy → koffan LXC 10.0.10.12:8080
|
|
```
|
|
|
|
Key constraint: the VPS NetBird peer only has a route to the Caddy Pi
|
|
(`10.0.15.5/32`, network "Pi Caddy RS") — it can **not** reach LAN apps
|
|
directly. Every public app is therefore proxied through the Pi Caddy, which
|
|
keeps a single ingress point into the LAN and reuses the existing
|
|
CADDY→TRUSTED firewall policies. No NetBird ACL changes are needed per app.
|
|
|
|
## Components (all on the VPS, `~/netbird-vps/`)
|
|
|
|
| Piece | Where | Notes |
|
|
|---|---|---|
|
|
| oauth2-proxy | `oauth2-proxy/docker-compose.yml.template` | Joins the `caddy` docker network, listens on 4180. All config via `OAUTH2_PROXY_*` env vars in the root `.env`. |
|
|
| Caddy vhost | `caddy/Caddyfile.template` (koffan block) | `forward_auth` + 401→login redirect + proxy to the Pi. |
|
|
| Zitadel app | project **public-apps** → application **oauth2-proxy** | Web / authorization code / PKCE; client secret lives only in the VPS `.env`. |
|
|
|
|
The session cookie domain is `.brianpooe.com`, so once a user has logged in to
|
|
one gated app, other gated apps on subdomains reuse the session.
|
|
`OAUTH2_PROXY_REDIRECT_URL` is deliberately **not** set: the proxy derives the
|
|
OIDC callback from the request host, so one instance serves every gated app —
|
|
each app's callback URL just needs to be registered on the Zitadel application.
|
|
|
|
## The Caddy vhost pattern
|
|
|
|
oauth2-proxy's `/oauth2/auth` endpoint only answers 202 or 401 — it never
|
|
redirects — so the vhost needs an explicit 401 handler to start the login flow:
|
|
|
|
```caddy
|
|
koffan.brianpooe.com {
|
|
import common_tls
|
|
import common_security
|
|
import common_access_log
|
|
|
|
# oauth2-proxy's own endpoints (login start, OIDC callback, sign-out).
|
|
handle /oauth2/* {
|
|
crowdsec
|
|
reverse_proxy oauth2-proxy:4180
|
|
}
|
|
|
|
handle {
|
|
crowdsec
|
|
forward_auth oauth2-proxy:4180 {
|
|
uri /oauth2/auth
|
|
copy_headers X-Auth-Request-User X-Auth-Request-Email
|
|
|
|
@unauthed status 401
|
|
handle_response @unauthed {
|
|
redir * /oauth2/start?rd={scheme}://{host}{uri}
|
|
}
|
|
}
|
|
reverse_proxy https://10.0.15.5 {
|
|
header_up Host koffan.home.brianpooe.com
|
|
transport http {
|
|
tls_server_name koffan.home.brianpooe.com
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The Host/SNI rewrite is what lets the Pi Caddy match its internal
|
|
`koffan.home.brianpooe.com` vhost and present its valid wildcard certificate.
|
|
|
|
## Adding another app
|
|
|
|
1. Register the new callback URL on the existing **oauth2-proxy** app in
|
|
Zitadel (`https://<app>.brianpooe.com/oauth2/callback`). This can be done
|
|
via the management API with the admin service-user PAT stored on the VPS
|
|
(`zitadel/zitadel-data/admin.pat`, service user `automation`, ORG_OWNER).
|
|
2. Copy the koffan block in `caddy/Caddyfile.template`, change the hostname
|
|
and internal target.
|
|
3. Render and restart:
|
|
|
|
```bash
|
|
cd ~/netbird-vps
|
|
./substitute_env.sh caddy/Caddyfile.template caddy/Caddyfile .env
|
|
docker compose restart caddy
|
|
```
|
|
|
|
No DNS work needed — `*.brianpooe.com` already points at the VPS.
|
|
|
|
## Gotchas
|
|
|
|
- **Restart Caddy, don't reload.** `substitute_env.sh` recreates the rendered
|
|
`Caddyfile` with a new inode, but the container bind-mounts the single file.
|
|
`caddy reload` then keeps serving the *old* config with no error. Always
|
|
`docker compose restart caddy` after re-rendering.
|
|
- **Zitadel image has no shell.** `docker exec ... cat` fails; read files from
|
|
the container with `docker cp zitadel-external-tls:/path - | tar -xO`.
|
|
- **The login-client PAT is not an admin.** It only carries
|
|
`IAM_LOGIN_CLIENT` and gets 403 on project/app creation — use the
|
|
`automation` service-user PAT for management API calls.
|
|
- **Double login on koffan.** Koffan's own shared household password is still
|
|
enabled behind the SSO gate. `KOFFAN_DISABLE_AUTH=true` would make Zitadel
|
|
the only gate, but it also removes the password for LAN users who reach
|
|
koffan directly via the Pi Caddy.
|